diff --git a/monkey/common/common_consts/time_formats.py b/monkey/common/common_consts/time_formats.py new file mode 100644 index 000000000..d150ce46e --- /dev/null +++ b/monkey/common/common_consts/time_formats.py @@ -0,0 +1,3 @@ +# Default time format used in the application, follows European standard. +# Example: 1992-03-04 10:32:05 +DEFAULT_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index feff589c1..beac0f716 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -1,6 +1,7 @@ import json import logging import platform +from datetime import datetime from pprint import pformat from socket import gethostname from urllib.parse import urljoin @@ -11,6 +12,7 @@ from requests.exceptions import ConnectionError import infection_monkey.monkeyfs as monkeyfs import infection_monkey.tunnel as tunnel from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH +from common.common_consts.time_formats import DEFAULT_TIME_FORMAT from common.common_consts.timeouts import ( LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT, @@ -60,6 +62,7 @@ class ControlClient(object): "internet_access": has_internet_access, "config": WormConfiguration.as_dict(), "parent": parent, + "launch_time": str(datetime.now().strftime(DEFAULT_TIME_FORMAT)), } if ControlClient.proxies: diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 817a43333..67640d352 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -24,6 +24,7 @@ from monkey_island.cc.resources.configuration_export import ConfigurationExport 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.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(ZeroTrustReport, "/api/report/zero-trust/") api.add_resource(AttackReport, "/api/report/attack") api.add_resource(RansomwareReport, "/api/report/ransomware") + api.add_resource(ManualExploitation, "/api/exploitations/manual") 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/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index fc87c4605..70ca9fbf9 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -40,6 +40,7 @@ class Monkey(Document): hostname = StringField() internet_access = BooleanField() ip_addresses = ListField(StringField()) + launch_time = StringField() keepalive = DateTimeField() modifytime = DateTimeField() # TODO make "parent" an embedded document, so this can be removed and the schema explained ( diff --git a/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py new file mode 100644 index 000000000..5754bd49f --- /dev/null +++ b/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py @@ -0,0 +1,13 @@ +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 + + +class ManualExploitation(flask_restful.Resource): + @jwt_required + def get(self): + manual_exploitations = [ + exploitation.__dict__ for exploitation in get_manual_exploitations() + ] + return {"manual_exploitations": manual_exploitations} diff --git a/monkey/monkey_island/cc/services/exploitations/manual_exploitation.py b/monkey/monkey_island/cc/services/exploitations/manual_exploitation.py new file mode 100644 index 000000000..303fe8db5 --- /dev/null +++ b/monkey/monkey_island/cc/services/exploitations/manual_exploitation.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import List + +from monkey_island.cc.database import mongo +from monkey_island.cc.services.node import NodeService + + +@dataclass +class ManualExploitation: + hostname: str + ip_addresses: List[str] + start_time: str + + +def get_manual_exploitations() -> List[ManualExploitation]: + monkeys = get_manual_monkeys() + return [monkey_to_manual_exploitation(monkey) for monkey in monkeys] + + +def get_manual_monkeys(): + return [ + monkey for monkey in mongo.db.monkey.find({}) if NodeService.get_monkey_manual_run(monkey) + ] + + +def monkey_to_manual_exploitation(monkey: dict) -> ManualExploitation: + return ManualExploitation( + hostname=monkey["hostname"], + ip_addresses=monkey["ip_addresses"], + start_time=monkey["launch_time"], + ) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 5a27b8933..20574c54f 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -21,6 +21,7 @@ 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.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501 ExploiterDescriptorEnum, @@ -553,12 +554,8 @@ class ReportService: return None @staticmethod - def get_manual_monkeys(): - return [ - monkey["hostname"] - for monkey in mongo.db.monkey.find({}, {"hostname": 1, "parent": 1, "guid": 1}) - if NodeService.get_monkey_manual_run(monkey) - ] + def get_manual_monkey_hostnames(): + return [monkey["hostname"] for monkey in get_manual_monkeys()] @staticmethod def get_config_users(): @@ -654,7 +651,7 @@ class ReportService: exploited_nodes = ReportService.get_exploited() report = { "overview": { - "manual_monkeys": ReportService.get_manual_monkeys(), + "manual_monkeys": ReportService.get_manual_monkey_hostnames(), "config_users": config_users, "config_passwords": config_passwords, "config_exploits": ReportService.get_config_exploits(), diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/RansomwareReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/RansomwareReport.js index dd0cc2f53..3e7a96311 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/RansomwareReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/RansomwareReport.js @@ -6,6 +6,7 @@ import FileEncryptionTable from './ransomware/FileEncryptionTable'; import LateralMovement from './ransomware/LateralMovement'; import '../../styles/pages/report/RansomwareReport.scss'; +import BreachSection from './ransomware/BreachSection'; class RansomwareReport extends React.Component { @@ -16,6 +17,7 @@ class RansomwareReport extends React.Component { generateReportContent() { return (
+
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/RenderArrays.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/RenderArrays.js index 7bbef33bc..692484f27 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/common/RenderArrays.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/RenderArrays.js @@ -1,8 +1,36 @@ import React from 'react'; -export let renderArray = function (val) { - return <>{val.map(x =>
{x}
)}; +export let renderArray = function (val, className='') { + return <>{val.map(x =>
{x}
)}; }; export let renderIpAddresses = function (val) { - return
{renderArray(val.ip_addresses)} {(val.domain_name ? ' ('.concat(val.domain_name, ')') : '')}
; + return
+ {renderArray(val.ip_addresses, 'ip-address')} {(val.domain_name ? ' ('.concat(val.domain_name, ')') : '')} +
; }; + +export let renderLimitedArray = function (array, + limit, + className='', + separator=',') { + let elements = []; + if(array.length < limit){ + limit = array.length; + } + for(let i = 0; i < limit; i++){ + let element = ''; + if(i !== 0) { + element = (<>{separator} {array[i]}); + } else { + element = (<>{array[i]}); + } + elements.push(
{element}
); + } + let remainder = array.length - limit; + if(remainder > 0){ + elements.push(
+  and {remainder} more +
); + } + return elements +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/BreachSection.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/BreachSection.tsx new file mode 100644 index 000000000..1c2b71d99 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/BreachSection.tsx @@ -0,0 +1,49 @@ +import React, {useEffect, useState} from 'react'; +import IslandHttpClient from '../../IslandHttpClient'; +import NumberedReportSection from './NumberedReportSection'; +import LoadingIcon from '../../ui-components/LoadingIcon'; +import {renderLimitedArray} from '../common/RenderArrays'; + +function BreachSection() { + const [machines, setMachines] = useState(null); + let description = 'Ransomware attacks start after machines in the internal network get compromised. ' + + 'The initial compromise was simulated by running Monkey Agents manually.'; + + useEffect(() => { + IslandHttpClient.get('/api/exploitations/manual') + .then(resp => setMachines(resp.body['manual_exploitations'])); + }, []); + + if(machines !== null){ + let body = getBreachSectionBody(machines); + return () + } else { + return + } +} + +function getBreachSectionBody(machines) { + let machineList = []; + for(let i = 0; i < machines.length; i++){ + machineList.push(getMachine(machines[i])); + } + return ( +
+

Ransomware attack started from these machines on the network:

+
    + {machineList} +
+
+ ) + } + +function getMachine(machine) { + return ( +
  • + {machine['hostname']}  + ({renderLimitedArray(machine['ip_addresses'], 2, 'ip-address')}) at {machine['start_time']} +
  • + ) +} + +export default BreachSection; diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/report/RansomwareReport.scss b/monkey/monkey_island/cc/ui/src/styles/pages/report/RansomwareReport.scss index 0a3ea2bf9..143e3f835 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/report/RansomwareReport.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/report/RansomwareReport.scss @@ -18,3 +18,7 @@ margin-top: .28em; margin-right: .5em; } + +.ransomware-breach-section .ip-address { + display: inline-block; +} diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 5a430dc6c..b39d61dd8 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -70,6 +70,7 @@ PbaResults # unused class (monkey/monkey_island/cc/models/pba_results.py:4) internet_access # unused variable (monkey/monkey_island/cc/models/monkey.py:43) config_error # unused variable (monkey/monkey_island/cc/models/monkey.py:53) pba_results # unused variable (monkey/monkey_island/cc/models/monkey.py:55) +launch_time # unused variable (monkey/monkey_island/cc/models/monkey.py) command_control_channel # unused variable (monkey/monkey_island/cc/models/monkey.py:58) meta # unused variable (monkey/monkey_island/cc/models/zero_trust/finding.py:37) meta # unused variable (monkey/monkey_island/cc/models/monkey_ttl.py:34)