From 1448bb1850af30fd6baed4fd4665f18f09f659c6 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 28 Jul 2021 12:13:37 +0300 Subject: [PATCH] Island: extract monkey exploitations into a separate service and a separate endpoint This change not only removes complexity from the huge report service, but also allows different UI components to call the API without forcing the whole report to be generated --- monkey/monkey_island/cc/app.py | 2 + .../exploitations/monkey_exploitation.py | 13 ++++ .../services/ransomware/ransomware_report.py | 10 +++- .../exploitations/monkey_exploitation.py | 60 +++++++++++++++++++ .../cc/services/reporting/report.py | 48 ++------------- .../report-components/SecurityReport.js | 2 +- .../security/BreachedServers.js | 50 ---------------- .../security/BreachedServers.tsx | 59 ++++++++++++++++++ .../ransomware/test_ransomware_report.py | 9 +-- .../exploitations/test_monkey_exploitation.py | 20 +++++++ .../cc/services/reporting/test_report.py | 11 ---- 11 files changed, 172 insertions(+), 112 deletions(-) create mode 100644 monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py create mode 100644 monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py delete mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.tsx create mode 100644 monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 67640d352..b25ae476c 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -25,6 +25,7 @@ from monkey_island.cc.resources.configuration_import import ConfigurationImport from monkey_island.cc.resources.edge import Edge from monkey_island.cc.resources.environment import Environment from monkey_island.cc.resources.exploitations.manual_exploitation import ManualExploitation +from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_configuration import IslandConfiguration from monkey_island.cc.resources.island_logs import IslandLog from monkey_island.cc.resources.island_mode import IslandMode @@ -156,6 +157,7 @@ def init_api_resources(api): api.add_resource(AttackReport, "/api/report/attack") api.add_resource(RansomwareReport, "/api/report/ransomware") api.add_resource(ManualExploitation, "/api/exploitations/manual") + 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/") diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py new file mode 100644 index 000000000..5e00a51a0 --- /dev/null +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -0,0 +1,13 @@ +import flask_restful + +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( + get_monkey_exploited, +) + + +class MonkeyExploitation(flask_restful.Resource): + @jwt_required + def get(self): + monkey_exploitations = [exploitation.__dict__ for exploitation in get_monkey_exploited()] + return {"monkey_exploitations": monkey_exploitations} diff --git a/monkey/monkey_island/cc/services/ransomware/ransomware_report.py b/monkey/monkey_island/cc/services/ransomware/ransomware_report.py index 0e0f1c299..5dd384511 100644 --- a/monkey/monkey_island/cc/services/ransomware/ransomware_report.py +++ b/monkey/monkey_island/cc/services/ransomware/ransomware_report.py @@ -1,11 +1,15 @@ from typing import Dict, List +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( + MonkeyExploitation, + get_monkey_exploited, +) from monkey_island.cc.services.reporting.report import ReportService def get_propagation_stats() -> Dict: scanned = ReportService.get_scanned() - exploited = ReportService.get_exploited() + exploited = get_monkey_exploited() return { "num_scanned_nodes": len(scanned), @@ -14,11 +18,11 @@ def get_propagation_stats() -> Dict: } -def _get_exploit_counts(exploited: List[Dict]) -> Dict: +def _get_exploit_counts(exploited: List[MonkeyExploitation]) -> Dict: exploit_counts = {} for node in exploited: - for exploit in node["exploits"]: + for exploit in node.exploits: exploit_counts[exploit] = exploit_counts.get(exploit, 0) + 1 return exploit_counts diff --git a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py new file mode 100644 index 000000000..ac400d8cc --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py @@ -0,0 +1,60 @@ +import logging +from dataclasses import dataclass +from typing import List + +from monkey_island.cc.database import mongo +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501 + ExploiterDescriptorEnum, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class MonkeyExploitation: + label: str + ip_addresses: List[str] + domain_name: str + exploits: List[str] + + +def get_monkey_exploited() -> List[MonkeyExploitation]: + exploited_with_monkeys = [ + NodeService.get_displayed_node_by_id(monkey["_id"], True) + for monkey in mongo.db.monkey.find({}, {"_id": 1}) + if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey["_id"])) + ] + + exploited_without_monkeys = [ + NodeService.get_displayed_node_by_id(node["_id"], True) + for node in mongo.db.node.find({"exploited": True}, {"_id": 1}) + ] + + exploited = exploited_with_monkeys + exploited_without_monkeys + + exploited = [ + MonkeyExploitation( + label=exploited_node["label"], + ip_addresses=exploited_node["ip_addresses"], + domain_name=exploited_node["domain_name"], + exploits=get_exploits_used_on_node(exploited_node), + ) + for exploited_node in exploited + ] + + logger.info("Exploited nodes generated for reporting") + + return exploited + + +def get_exploits_used_on_node(node: dict) -> List[str]: + return list( + set( + [ + ExploiterDescriptorEnum.get_by_class_name(exploit["exploiter"]).display_name + for exploit in node["exploits"] + if exploit["result"] + ] + ) + ) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 20574c54f..6b9265435 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -21,8 +21,11 @@ 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.exploitations.manual_exploitation import get_manual_monkeys from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.reporting.exploitations.manual_exploitation import get_manual_monkeys +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( + get_monkey_exploited, +) from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501 ExploiterDescriptorEnum, ) @@ -150,47 +153,6 @@ class ReportService: nodes = nodes_without_monkeys + nodes_with_monkeys return nodes - @staticmethod - def get_exploited(): - exploited_with_monkeys = [ - NodeService.get_displayed_node_by_id(monkey["_id"], True) - for monkey in mongo.db.monkey.find({}, {"_id": 1}) - if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey["_id"])) - ] - - exploited_without_monkeys = [ - NodeService.get_displayed_node_by_id(node["_id"], True) - for node in mongo.db.node.find({"exploited": True}, {"_id": 1}) - ] - - exploited = exploited_with_monkeys + exploited_without_monkeys - - exploited = [ - { - "label": exploited_node["label"], - "ip_addresses": exploited_node["ip_addresses"], - "domain_name": exploited_node["domain_name"], - "exploits": ReportService.get_exploits_used_on_node(exploited_node), - } - for exploited_node in exploited - ] - - logger.info("Exploited nodes generated for reporting") - - return exploited - - @staticmethod - def get_exploits_used_on_node(node: dict) -> List[str]: - return list( - set( - [ - ExploiterDescriptorEnum.get_by_class_name(exploit["exploiter"]).display_name - for exploit in node["exploits"] - if exploit["result"] - ] - ) - ) - @staticmethod def get_stolen_creds(): creds = [] @@ -648,7 +610,7 @@ class ReportService: monkey_latest_modify_time = Monkey.get_latest_modifytime() scanned_nodes = ReportService.get_scanned() - exploited_nodes = ReportService.get_exploited() + exploited_nodes = get_monkey_exploited() report = { "overview": { "manual_monkeys": ReportService.get_manual_monkey_hostnames(), diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index c9fdd2c52..f7cffd277 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -566,7 +566,7 @@ class ReportPageComponent extends AuthComponent {
- +
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js deleted file mode 100644 index 827549c1a..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table'; -import Pluralize from 'pluralize'; -import {renderArray, renderIpAddresses} from '../common/RenderArrays'; - - -const columns = [ - { - Header: 'Breached Servers', - columns: [ - {Header: 'Machine', accessor: 'label'}, - { - Header: 'IP Addresses', id: 'ip_addresses', - accessor: x => renderIpAddresses(x) - }, - {Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)} - ] - } -]; - -const pageSize = 10; - -class BreachedServersComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( - <> -

