Merge pull request #389 from guardicore/bugfix/374-monkey-telemetry-revival

Bugfix/374 monkey telemetry revival
This commit is contained in:
Shay Nehmad 2019-07-23 13:24:51 +03:00 committed by GitHub
commit 6b353b7a7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 32 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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},

View File

@ -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)

View File

@ -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):