Fixed segmentation findings to use the same infrastructure as other findings.

Small segmentation finding bugfix
This commit is contained in:
VakarisZ 2020-10-13 17:44:25 +03:00
parent eac960c73d
commit eb5648dc0e
7 changed files with 64 additions and 104 deletions

View File

@ -1,8 +1,10 @@
from __future__ import annotations
from typing import List from typing import List
from mongoengine import Document, EmbeddedDocumentListField from mongoengine import Document, EmbeddedDocumentListField
from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.event import Event
from monkey_island.cc.models.zero_trust.subnet_pair import SubnetPair
class MonkeyFindingDetails(Document): class MonkeyFindingDetails(Document):
@ -13,8 +15,16 @@ class MonkeyFindingDetails(Document):
# SCHEMA # SCHEMA
events = EmbeddedDocumentListField(document_type=Event, required=False) events = EmbeddedDocumentListField(document_type=Event, required=False)
checked_subnet_pairs = EmbeddedDocumentListField(document_type=SubnetPair, required=False)
# LOGIC # LOGIC
def add_events(self, events: List[Event]) -> None: def add_events(self, events: List[Event]) -> MonkeyFindingDetails:
self.update(push_all__events=events) self.update(push_all__events=events)
return self return self
def add_checked_subnet_pair(self, subnet_pair: SubnetPair) -> MonkeyFindingDetails:
self.update(push__checked_subnet_pairs=subnet_pair)
return self
def is_with_subnet_pair(self, subnet_pair: SubnetPair):
return subnet_pair in self.checked_subnet_pairs

View File

@ -1,50 +0,0 @@
from mongoengine import StringField
import common.common_consts.zero_trust_consts as zero_trust_consts
from monkey_island.cc.models.zero_trust.finding import Finding
def need_to_overwrite_status(saved_status, new_status):
return (saved_status == zero_trust_consts.STATUS_PASSED) and (new_status == zero_trust_consts.STATUS_FAILED)
class SegmentationFinding(Finding):
first_subnet = StringField()
second_subnet = StringField()
@staticmethod
def create_or_add_to_existing_finding(subnets, status, segmentation_event):
"""
Creates a segmentation finding. If a segmentation finding with the relevant subnets already exists, adds the
event to the existing finding, and the "worst" status is chosen (i.e. if the existing one is "Failed" it will
remain so).
:param subnets: the 2 subnets of this finding.
:param status: STATUS_PASSED or STATUS_FAILED
:param segmentation_event: The specific event
"""
assert len(subnets) == 2
# Sort them so A -> B and B -> A segmentation findings will be the same one.
subnets.sort()
existing_findings = SegmentationFinding.objects(first_subnet=subnets[0], second_subnet=subnets[1])
if len(existing_findings) == 0:
# No finding exists - create.
new_finding = SegmentationFinding(
first_subnet=subnets[0],
second_subnet=subnets[1],
test=zero_trust_consts.TEST_SEGMENTATION,
status=status,
events=[segmentation_event]
)
new_finding.save()
else:
# A finding exists (should be one). Add the event to it.
assert len(existing_findings) == 1
existing_finding = existing_findings[0]
existing_finding.events.append(segmentation_event)
if need_to_overwrite_status(existing_finding.status, status):
existing_finding.status = status
existing_finding.save()

View File

@ -0,0 +1,19 @@
from typing import List
from mongoengine import EmbeddedDocument, StringField
class SubnetPair(EmbeddedDocument):
"""
This model represents a pair of subnets. It is meant to hold details about cross-segmentation check between two
subnets.
"""
# SCHEMA
first_subnet = StringField()
second_subnet = StringField()
# LOGIC
@staticmethod
def create_subnet_pair(subnets: List[str]):
subnets.sort()
return SubnetPair(first_subnet=subnets[0], second_subnet=subnets[1])

View File

