diff --git a/monkey/monkey_island/cc/consts.py b/monkey/monkey_island/cc/consts.py index deb1db449..c302f6fb7 100644 --- a/monkey/monkey_island/cc/consts.py +++ b/monkey/monkey_island/cc/consts.py @@ -3,3 +3,4 @@ import os __author__ = 'itay.mizeretz' MONKEY_ISLAND_ABS_PATH = os.path.join(os.getcwd(), 'monkey_island') +DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5 diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 0b910c84b..f1bd12905 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -1,11 +1,11 @@ """ Define a Document Schema for the Monkey document. """ -import mongoengine from mongoengine import Document, StringField, ListField, BooleanField, EmbeddedDocumentField, ReferenceField, \ - DateTimeField + DateTimeField, DynamicField, DoesNotExist -from monkey_island.cc.models.monkey_ttl import MonkeyTtl +from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document +from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS class Monkey(Document): @@ -26,8 +26,11 @@ class Monkey(Document): ip_addresses = ListField(StringField()) keepalive = DateTimeField() modifytime = DateTimeField() - # TODO change this to an embedded document as well - RN it's an unnamed tuple which is confusing. - parent = ListField(ListField(StringField())) + # TODO make "parent" an embedded document, so this can be removed and the schema explained (and validated) verbosly. + # This is a temporary fix, since mongoengine doesn't allow for lists of strings to be null + # (even with required=False of null=True). + # See relevant issue: https://github.com/MongoEngine/mongoengine/issues/1904 + parent = ListField(ListField(DynamicField())) config_error = BooleanField() critical_services = ListField(StringField()) pba_results = ListField() @@ -38,13 +41,22 @@ class Monkey(Document): @staticmethod def get_single_monkey_by_id(db_id): try: - return Monkey.objects(id=db_id)[0] - except IndexError: - raise MonkeyNotFoundError("id: {0}".format(str(db_id))) + return Monkey.objects.get(id=db_id) + except DoesNotExist as ex: + raise MonkeyNotFoundError("info: {0} | id: {1}".format(ex.message, str(db_id))) + + @staticmethod + def get_single_monkey_by_guid(monkey_guid): + try: + return Monkey.objects.get(guid=monkey_guid) + except DoesNotExist as ex: + raise MonkeyNotFoundError("info: {0} | guid: {1}".format(ex.message, str(monkey_guid))) @staticmethod def get_latest_modifytime(): - return Monkey.objects.order_by('-modifytime').first().modifytime + if Monkey.objects.count() > 0: + return Monkey.objects.order_by('-modifytime').first().modifytime + return None def is_dead(self): monkey_is_dead = False @@ -55,7 +67,7 @@ class Monkey(Document): if MonkeyTtl.objects(id=self.ttl_ref.id).count() == 0: # No TTLs - monkey has timed out. The monkey is MIA. monkey_is_dead = True - except (mongoengine.DoesNotExist, AttributeError): + except (DoesNotExist, AttributeError): # Trying to dereference unknown document - the monkey is MIA. monkey_is_dead = True return monkey_is_dead @@ -68,6 +80,10 @@ class Monkey(Document): os = "windows" return os + def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS): + self.ttl_ref = create_monkey_ttl_document(duration) + self.save() + class MonkeyNotFoundError(Exception): pass diff --git a/monkey/monkey_island/cc/models/monkey_ttl.py b/monkey/monkey_island/cc/models/monkey_ttl.py index 9ccf77974..b3e59d5ed 100644 --- a/monkey/monkey_island/cc/models/monkey_ttl.py +++ b/monkey/monkey_island/cc/models/monkey_ttl.py @@ -38,3 +38,16 @@ class MonkeyTtl(Document): } expire_at = DateTimeField() + + +def create_monkey_ttl_document(expiry_duration_in_seconds): + """ + Create a new Monkey TTL document and save it as a document. + :param expiry_duration_in_seconds: How long should the TTL last for. THIS IS A LOWER BOUND - depends on mongodb + performance. + :return: The TTL document. To get its ID use `.id`. + """ + # The TTL data uses the new `models` module which depends on mongoengine. + current_ttl = MonkeyTtl.create_ttl_expire_in(expiry_duration_in_seconds) + current_ttl.save() + return current_ttl diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py index a744db6b6..717fb309a 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -46,6 +46,19 @@ class TestMonkey(IslandTestCase): self.assertTrue(mia_monkey.is_dead()) self.assertFalse(alive_monkey.is_dead()) + def test_ttl_renewal(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + # Arrange + monkey = Monkey(guid=str(uuid.uuid4())) + monkey.save() + self.assertIsNone(monkey.ttl_ref) + + # act + assert + monkey.renew_ttl() + self.assertIsNotNone(monkey.ttl_ref) + def test_get_single_monkey_by_id(self): self.fail_if_not_testing_env() self.clean_monkey_db() diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 36720e465..8e523a8a7 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -5,26 +5,17 @@ import dateutil.parser import flask_restful from flask import request +from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS from monkey_island.cc.database import mongo -from monkey_island.cc.models.monkey_ttl import MonkeyTtl +from monkey_island.cc.models.monkey_ttl import create_monkey_ttl_document from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService -MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5 - __author__ = 'Barak' # TODO: separate logic from interface -def create_monkey_ttl(): - # The TTL data uses the new `models` module which depends on mongoengine. - current_ttl = MonkeyTtl.create_ttl_expire_in(MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) - current_ttl.save() - ttlid = current_ttl.id - return ttlid - - class Monkey(flask_restful.Resource): # Used by monkey. can't secure. @@ -58,8 +49,8 @@ class Monkey(flask_restful.Resource): tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip) - ttlid = create_monkey_ttl() - update['$set']['ttl_ref'] = ttlid + ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) + update['$set']['ttl_ref'] = ttl.id return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) @@ -120,7 +111,8 @@ class Monkey(flask_restful.Resource): tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") monkey_json.pop('tunnel') - monkey_json['ttl_ref'] = create_monkey_ttl() + 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}, diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index d4aaa72df..279547dc1 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -15,10 +15,10 @@ from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.node import NodeService from monkey_island.cc.encryptor import encryptor from monkey_island.cc.services.wmi_handler import WMIHandler +from monkey_island.cc.models.monkey import Monkey __author__ = 'Barak' - logger = logging.getLogger(__name__) @@ -49,6 +49,9 @@ class Telemetry(flask_restful.Resource): telemetry_json = json.loads(request.data) telemetry_json['timestamp'] = datetime.now() + # Monkey communicated, so it's alive. Update the TTL. + Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).renew_ttl() + monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) try: @@ -59,7 +62,7 @@ class Telemetry(flask_restful.Resource): else: logger.info('Got unknown type of telemetry: %s' % telem_category) except Exception as ex: - logger.error("Exception caught while processing telemetry", exc_info=True) + logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True) telem_id = mongo.db.telemetry.insert(telemetry_json) return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) @@ -188,7 +191,7 @@ class Telemetry(flask_restful.Resource): Telemetry.add_system_info_creds_to_config(creds) Telemetry.replace_user_dot_with_comma(creds) if 'mimikatz' in telemetry_json['data']: - users_secrets = mimikatz_utils.MimikatzSecrets.\ + users_secrets = mimikatz_utils.MimikatzSecrets. \ extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) if 'wmi' in telemetry_json['data']: wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index a4f090758..e271c45c5 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime import dateutil @@ -9,6 +10,8 @@ from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo from monkey_island.cc.services.node import NodeService +logger = logging.getLogger(__name__) + __author__ = 'itay.mizeretz' @@ -23,11 +26,15 @@ class TelemetryFeed(flask_restful.Resource): telemetries = telemetries.sort([('timestamp', flask_pymongo.ASCENDING)]) - return \ - { - 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries], - 'timestamp': datetime.now().isoformat() - } + try: + return \ + { + 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries], + 'timestamp': datetime.now().isoformat() + } + except KeyError as err: + logger.error("Failed parsing telemetries. Error: {0}.".format(err.message)) + return {'telemetries': [], 'timestamp': datetime.now().isoformat()} @staticmethod def get_displayed_telemetry(telem):