diff --git a/monkey/infection_monkey/agent_event_forwarder.py b/monkey/infection_monkey/agent_event_forwarder.py index f253255d3..cf585a4bb 100644 --- a/monkey/infection_monkey/agent_event_forwarder.py +++ b/monkey/infection_monkey/agent_event_forwarder.py @@ -79,8 +79,8 @@ class BatchingAgentEventForwarder: try: logger.debug(f"Sending Agent events to Island: {events}") self._island_api_client.send_events(events) - except Exception as err: - logger.warning(f"Exception caught when connecting to the Island: {err}") + except Exception: + logger.exception("Exception caught when connecting to the Island") def _send_remaining_events(self): self._send_events_to_island() diff --git a/monkey/monkey_island/cc/models/machine.py b/monkey/monkey_island/cc/models/machine.py index 99d4ae5eb..ece877b9e 100644 --- a/monkey/monkey_island/cc/models/machine.py +++ b/monkey/monkey_island/cc/models/machine.py @@ -38,3 +38,6 @@ class Machine(MutableInfectionMonkeyBaseModel): _make_immutable_sequence = validator("network_interfaces", pre=True, allow_reuse=True)( make_immutable_sequence ) + + def __hash__(self): + return self.id diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index 40839c388..9e205e820 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -94,6 +94,8 @@ def initialize_services(container: DIContainer, data_dir: Path): container.resolve(AWSService), container.resolve(IAgentConfigurationRepository), container.resolve(ICredentialsRepository), + container.resolve(IMachineRepository), + container.resolve(INodeRepository), ) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 52ed04df9..b3673dd90 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -2,15 +2,20 @@ import functools import ipaddress import logging from itertools import chain, product -from typing import List +from typing import List, Optional from common.network.network_range import NetworkRange from common.network.network_utils import get_my_ip_addresses_legacy, get_network_interfaces 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.models import CommunicationType, Machine, Monkey from monkey_island.cc.models.report import get_report, save_report -from monkey_island.cc.repository import IAgentConfigurationRepository, ICredentialsRepository +from monkey_island.cc.repository import ( + IAgentConfigurationRepository, + ICredentialsRepository, + IMachineRepository, + INodeRepository, +) 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 ( @@ -30,10 +35,11 @@ logger = logging.getLogger(__name__) class ReportService: - - _aws_service = None - _agent_configuration_repository = None - _credentials_repository = None + _aws_service: Optional[AWSService] = None + _agent_configuration_repository: Optional[IAgentConfigurationRepository] = None + _credentials_repository: Optional[ICredentialsRepository] = None + _machine_repository: Optional[IMachineRepository] = None + _node_repository: Optional[INodeRepository] = None class DerivedIssueEnum: ZEROLOGON_PASS_RESTORE_FAILED = "zerologon_pass_restore_failed" @@ -44,10 +50,14 @@ class ReportService: aws_service: AWSService, agent_configuration_repository: IAgentConfigurationRepository, credentials_repository: ICredentialsRepository, + machine_repository: IMachineRepository, + node_repository: INodeRepository, ): cls._aws_service = aws_service cls._agent_configuration_repository = agent_configuration_repository cls._credentials_repository = credentials_repository + cls._machine_repository = machine_repository + cls._node_repository = node_repository # This should pull from Simulation entity @staticmethod @@ -102,39 +112,42 @@ class ReportService: def get_scanned(): formatted_nodes = [] - nodes = ReportService.get_all_displayed_nodes() + machines = ReportService._machine_repository.get_machines() - for node in nodes: - # This information should be evident from the map, not sure a table/list is a good way - # to display it anyways - nodes_that_can_access_current_node = node["accessible_from_nodes_hostnames"] - formatted_nodes.append( - { - "label": node["label"], - "ip_addresses": node["ip_addresses"], - "accessible_from_nodes": nodes_that_can_access_current_node, - "services": node["services"], - "domain_name": node["domain_name"], - "pba_results": node["pba_results"] if "pba_results" in node else "None", - } - ) - - logger.info("Scanned nodes generated for reporting") + for machine in machines: + accessible_from = ReportService.get_scanners_of_machine(machine) + if accessible_from: + formatted_nodes.append( + { + "hostname": machine.hostname, + "ip_addresses": [str(iface.ip) for iface in machine.network_interfaces], + "accessible_from_nodes": [m.dict(simplify=True) for m in accessible_from], + "domain_name": "", + # TODO add services + "services": [], + } + ) return formatted_nodes - @staticmethod - def get_all_displayed_nodes(): - nodes_without_monkeys = [ - NodeService.get_displayed_node_by_id(node["_id"], True) - for node in mongo.db.node.find({}, {"_id": 1}) - ] - nodes_with_monkeys = [ - NodeService.get_displayed_node_by_id(monkey["_id"], True) - for monkey in mongo.db.monkey.find({}, {"_id": 1}) - ] - nodes = nodes_without_monkeys + nodes_with_monkeys - return nodes + @classmethod + def get_scanners_of_machine(cls, machine: Machine) -> List[Machine]: + if not cls._node_repository: + raise RuntimeError("Node repository does not exist") + if not cls._machine_repository: + raise RuntimeError("Machine repository does not exist") + + nodes = cls._node_repository.get_nodes() + scanner_machines = set() + for node in nodes: + for dest, conn in node.connections.items(): + if CommunicationType.SCANNED in conn and dest == machine.id: + scanner_machine = ReportService._machine_repository.get_machine_by_id( + node.machine_id + ) + scanner_machines.add(scanner_machine) + + return list(scanner_machines) @staticmethod def process_exploit(exploit) -> ExploiterReportInfo: 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 fd1accfb8..ba3f53232 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 @@ -2,7 +2,6 @@ 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'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {edgeGroupToColor, getOptions} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/security/StolenPasswords'; @@ -23,7 +22,7 @@ import {smbPasswordReport, smbPthReport} from './security/issues/SmbIssue'; import {hadoopIssueOverview, hadoopIssueReport} from './security/issues/HadoopIssue'; import {mssqlIssueOverview, mssqlIssueReport} from './security/issues/MssqlIssue'; import {wmiPasswordIssueReport, wmiPthIssueReport} from './security/issues/WmiIssue'; -import {sshKeysReport, shhIssueReport, sshIssueOverview} from './security/issues/SshIssue'; +import {shhIssueReport, sshIssueOverview, sshKeysReport} from './security/issues/SshIssue'; import {log4shellIssueOverview, log4shellIssueReport} from './security/issues/Log4ShellIssue'; import { crossSegmentIssueOverview, @@ -31,11 +30,13 @@ import { islandCrossSegmentIssueReport } from './security/issues/CrossSegmentIssue'; import { - sharedCredsDomainIssueReport, sharedCredsIssueReport, sharedLocalAdminsIssueReport, sharedAdminsDomainIssueOverview, + sharedCredsDomainIssueReport, + sharedCredsIssueReport, + sharedLocalAdminsIssueReport, sharedPasswordsIssueOverview } from './security/issues/SharedPasswordsIssue'; -import {tunnelIssueReport, tunnelIssueOverview} from './security/issues/TunnelIssue'; +import {tunnelIssueOverview, tunnelIssueReport} from './security/issues/TunnelIssue'; import {stolenCredsIssueOverview} from './security/issues/StolenCredsIssue'; import {strongUsersOnCritIssueReport} from './security/issues/StrongUsersOnCritIssue'; import { @@ -538,9 +539,11 @@ class ReportPageComponent extends AuthComponent { + {/* Post breach data should be separated from scanned machines
+ */}
; }; +export let renderMachineArray = function(array) { + return <>{array.map(x =>
{x.network_interfaces[0]}
)}; +} + export let renderLimitedArray = function (array, limit, className='', diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js index 229bbfed2..b2a58d85f 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js @@ -1,19 +1,20 @@ import React from 'react'; import ReactTable from 'react-table'; import Pluralize from 'pluralize'; -import {renderArray, renderIpAddresses} from '../common/RenderArrays'; +import {renderArray, renderIpAddresses, renderMachineArray} from '../common/RenderArrays'; const columns = [ { Header: 'Scanned Servers', columns: [ - {Header: 'Machine', accessor: 'label'}, + {Header: 'Machine', id: 'machine', accessor: x => x.ip_addresses[0]}, { Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderIpAddresses(x) }, - {Header: 'Accessible From', id: 'accessible_from_nodes', accessor: x => renderArray(x.accessible_from_nodes)}, + {Header: 'Accessible From', id: 'accessible_from_nodes', + accessor: x => renderMachineArray(x.accessible_from_nodes)}, {Header: 'Services', id: 'services', accessor: x => renderArray(x.services)} ] } @@ -27,7 +28,6 @@ class ScannedServersComponent extends React.Component { } render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; let showPagination = this.props.data.length > pageSize; 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 new file mode 100644 index 000000000..cd38d6a6b --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -0,0 +1,77 @@ +from ipaddress import IPv4Interface +from unittest.mock import MagicMock + +from monkey_island.cc.models import CommunicationType, Machine, Node +from monkey_island.cc.services.reporting.report import ReportService + +ISLAND_MACHINE = Machine( + id=99, + island=True, + hostname="Island", + hardware_id=5, + network_interfaces=[IPv4Interface("10.10.10.99/24")], +) + +MACHINE_1 = Machine( + id=1, + hardware_id=9, + hostname="machine_1", + network_interfaces=[IPv4Interface("10.10.10.1/24")], +) + +MACHINE_2 = Machine( + id=2, + hardware_id=9, + network_interfaces=[IPv4Interface("10.10.10.2/24")], +) + +MACHINE_3 = Machine( + id=3, + hardware_id=9, + network_interfaces=[IPv4Interface("10.10.10.3/24")], +) + +NODES = [ + Node( + machine_id=1, + connections={"2": frozenset([CommunicationType.EXPLOITED, CommunicationType.SCANNED])}, + ), + Node(machine_id=99, connections={"1": frozenset([CommunicationType.SCANNED])}), + Node( + machine_id=3, + connections={"99": frozenset([CommunicationType.CC, CommunicationType.EXPLOITED])}, + ), +] + +MACHINES = [MACHINE_1, MACHINE_2, MACHINE_3, ISLAND_MACHINE] + +EXPECTED_SCANNED_MACHINES = [ + { + "hostname": MACHINE_1.hostname, + "ip_addresses": [str(iface.ip) for iface in MACHINE_1.network_interfaces], + "accessible_from_nodes": [ISLAND_MACHINE.dict(simplify=True)], + "services": [], + "domain_name": "", + }, + { + "hostname": MACHINE_2.hostname, + "ip_addresses": [str(iface.ip) for iface in MACHINE_2.network_interfaces], + "accessible_from_nodes": [MACHINE_1.dict(simplify=True)], + "services": [], + "domain_name": "", + }, +] + + +def get_machine_by_id(machine_id): + return [machine for machine in MACHINES if machine_id == machine.id][0] + + +def test_get_scanned(): + ReportService._node_repository = MagicMock() + ReportService._node_repository.get_nodes.return_value = NODES + ReportService._machine_repository = MagicMock() + ReportService._machine_repository.get_machines.return_value = MACHINES + ReportService._machine_repository.get_machine_by_id = get_machine_by_id + scanned = ReportService.get_scanned() + assert scanned == EXPECTED_SCANNED_MACHINES