@ -5,8 +5,8 @@ from common.network.network_range import NetworkRange
from common.network.segmentation_utils import get_ip_if_in_subnet, get_ip_in_src_and_not_in_dst from common.network.segmentation_utils import get_ip_if_in_subnet, get_ip_in_src_and_not_in_dst
from monkey_island.cc.models import Monkey from monkey_island.cc.models import Monkey
from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.event import Event
from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding
from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups
from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService
SEGMENTATION_DONE_EVENT_TEXT = "Monkey on {hostname} is done attempting cross-segment communications " \ SEGMENTATION_DONE_EVENT_TEXT = "Monkey on {hostname} is done attempting cross-segment communications " \
"from `{src_seg}` segments to `{dst_seg}` segments." "from `{src_seg}` segments to `{dst_seg}` segments."
@ -26,10 +26,11 @@ def check_segmentation_violation(current_monkey, target_ip):
target_subnet = subnet_pair[1] target_subnet = subnet_pair[1]
if is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet): if is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet):
event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet) event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet)
SegmentationFinding.create_or_add_to_existing_finding( MonkeyFindingService.create_or_add_to_existing(
subnets=[source_subnet, target_subnet], test=zero_trust_consts.TEST_SEGMENTATION,
subnet_pairs=[[source_subnet, target_subnet]],
status=zero_trust_consts.STATUS_FAILED, status=zero_trust_consts.STATUS_FAILED,
segmentation_event=event events=[event]
) )
@ -90,10 +91,11 @@ def create_or_add_findings_for_all_pairs(all_subnets, current_monkey):
all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets) all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets)
for subnet_pair in all_subnets_pairs_for_this_monkey: for subnet_pair in all_subnets_pairs_for_this_monkey:
SegmentationFinding.create_or_add_to_existing_finding( MonkeyFindingService.create_or_add_to_existing(
subnets=list(subnet_pair), subnet_pairs=[list(subnet_pair)],
status=zero_trust_consts.STATUS_PASSED, status=zero_trust_consts.STATUS_PASSED,
segmentation_event=get_segmentation_done_event(current_monkey, subnet_pair) events=[get_segmentation_done_event(current_monkey, subnet_pair)],
test=zero_trust_consts.TEST_SEGMENTATION
) )

View File

@ -1,36 +0,0 @@
from typing import List
from bson import ObjectId
from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails
# How many events of a single finding to return to UI.
# 50 will return 50 latest and 50 oldest events from a finding
EVENT_FETCH_CNT = 50
class EventsService:
@staticmethod
def fetch_events_for_display(finding_id: ObjectId):
pipeline = [{'$match': {'_id': finding_id}},
{'$addFields': {'oldest_events': {'$slice': ['$events', EVENT_FETCH_CNT]},
'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]},
'event_count': {'$size': '$events'}}},
{'$unset': ['events']}]
details = list(MonkeyFindingDetails.objects.aggregate(*pipeline))
if details:
details = details[0]
details['latest_events'] = EventsService._get_events_without_overlap(details['event_count'],
details['latest_events'])
return details
@staticmethod
def _get_events_without_overlap(event_count: int, events: List[object]) -> List[object]:
overlap_count = event_count - EVENT_FETCH_CNT
if overlap_count >= EVENT_FETCH_CNT:
return events
elif overlap_count <= 0:
return []
else:
return events[-1 * overlap_count:]

View File

