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/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/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e63484e70..a2a6381ad 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,8 +74,9 @@ 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.") + should_stop = ControlChannel(WormConfiguration.current_server, GUID).should_agent_stop() + if should_stop: + logger.info("The Monkey Island has instructed this agent to stop") return if InfectionMonkey._is_upgrade_to_64_needed(): @@ -126,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() 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/app.py b/monkey/monkey_island/cc/app.py index 333232bd2..ce223bda4 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -8,6 +8,11 @@ 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.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 +35,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 +100,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,30 +128,26 @@ def init_api_resources(api): 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/") - api.add_resource( - Telemetry, "/api/telemetry", "/api/telemetry/", "/api/telemetry/" - ) + api.add_resource(LocalRun, "/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") @@ -158,9 +158,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( @@ -170,10 +170,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/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 24c8363d3..af17e45a2 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, @@ -20,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: @@ -38,7 +43,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 ( @@ -95,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(): + return Monkey.objects(guid=self.parent[0][0]).first() + else: + raise ParentNotFoundError(f"No parent was found for agent with GUID {self.guid}") + def get_os(self): os = "unknown" if self.description.lower().find("linux") != -1: 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/agent_controls/stop_agent_check.py b/monkey/monkey_island/cc/resources/agent_controls/stop_agent_check.py new file mode 100644 index 000000000..3fb948a68 --- /dev/null +++ b/monkey/monkey_island/cc/resources/agent_controls/stop_agent_check.py @@ -0,0 +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): + 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..a8819243b --- /dev/null +++ b/monkey/monkey_island/cc/resources/agent_controls/stop_all_agents.py @@ -0,0 +1,23 @@ +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.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_mutex: + 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": should_agent_die(monkey_guid)} diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 3853b58ed..ae8493398 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_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 @@ -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_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"]) 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/monkey_control/__init__.py b/monkey/monkey_island/cc/resources/monkey_control/__init__.py deleted file mode 100644 index e69de29bb..000000000 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 deleted file mode 100644 index 817d6db94..000000000 --- a/monkey/monkey_island/cc/resources/monkey_control/stop_agent_check.py +++ /dev/null @@ -1,9 +0,0 @@ -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} 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/resources/utils/semaphores.py b/monkey/monkey_island/cc/resources/utils/semaphores.py new file mode 100644 index 000000000..4c9ef5ecc --- /dev/null +++ b/monkey/monkey_island/cc/resources/utils/semaphores.py @@ -0,0 +1,5 @@ +from gevent.lock import BoundedSemaphore + +# Mutex avoids race condition between monkeys +# being marked dead and monkey waking up as alive +agent_killing_mutex = BoundedSemaphore() diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py index 027bd49e2..7aeb1bfcf 100644 --- a/monkey/monkey_island/cc/services/database.py +++ b/monkey/monkey_island/cc/services/database.py @@ -3,6 +3,7 @@ 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.config import ConfigService @@ -23,6 +24,7 @@ class Database(object): if not x.startswith("system.") and not x == AttackMitigations.COLLECTION_NAME ] ConfigService.init_config() + Database.init_agent_controls() logger.info("DB was reset") return jsonify(status="OK") @@ -31,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 5529cc70d..e766d2e14 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,56 @@ 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 not 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 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/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) 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 9007194b0..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,32 +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}) - }); - } - - 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. @@ -193,7 +166,6 @@ class PreviewPaneComponent extends AuthComponent { {this.ipsRow(asset)} {this.servicesRow(asset)} {this.accessibleRow(asset)} - {this.forceKillRow(asset)} {this.downloadLogRow(asset)} 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..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,9 +84,14 @@ class MapPageComponent extends AuthComponent { } killAllMonkeys = () => { - this.authFetch('/api?action=killall') + this.authFetch('/api/monkey_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/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) 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)