- The Monkey successfully breached {this.props.data.length} {Pluralize('machine', this.props.data.length)}: -

-
- -
- - ); - } -} - -export default BreachedServersComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.tsx new file mode 100644 index 000000000..f2f774d61 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.tsx @@ -0,0 +1,59 @@ +import React, {useEffect, useState} from 'react'; +import ReactTable from 'react-table'; +import Pluralize from 'pluralize'; +import {renderArray, renderIpAddresses} from '../common/RenderArrays'; +import LoadingIcon from '../../ui-components/LoadingIcon'; +import IslandHttpClient from '../../IslandHttpClient'; + + +const columns = [ + { + Header: 'Breached Servers', + columns: [ + {Header: 'Machine', accessor: 'label'}, + { + Header: 'IP Addresses', id: 'ip_addresses', + accessor: x => renderIpAddresses(x) + }, + {Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)} + ] + } +]; + +const pageSize = 10; + +function BreachedServersComponent() { + + const [exploitations, setExploitations] = useState(null); + + useEffect(() => { + IslandHttpClient.get('/api/exploitations/monkey') + .then(res => setExploitations(res.body['monkey_exploitations'])) + }, []); + + if(exploitations === null){ + return + } + + let defaultPageSize = exploitations.length > pageSize ? pageSize : exploitations.length; + let showPagination = exploitations.length > pageSize; + return ( + <> +

+ The Monkey successfully breached {exploitations.length} {Pluralize('machine', exploitations.length)}: +

+
+ +
+ + ); + +} + +export default BreachedServersComponent; diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/ransomware/test_ransomware_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/ransomware/test_ransomware_report.py index 4c586aa81..a12b2aa9c 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/ransomware/test_ransomware_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/ransomware/test_ransomware_report.py @@ -1,6 +1,7 @@ import pytest from monkey_island.cc.services.ransomware import ransomware_report +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.services.reporting.report import ReportService @@ -8,13 +9,13 @@ from monkey_island.cc.services.reporting.report import ReportService def patch_report_service_for_stats(monkeypatch): TEST_SCANNED_RESULTS = [{}, {}, {}, {}] TEST_EXPLOITED_RESULTS = [ - {"exploits": ["SSH Exploiter"]}, - {"exploits": ["SSH Exploiter", "SMB Exploiter"]}, - {"exploits": ["WMI Exploiter"]}, + MonkeyExploitation("", [], "", exploits=["SSH Exploiter"]), + MonkeyExploitation("", [], "", exploits=["SSH Exploiter", "SMB Exploiter"]), + MonkeyExploitation("", [], "", exploits=["WMI Exploiter"]), ] monkeypatch.setattr(ReportService, "get_scanned", lambda: TEST_SCANNED_RESULTS) - monkeypatch.setattr(ReportService, "get_exploited", lambda: TEST_EXPLOITED_RESULTS) + monkeypatch.setattr(ransomware_report, "get_monkey_exploited", lambda: TEST_EXPLOITED_RESULTS) def test_get_propagation_stats__num_scanned(patch_report_service_for_stats): diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py new file mode 100644 index 000000000..cdee46ba1 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py @@ -0,0 +1,20 @@ +from tests.unit_tests.monkey_island.cc.services.reporting.test_report import ( + NODE_DICT, + NODE_DICT_DUPLICATE_EXPLOITS, + NODE_DICT_FAILED_EXPLOITS, +) + +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( + get_exploits_used_on_node, +) + + +def test_get_exploits_used_on_node(): + exploits = get_exploits_used_on_node(NODE_DICT) + assert sorted(exploits) == sorted(["Elastic Groovy Exploiter", "Drupal Server Exploiter"]) + + exploits = get_exploits_used_on_node(NODE_DICT_DUPLICATE_EXPLOITS) + assert exploits == ["Drupal Server Exploiter"] + + exploits = get_exploits_used_on_node(NODE_DICT_FAILED_EXPLOITS) + assert exploits == [] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index 989c46eed..0093e4235 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -164,14 +164,3 @@ def test_get_stolen_creds_no_creds(fake_mongo): expected_stolen_creds_no_creds = [] assert expected_stolen_creds_no_creds == stolen_creds_no_creds - - -def test_get_exploits_used_on_node(): - exploits = ReportService.get_exploits_used_on_node(NODE_DICT) - assert sorted(exploits) == sorted(["Elastic Groovy Exploiter", "Drupal Server Exploiter"]) - - exploits = ReportService.get_exploits_used_on_node(NODE_DICT_DUPLICATE_EXPLOITS) - assert exploits == ["Drupal Server Exploiter"] - - exploits = ReportService.get_exploits_used_on_node(NODE_DICT_FAILED_EXPLOITS) - assert exploits == []