@ -2,7 +2,7 @@ from typing import List
from common.common_consts import zero_trust_consts from common.common_consts import zero_trust_consts
from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.finding import Finding
from monkey_island.cc.services.zero_trust.events_service import EventsService from monkey_island.cc.services.zero_trust.monkey_details_service import MonkeyDetailsService
class FindingService: class FindingService:
@ -13,7 +13,7 @@ class FindingService:
details = [] details = []
for i in range(len(findings)): for i in range(len(findings)):
if findings[i].type == zero_trust_consts.MONKEY_FINDING: if findings[i].type == zero_trust_consts.MONKEY_FINDING:
details = EventsService.fetch_events_for_display(findings[i].details.id) details = MonkeyDetailsService.fetch_details_for_display(findings[i].details.id)
elif findings[i].type == zero_trust_consts.SCOUTSUITE_FINDING: elif findings[i].type == zero_trust_consts.SCOUTSUITE_FINDING:
details = findings[i].details.fetch().to_mongo() details = findings[i].details.fetch().to_mongo()
findings[i] = findings[i].to_mongo() findings[i] = findings[i].to_mongo()

View File

@ -6,12 +6,13 @@ from common.common_consts import zero_trust_consts
from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.event import Event
from monkey_island.cc.models.zero_trust.finding import Finding 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.monkey_finding_details import MonkeyFindingDetails
from monkey_island.cc.models.zero_trust.subnet_pair import SubnetPair
class MonkeyFindingService: class MonkeyFindingService:
@staticmethod @staticmethod
def create_or_add_to_existing(test, status, events): def create_or_add_to_existing(test, status, events, subnet_pairs=None):
""" """
Create a new finding or add the events to an existing one if it's the same (same meaning same status and same Create a new finding or add the events to an existing one if it's the same (same meaning same status and same
test). test).
@ -23,15 +24,18 @@ class MonkeyFindingService:
assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status)
if len(existing_findings) == 0: if len(existing_findings) == 0:
MonkeyFindingService.create_new_finding(test, status, events) MonkeyFindingService.create_new_finding(test, status, events, subnet_pairs)
else: else:
# Now we know for sure this is the only one # Now we know for sure this is the only one
MonkeyFindingService.add_events(existing_findings[0], events) MonkeyFindingService.add_events(existing_findings[0], events)
if subnet_pairs:
MonkeyFindingService.add_subnet_pairs(existing_findings[0], subnet_pairs)
@staticmethod @staticmethod
def create_new_finding(test: str, status: str, events: List[Event]): def create_new_finding(test: str, status: str, events: List[Event], subnet_pairs: List[SubnetPair]):
details = MonkeyFindingDetails() details = MonkeyFindingDetails()
details.events = events details.events = events
details.subnet_pairs = subnet_pairs
details.save() details.save()
Finding.save_finding(test, status, details) Finding.save_finding(test, status, details)
@ -39,6 +43,20 @@ class MonkeyFindingService:
def add_events(finding: Finding, events: List[Event]): def add_events(finding: Finding, events: List[Event]):
finding.details.fetch().add_events(events).save() finding.details.fetch().add_events(events).save()
@staticmethod
def add_subnet_pairs(finding: Finding, subnet_pairs: List[List[str]]):
finding_details = finding.details.fetch()
for subnet_pair in subnet_pairs:
subnet_pair_document = SubnetPair.create_subnet_pair(subnet_pair)
if not MonkeyFindingService.is_subnet_pair_in_finding(finding, subnet_pair_document):
finding_details.add_checked_subnet_pair(subnet_pair_document)
finding_details.save()
@staticmethod
def is_subnet_pair_in_finding(finding: Finding, subnet_pair: SubnetPair):
details = finding.details.fetch()
return details.is_with_subnet_pair(subnet_pair)
@staticmethod @staticmethod
def get_events_by_finding(finding_id: str) -> List[object]: def get_events_by_finding(finding_id: str) -> List[object]:
finding = Finding.objects.get(id=finding_id) finding = Finding.objects.get(id=finding_id)
@ -50,8 +68,5 @@ class MonkeyFindingService:
@staticmethod @staticmethod
def add_malicious_activity_to_timeline(events): def add_malicious_activity_to_timeline(events):
MonkeyFindingService.create_or_add_to_existing( MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE,
test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE, status=zero_trust_consts.STATUS_VERIFY, events=events)
status=zero_trust_consts.STATUS_VERIFY,
events=events
)