diff --git a/monkey/common/network/segmentation_utils.py b/monkey/common/network/segmentation_utils.py new file mode 100644 index 000000000..6569d636b --- /dev/null +++ b/monkey/common/network/segmentation_utils.py @@ -0,0 +1,18 @@ +def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): + """ + Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. + :param ip_addresses: List[str]: List of IP addresses to test. + :param source_subnet: NetworkRange: Subnet to want an IP to not be in. + :param target_subnet: NetworkRange: Subnet we want an IP to be in. + :return: The cross segment IP if in source but not in target, else None. Union[str, None] + """ + if get_ip_if_in_subnet(ip_addresses, target_subnet) is not None: + return None + return get_ip_if_in_subnet(ip_addresses, source_subnet) + + +def get_ip_if_in_subnet(ip_addresses, source_subnet): + for ip_address in ip_addresses: + if source_subnet.is_in_range(ip_address): + return ip_address + return None diff --git a/monkey/common/network/segmentation_utils_test.py b/monkey/common/network/segmentation_utils_test.py new file mode 100644 index 000000000..7ef3e4450 --- /dev/null +++ b/monkey/common/network/segmentation_utils_test.py @@ -0,0 +1,19 @@ +from common.network.network_range import * +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestSegmentationUtils(IslandTestCase): + def test_get_ip_in_src_and_not_in_dst(self): + self.fail_if_not_testing_env() + source = CidrRange("1.1.1.0/24") + target = CidrRange("2.2.2.0/24") + self.assertIsNone(get_ip_in_src_and_not_in_dst( + [text_type("2.2.2.2")], source, target + )) + self.assertIsNone(get_ip_in_src_and_not_in_dst( + [text_type("3.3.3.3"), text_type("4.4.4.4")], source, target + )) + self.assertIsNotNone(get_ip_in_src_and_not_in_dst( + [text_type("8.8.8.8"), text_type("1.1.1.1")], source, target + )) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 692e278fb..3cd20d9c2 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -113,7 +113,7 @@ class InfectionMonkey(object): if monkey_tunnel: monkey_tunnel.start() - StateTelem(False).send() + StateTelem(is_done=False).send() TunnelTelem().send() if WormConfiguration.collect_system_info: @@ -225,7 +225,7 @@ class InfectionMonkey(object): InfectionMonkey.close_tunnel() firewall.close() else: - StateTelem(False).send() # Signal the server (before closing the tunnel) + StateTelem(is_done=True).send() # Signal the server (before closing the tunnel) InfectionMonkey.close_tunnel() firewall.close() if WormConfiguration.send_log_to_server: diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index c232ab975..31d7332bd 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -1,7 +1,11 @@ import abc +import json +import logging from infection_monkey.control import ControlClient +logger = logging.getLogger(__name__) + __author__ = 'itay.mizeretz' @@ -19,7 +23,9 @@ class BaseTelem(object): """ Sends telemetry to island """ - ControlClient.send_telemetry(self.telem_category, self.get_data()) + data = self.get_data() + logger.debug("Sending {} telemetry. Data: {}".format(self.telem_category, json.dumps(data))) + ControlClient.send_telemetry(self.telem_category, data) @abc.abstractproperty def telem_category(self): diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index 1869d6f18..5454ad9e1 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -32,6 +32,8 @@ class Finding(Document): test = StringField(required=True, choices=TESTS) status = StringField(required=True, choices=ORDERED_TEST_STATUSES) events = EmbeddedDocumentListField(document_type=Event) + # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance + meta = {'allow_inheritance': True} # LOGIC def get_test_explanation(self): diff --git a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py new file mode 100644 index 000000000..428af72cb --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py @@ -0,0 +1,52 @@ +from mongoengine import StringField + +from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_CONCLUSIVE, STATUS_POSITIVE +from monkey_island.cc.models.zero_trust.finding import Finding + + +def need_to_overwrite_status(saved_status, new_status): + return (saved_status == STATUS_POSITIVE) and (new_status == STATUS_CONCLUSIVE) + + +class SegmentationFinding(Finding): + """ + trying to add conclusive: + If the finding doesn't exist at all: create conclusive + else: + if positive, turn to conclusive + add event + + trying to add positive: + If the finding doesn't exist at all: create positive + else: add event + """ + first_subnet = StringField() + second_subnet = StringField() + + @staticmethod + def create_or_add_to_existing_finding(subnets, status, segmentation_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=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() diff --git a/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py new file mode 100644 index 000000000..ad3ff9b97 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py @@ -0,0 +1,52 @@ +from common.data.zero_trust_consts import STATUS_CONCLUSIVE, EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding + + +class TestSegmentationFinding(IslandTestCase): + def test_create_or_add_to_existing_finding(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + first_segment = "1.1.1.0/24" + second_segment = "2.2.2.0-2.2.2.254" + third_segment = "3.3.3.3" + event = Event.create_event("bla", "bla", EVENT_TYPE_MONKEY_NETWORK) + + SegmentationFinding.create_or_add_to_existing_finding( + subnets=[first_segment, second_segment], + status=STATUS_CONCLUSIVE, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 1) + self.assertEquals(len(SegmentationFinding.objects()[0].events), 1) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[second_segment, first_segment], + status=STATUS_CONCLUSIVE, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 1) + self.assertEquals(len(SegmentationFinding.objects()[0].events), 2) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[first_segment, third_segment], + status=STATUS_CONCLUSIVE, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 2) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[second_segment, third_segment], + status=STATUS_CONCLUSIVE, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 3) diff --git a/monkey/monkey_island/cc/services/configuration/__init__.py b/monkey/monkey_island/cc/services/configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py new file mode 100644 index 000000000..34d6a9bb5 --- /dev/null +++ b/monkey/monkey_island/cc/services/configuration/utils.py @@ -0,0 +1,5 @@ +from monkey_island.cc.services.config import ConfigService + + +def get_config_network_segments_as_subnet_groups(): + return [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 1d7ac162d..fdba3b549 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -9,10 +9,12 @@ from enum import Enum from six import text_type +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.report_exporter_manager import ReportExporterManager from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.node import NodeService from monkey_island.cc.utils import local_ip_addresses, get_subnets @@ -423,23 +425,6 @@ class ReportService: return issues - @staticmethod - def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): - """ - Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. - :param ip_addresses: List of IP addresses to test. - :param source_subnet: Subnet to want an IP to not be in. - :param target_subnet: Subnet we want an IP to be in. - :return: - """ - for ip_address in ip_addresses: - if target_subnet.is_in_range(ip_address): - return None - for ip_address in ip_addresses: - if source_subnet.is_in_range(ip_address): - return ip_address - return None - @staticmethod def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range): """ @@ -502,9 +487,9 @@ class ReportService: target_ip = scan['data']['machine']['ip_addr'] if target_subnet_range.is_in_range(text_type(target_ip)): monkey = NodeService.get_monkey_by_guid(scan['monkey_guid']) - cross_segment_ip = ReportService.get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], - source_subnet_range, - target_subnet_range) + cross_segment_ip = get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], + source_subnet_range, + target_subnet_range) if cross_segment_ip is not None: cross_segment_issues.append( @@ -552,7 +537,7 @@ class ReportService: cross_segment_issues = [] # For now the feature is limited to 1 group. - subnet_groups = [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] + subnet_groups = get_config_network_segments_as_subnet_groups() for subnet_group in subnet_groups: cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py index 3b532ff22..8ae386388 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scan.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py @@ -3,11 +3,13 @@ import copy from monkey_island.cc.database import mongo from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry from monkey_island.cc.services.telemetry.zero_trust_tests.data_endpoints import test_open_data_endpoints +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import test_segmentation_violation def process_scan_telemetry(telemetry_json): update_edges_and_nodes_based_on_scan_telemetry(telemetry_json) test_open_data_endpoints(telemetry_json) + test_segmentation_violation(telemetry_json) def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json): diff --git a/monkey/monkey_island/cc/services/telemetry/processing/state.py b/monkey/monkey_island/cc/services/telemetry/processing/state.py index ac8e32939..46176c9b9 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/state.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/state.py @@ -1,4 +1,6 @@ from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import \ + test_positive_findings_for_unreached_segments def process_state_telemetry(telemetry_json): @@ -8,3 +10,6 @@ def process_state_telemetry(telemetry_json): NodeService.set_monkey_dead(monkey, True) else: NodeService.set_monkey_dead(monkey, False) + + if telemetry_json['data']['done']: + test_positive_findings_for_unreached_segments(telemetry_json) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py new file mode 100644 index 000000000..bb447d992 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py @@ -0,0 +1,100 @@ +import itertools +from six import text_type + +from common.data.zero_trust_consts import STATUS_CONCLUSIVE, EVENT_TYPE_MONKEY_NETWORK, STATUS_POSITIVE, \ + EVENT_TYPE_ISLAND +from common.network.network_range import NetworkRange +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst, get_ip_if_in_subnet +from monkey_island.cc.models import Monkey +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 + +SEGMENTATION_VIOLATION_EVENT_TEXT = \ + "Segmentation violation! Monkey on '{hostname}', with the {source_ip} IP address (in segment {source_seg}) " \ + "managed to communicate cross segment to {target_ip} (in segment {target_seg})." + + +def is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet): + if source_subnet == target_subnet: + return False + source_subnet_range = NetworkRange.get_range_obj(source_subnet) + target_subnet_range = NetworkRange.get_range_obj(target_subnet) + + if target_subnet_range.is_in_range(text_type(target_ip)): + cross_segment_ip = get_ip_in_src_and_not_in_dst( + current_monkey.ip_addresses, + source_subnet_range, + target_subnet_range) + + return cross_segment_ip is not None + + +def test_segmentation_violation(scan_telemetry_json): + # TODO - lower code duplication between this and report.py. + # TODO - single machine + current_monkey = Monkey.get_single_monkey_by_guid(scan_telemetry_json['monkey_guid']) + target_ip = scan_telemetry_json['data']['machine']['ip_addr'] + subnet_groups = get_config_network_segments_as_subnet_groups() + for subnet_group in subnet_groups: + subnet_pairs = itertools.product(subnet_group, subnet_group) + for subnet_pair in subnet_pairs: + source_subnet = subnet_pair[0] + target_subnet = subnet_pair[1] + 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) + SegmentationFinding.create_or_add_to_existing_finding( + subnets=[source_subnet, target_subnet], + status=STATUS_CONCLUSIVE, + segmentation_event=event + ) + + +def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet): + return Event.create_event( + title="Segmentation event", + message=SEGMENTATION_VIOLATION_EVENT_TEXT.format( + hostname=current_monkey.hostname, + source_ip=get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(source_subnet)), + source_seg=source_subnet, + target_ip=target_ip, + target_seg=target_subnet + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + ) + + +def test_positive_findings_for_unreached_segments(state_telemetry_json): + flat_all_subnets = [item for sublist in get_config_network_segments_as_subnet_groups() for item in sublist] + current_monkey = Monkey.get_single_monkey_by_guid(state_telemetry_json['monkey_guid']) + create_or_add_findings_for_all_pairs(flat_all_subnets, current_monkey) + + +def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): + # Filter the subnets that this monkey is part of. + this_monkey_subnets = [] + for subnet in all_subnets: + if get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(subnet)) is not None: + this_monkey_subnets.append(subnet) + + # Get all the other subnets. + other_subnets = list(set(all_subnets) - set(this_monkey_subnets)) + + # Calculate the cartesian product - (this monkey subnets X other subnets). These pairs are the pairs that the monkey + # should have tested. + all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets) + + for subnet_pair in all_subnets_pairs_for_this_monkey: + SegmentationFinding.create_or_add_to_existing_finding( + subnets=list(subnet_pair), + status=STATUS_POSITIVE, + segmentation_event=Event.create_event( + "Segmentation test done", + message="Monkey on {hostname} is done attempting cross-segment communications from `{src_seg}` " + "segments to `{dst_seg}` segments.".format( + hostname=current_monkey.hostname, + src_seg=subnet_pair[0], + dst_seg=subnet_pair[1]), + event_type=EVENT_TYPE_ISLAND + ) + ) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py new file mode 100644 index 000000000..f345d4482 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py @@ -0,0 +1,51 @@ +import uuid + +from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_POSITIVE, STATUS_CONCLUSIVE, \ + EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models import Monkey +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.segmentation_finding import SegmentationFinding +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import create_or_add_findings_for_all_pairs +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + +FIRST_SUBNET = "1.1.1.1" +SECOND_SUBNET = "2.2.2.0/24" +THIRD_SUBNET = "3.3.3.3-3.3.3.200" + + +class TestSegmentationTests(IslandTestCase): + def test_create_findings_for_all_done_pairs(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET] + + monkey = Monkey( + guid=str(uuid.uuid4()), + ip_addresses=[FIRST_SUBNET]) + + # no findings + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0) + + # This is like the monkey is done and sent done telem + create_or_add_findings_for_all_pairs(all_subnets, monkey) + + # There are 2 subnets in which the monkey is NOT + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_POSITIVE)), 2) + + # This is a monkey from 2nd subnet communicated with 1st subnet. + SegmentationFinding.create_or_add_to_existing_finding( + [FIRST_SUBNET, SECOND_SUBNET], + STATUS_CONCLUSIVE, + Event.create_event(title="sdf", message="asd", event_type=EVENT_TYPE_MONKEY_NETWORK) + ) + + print("Printing all segmentation findings") + all_findings = Finding.objects(test=TEST_SEGMENTATION) + for f in all_findings: + print(f.to_json()) + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_POSITIVE)), 1) + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_CONCLUSIVE)), 1) + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 2) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 09ccebad9..f366d73bd 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -3087,8 +3087,7 @@ "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" }, "commondir": { "version": "1.0.1", @@ -3657,6 +3656,270 @@ "es5-ext": "^0.10.9" } }, + "d3": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.11.0.tgz", + "integrity": "sha512-LXgMVUAEAzQh6WfEEOa8tJX4RA64ZJ6twC3CJ+Xzid+fXWLTZkkglagXav/eOoQgzQi5rzV0xC4Sfspd6hFDHA==", + "requires": { + "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" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz", + "integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "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==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz", + "integrity": "sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg==" + }, + "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==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", + "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==" + }, + "d3-drag": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz", + "integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", + "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==" + }, + "d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz", + "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==" + }, + "d3-geo": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz", + "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==" + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + }, + "d3-polygon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", + "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==" + }, + "d3-quadtree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz", + "integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA==" + }, + "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==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "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==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", + "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==" + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", + "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==" + }, + "d3-transition": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", + "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "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==" + }, + "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==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -8419,8 +8682,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -8441,14 +8703,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8463,20 +8723,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -8593,8 +8850,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -8606,7 +8862,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8621,7 +8876,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8629,14 +8883,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -8655,7 +8907,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -8736,8 +8987,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -8749,7 +8999,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8835,8 +9084,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -8872,7 +9120,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8892,7 +9139,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8936,14 +9182,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -15091,6 +15335,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rxjs": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", @@ -19041,6 +19290,7 @@ "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -19209,6 +19459,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 1a60ee27c..872a22bdc 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -70,6 +70,7 @@ "bootstrap": "3.4.1", "classnames": "^2.2.6", "core-js": "^2.5.7", + "d3": "^5.11.0", "downloadjs": "^1.4.7", "fetch": "^1.1.0", "file-saver": "^2.0.2", diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js old mode 100644 new mode 100755 diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js index e2b16c91b..824885cad 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js @@ -1,7 +1,7 @@ import React, {Component} from "react"; import PillarLabel from "./PillarLabel"; -import PaginatedTable from "../common/PaginatedTable"; import * as PropTypes from "prop-types"; +import ResponsiveVennDiagram from "./venn-components/ResponsiveVennDiagram"; const columns = [ { @@ -20,13 +20,8 @@ const columns = [ class PillarOverview extends Component { render() { - const data = this.props.grades.map((grade) => { - const newGrade = JSON.parse(JSON.stringify(grade)); - newGrade.pillar = {name: grade.pillar, status: this.props.pillarsToStatuses[grade.pillar]}; - return newGrade; - }); return (
- +
); } } @@ -35,5 +30,4 @@ export default PillarOverview; PillarOverview.propTypes = { grades: PropTypes.array, - status: PropTypes.string, }; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store new file mode 100644 index 000000000..344923cf9 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store differ diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js new file mode 100644 index 000000000..95d86db2f --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js @@ -0,0 +1,38 @@ +import React from 'react' +import PropTypes from 'prop-types'; +import * as d3 from 'd3' + +class ArcNode extends React.Component { + render() { + let {prefix, index, data} = this.props; + + let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0); + let id = prefix + 'Node_' + index; + + return ( + + + + + {data.label} + + + + ); + } +} + +ArcNode.propTypes = { + data: PropTypes.object +}; + +export default ArcNode; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js new file mode 100644 index 000000000..43ef529f3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js @@ -0,0 +1,43 @@ +import React from 'react' +import PropTypes from 'prop-types'; + +class CircularNode extends React.Component { + render() { + let {prefix, index, data} = this.props; + + let tspans = data.label.split("|").map((d_, i_) => { + let halfTextHeight = data.fontStyle.size * data.label.split("|").length / 2; + let key = 'vennDiagramCircularNode' + index + '_Tspan' + i_; + + return ( + {d_} + ); + }); + + let translate = 'translate(' + data.cx + ',' + data.cy + ')'; + + return ( + + + + {tspans} + + + ); + } +} + +CircularNode.propTypes = { + index: PropTypes.number, + data: PropTypes.object +}; + +export default CircularNode; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js new file mode 100644 index 000000000..4b2069f06 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Dimensions from 'react-dimensions' +import VennDiagram from './VennDiagram' + +const VENN_MIN_WIDTH = '300px'; + +class ResponsiveVennDiagram extends React.Component { + constructor(props) { + super(props); + } + + render() { + const {pillarsGrades} = this.props; + + return ( +
+ +
+ ); + } +} + +ResponsiveVennDiagram.propTypes = { + pillarsGrades: PropTypes.array +}; + +export default Dimensions()(ResponsiveVennDiagram); diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Tooltip.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Tooltip.js new file mode 100644 index 000000000..e103af3c3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Tooltip.js @@ -0,0 +1,43 @@ +import React from 'react' +import PropTypes from 'prop-types'; + +class Tooltip extends React.Component { + + render() { + const {prefix, bcolor, top, left, display, html} = this.props; + + const style = { + backgroundColor: bcolor, + border: '1px solid #FFFFFF', + borderRadius: '2px', + fontSize: 10, + padding: 8, + display, + opacity: 0.9, + position: 'fixed', + top, + left, + pointerEvents: 'none' + }; + + return ( + +
+ {html.split('\n').map((i_, key_) => { + return
{i_}
; + })} +
+ ); + } +} + +Tooltip.propTypes = { + prefix: PropTypes.string, + bcolor: PropTypes.string, + top: PropTypes.number, + left: PropTypes.number, + display: PropTypes.string, + html: PropTypes.string +}; + +export default Tooltip; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js new file mode 100644 index 000000000..230e277f6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js @@ -0,0 +1,15 @@ +export class TypographicUtilities { + + static removeAmpersand(string_) { + return string_.replace(' & ', 'And'); + } + + static removeBrokenBar(string_) { + return string_.replace(/\|/g, ' '); + } + + static setTitle(string_) { + return string_.charAt(0).toUpperCase() + string_.substr(1).toLowerCase(); + } + +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css new file mode 100644 index 000000000..dd4883125 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css @@ -0,0 +1,17 @@ +@import url('https://fonts.googleapis.com/css?family=Noto+Sans&display=swap'); + +body { + margin: 0; + font-family: "Noto Sans", sans-serif; +} + +svg { + + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ + +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js new file mode 100644 index 000000000..25e57c2dd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js @@ -0,0 +1,253 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Tooltip from './Tooltip' +import CircularNode from './CircularNode' +import ArcNode from './ArcNode' +import {TypographicUtilities} from './Utility.js' +import './VennDiagram.css' + +class VennDiagram extends React.Component { + constructor(props_) { + super(props_); + + this.state = {tooltip: {top: 0, left: 0, display: 'none', html: ''}}; + + this.width = this.height = 512; + + this.colors = ['#777777', '#D9534F', '#F0AD4E', '#5CB85C']; + this.prefix = 'vennDiagram'; + this.suffices = ['', '|tests are|conclusive', '|tests were|inconclusive', '|tests|performed']; + this.fontStyles = [{size: Math.max(9, this.width / 32), color: 'white'}, { + size: Math.max(6, this.width / 52), + color: 'black' + }]; + this.offset = this.width / 16; + + this.thirdWidth = this.width / 3; + this.sixthWidth = this.width / 6; + this.width2By7 = 2 * this.width / 7; + this.width1By11 = this.width / 11; + this.width1By28 = this.width / 28; + + this.toggle = false; + + this.layout = { + Data: {cx: 0, cy: 0, r: this.thirdWidth - this.offset * 2, offset: {x: 0, y: 0}}, + People: {cx: -this.width2By7, cy: 0, r: this.sixthWidth, offset: {x: this.width1By11, y: 0}}, + Networks: {cx: this.width2By7, cy: 0, r: this.sixthWidth, offset: {x: -this.width1By11, y: 0}}, + Devices: {cx: 0, cy: this.width2By7, r: this.sixthWidth, offset: {x: 0, y: -this.width1By11}}, + Workloads: {cx: 0, cy: -this.width2By7, r: this.sixthWidth, offset: {x: 0, y: this.width1By11}}, + VisibilityAndAnalytics: {inner: this.thirdWidth - this.width1By28, outer: this.thirdWidth}, + AutomationAndOrchestration: { + inner: this.thirdWidth - this.width1By28 * 2, + outer: this.thirdWidth - this.width1By28 + } + }; + + /* + + RULE #1: All scores have to be equal 0, except Unexecuted [U] which could be also a negative integer + sum(C, I, P) has to be <=0 + + RULE #2: Conclusive [C] has to be > 0, + sum(C) > 0 + + RULE #3: Inconclusive [I] has to be > 0 while Conclusive has to be 0, + sum(C, I) > 0 and C * I = 0, while C has to be 0 + + RULE #4: Positive [P] and Unexecuted have to be positive + sum(P, U) >= 2 and P * U = positive integer, while + if the P is bigger by 2 then negative U, first conditional + would be true. + + */ + + this.rules = [ + + { + id: 'Rule #1', f: function (d_) { + return d_['Conclusive'] + d_['Inconclusive'] + d_['Positive'] === 0; + } + }, + { + id: 'Rule #2', f: function (d_) { + return d_['Conclusive'] > 0; + } + }, + { + id: 'Rule #3', f: function (d_) { + return d_['Conclusive'] === 0 && d_['Inconclusive'] > 0; + } + }, + { + id: 'Rule #4', f: function (d_) { + return d_['Positive'] + d_['Unexecuted'] >= 2 && d_['Positive'] * d_['Unexecuted'] > 0; + } + } + + ]; + + this._onScroll = this._onScroll.bind(this); + } + + componentDidMount() { + this.parseData(); + window.addEventListener('scroll', this._onScroll); + } + + _onMouseMove(e) { + let self = this; + + if (!this.toggle) { + let hidden = 'none'; + let html = ''; + let bcolor = '#DEDEDE'; + + document.querySelectorAll('circle, path').forEach((d_, i_) => { + d_.setAttribute('opacity', "0.8"); + }); + + if (e.target.id.includes('Node')) { + html = e.target.dataset.tooltip; + this.divElement.style.cursor = 'pointer'; + hidden = 'block'; + e.target.setAttribute('opacity', 0.95); + bcolor = e.target.getAttribute('fill'); + + // Set highest z-index + e.target.parentNode.parentNode.appendChild(e.target.parentNode); + } else { + this.divElement.style.cursor = 'default'; + + // Return z indices to default + Object.keys(this.layout).forEach(function (d_, i_) { + document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode); + }) + } + + this.setState({ + target: e, + tooltip: { + target: e.target, + bcolor: bcolor, + top: e.clientY + 8, + left: e.clientX + 8, + display: hidden, + html: html + } + }); + } + } + + _onScroll(e) { + this.divElement.style.cursor = 'default'; + this.setState({target: null, tooltip: {target: null, bcolor: 'none', top: 0, left: 0, display: 'none', html: ''}}); + } + + _onClick(e) { + this.toggle = this.state.tooltip.target === e.target; + + //variable to external callback + //e.target.parentNode.id) + } + + parseData() { + let self = this; + let data = []; + const omit = (prop, {[prop]: _, ...rest}) => rest; + + this.props.pillarsGrades.forEach((d_, i_) => { + + let params = omit('pillar', d_); + let sum = Object.keys(params).reduce((sum_, key_) => sum_ + parseFloat(params[key_] || 0), 0); + let key = TypographicUtilities.removeAmpersand(d_.pillar); + let html = self.buildTooltipHtmlContent(d_); + let rule = null; + + for (let j = 0; j < self.rules.length; j++) { + if (self.rules[j].f(d_)) { + rule = j; + break; + } + } + + self.setLayoutElement(rule, key, html, d_); + data.push(this.layout[key]); + + }); + + this.setState({data: data}); + this.render(); + } + + buildTooltipHtmlContent(object_) { + return Object.keys(object_).reduce((out_, key_) => out_ + TypographicUtilities.setTitle(key_) + ': ' + object_[key_] + '\n', ''); + } + + setLayoutElement(rule_, key_, html_, d_) { + if (rule_ == null) { + throw Error('The node scores are invalid'); + } + + if (key_ === 'Data') { + this.layout[key_].fontStyle = this.fontStyles[0]; + } else { + this.layout[key_].fontStyle = this.fontStyles[1]; + } + + this.layout[key_].hex = this.colors[rule_]; + this.layout[key_].label = d_.pillar + this.suffices[rule_]; + this.layout[key_].node = d_; + this.layout[key_].tooltip = html_; + } + + render() { + if (this.state.data === undefined) { + return null; + } else { + // equivalent to center translate (width/2, height/2) + let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height; + + let nodes = Object.values(this.layout).map((d_, i_) => { + if (d_.hasOwnProperty('cx')) { + return ( + + ); + } else { + d_.label = TypographicUtilities.removeBrokenBar(d_.label); + + return ( + + ); + } + }); + + return ( +
this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} + onClick={this._onClick.bind(this)}> + + {nodes} + + +
+ ) + } + } +} + +VennDiagram.propTypes = { + pillarsGrades: PropTypes.array +}; + +export default VennDiagram;