diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 8bf970c4a..b3254d7cb 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 @@ -154,6 +155,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/manual_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py index 5754bd49f..7c5db2f75 100644 --- a/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py +++ b/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py @@ -1,7 +1,9 @@ import flask_restful from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.exploitations.manual_exploitation import get_manual_exploitations +from monkey_island.cc.services.reporting.exploitations.manual_exploitation import ( + get_manual_exploitations, +) class ManualExploitation(flask_restful.Resource): 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/__init__.py b/monkey/monkey_island/cc/services/reporting/exploitations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/exploitations/manual_exploitation.py b/monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py similarity index 100% rename from monkey/monkey_island/cc/services/exploitations/manual_exploitation.py rename to monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py 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..f06d23274 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py @@ -0,0 +1,62 @@ +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_nodes_monkeys_launched = [ + 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"])) + ] + + # The node got exploited, but no monkeys got launched. + # For example the exploited machine was too old. + exploited_nodes_monkeys_failed = [ + NodeService.get_displayed_node_by_id(node["_id"], True) + for node in mongo.db.node.find({"exploited": True}, {"_id": 1}) + ] + + exploited = exploited_nodes_monkeys_launched + exploited_nodes_monkeys_failed + + 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 7dc15f9b6..b9f8f6a47 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, ) @@ -148,47 +151,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 = [] @@ -646,7 +608,7 @@ class ReportService: monkey_latest_modify_time = Monkey.get_latest_modifytime() scanned_nodes = ReportService.get_scanned() - exploited_nodes = ReportService.get_exploited() + exploited_cnt = len(get_monkey_exploited()) report = { "overview": { "manual_monkeys": ReportService.get_manual_monkey_hostnames(), @@ -664,7 +626,7 @@ class ReportService: }, "glance": { "scanned": scanned_nodes, - "exploited": exploited_nodes, + "exploited_cnt": exploited_cnt, "stolen_creds": ReportService.get_stolen_creds(), "azure_passwords": ReportService.get_azure_creds(), "ssh_keys": ReportService.get_ssh_keys(), 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..4f8af8c62 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 @@ -1,4 +1,5 @@ import React, {Fragment} from 'react'; +import Pluralize from 'pluralize'; import BreachedServers from 'components/report-components/security/BreachedServers'; import ScannedServers from 'components/report-components/security/ScannedServers'; import PostBreach from 'components/report-components/security/PostBreach'; @@ -288,9 +289,9 @@ class ReportPageComponent extends AuthComponent {

Overview

- 0}/> + 0}/> { - this.state.report.glance.exploited.length > 0 ? + this.state.report.glance.exploited_cnt > 0 ? '' :

@@ -524,7 +525,7 @@ class ReportPageComponent extends AuthComponent { generateReportGlanceSection() { let exploitPercentage = - (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; + (100 * this.state.report.glance.exploited_cnt) / this.state.report.glance.scanned.length; return (

@@ -535,7 +536,7 @@ class ReportPageComponent extends AuthComponent { The Monkey discovered {this.state.report.glance.scanned.length} machines and successfully breached {this.state.report.glance.exploited.length} of them. + className='badge badge-danger'>{this.state.report.glance.exploited_cnt} of them.

- +

+ The Monkey successfully breached  + + {this.state.report.glance.exploited_cnt} + {Pluralize('machine', this.state.report.glance.exploited_cnt)}: +

+
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/LateralMovement.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/LateralMovement.tsx index c5a9f9f1e..b7026af5e 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/LateralMovement.tsx +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/LateralMovement.tsx @@ -1,11 +1,12 @@ import React, {ReactElement} from 'react'; import NumberedReportSection from './NumberedReportSection'; import pluralize from 'pluralize' +import BreachedServersComponent from '../security/BreachedServers'; const LATERAL_MOVEMENT_DESCRIPTION = 'After the initial breach, the attacker will begin the Lateral \ Movement phase of the attack. They will employ various \ techniques in order to compromise other systems in your \ - network and encrypt as many files as possible.' + network.' type PropagationStats = { num_scanned_nodes: number, @@ -18,6 +19,8 @@ function LateralMovement({propagationStats}: {propagationStats: PropagationStats <> {getScannedVsExploitedStats(propagationStats.num_scanned_nodes, propagationStats.num_exploited_nodes)} {getExploitationStatsPerExploit(propagationStats.num_exploited_per_exploit)} +
+ ) 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..d9145242e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.tsx @@ -0,0 +1,54 @@ +import React, {useEffect, useState} from 'react'; +import ReactTable from 'react-table'; +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 ( + <> +
+ +
+ + ); + +} + +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..f40e09c62 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py @@ -0,0 +1,24 @@ +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__2_exploits(): + exploits = get_exploits_used_on_node(NODE_DICT) + assert sorted(exploits) == sorted(["Elastic Groovy Exploiter", "Drupal Server Exploiter"]) + + +def test_get_exploits_used_on_node__duplicate_exploits(): + exploits = get_exploits_used_on_node(NODE_DICT_DUPLICATE_EXPLOITS) + assert exploits == ["Drupal Server Exploiter"] + + +def test_get_exploits_used_on_node__failed(): + 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 == []