diff --git a/monkey/monkey_island/cc/ui/src/components/map/MapPageWrapper.tsx b/monkey/monkey_island/cc/ui/src/components/map/MapPageWrapper.tsx index c008bfdb590..7b35453a462 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/MapPageWrapper.tsx +++ b/monkey/monkey_island/cc/ui/src/components/map/MapPageWrapper.tsx @@ -1,13 +1,12 @@ import React, {useEffect, useState} from 'react'; import IslandHttpClient, {APIEndpoint} from '../IslandHttpClient'; -import {arrayToObject, getCollectionObject} from '../utils/ServerUtils'; +import {arrayToObject, getAllAgents, getCollectionObject} from '../utils/ServerUtils'; import MapPage from '../pages/MapPage'; import MapNode, { Agent, Communications, CommunicationType, getMachineIp, - interfaceIp, Machine, Node } from '../types/MapNode'; @@ -25,7 +24,7 @@ const MapPageWrapper = (props) => { const [mapNodes, setMapNodes] = useState([]); const [nodes, setNodes] = useState>({}); const [machines, setMachines] = useState>({}); - const [agents, setAgents] = useState>({}); + const [agents, setAgents] = useState([]); const [propagationEvents, setPropagationEvents] = useState({}); const [graph, setGraph] = useState({edges: [], nodes: []}); @@ -37,7 +36,7 @@ const MapPageWrapper = (props) => { function fetchMapNodes() { getCollectionObject(APIEndpoint.nodes, 'machine_id').then(nodeObj => setNodes(nodeObj)); getCollectionObject(APIEndpoint.machines, 'id').then(machineObj => setMachines(machineObj)); - getCollectionObject(APIEndpoint.agents, 'machine_id').then(agentObj => setAgents(agentObj)); + getAllAgents().then(agents => setAgents(agents)); getPropagationEvents().then(events => setPropagationEvents(events)); } @@ -76,7 +75,6 @@ const MapPageWrapper = (props) => { function buildMapNodes(): MapNode[] { // Build the MapNodes list - let agentsById = arrayToObject(Object.values(agents), 'id'); let mapNodes: MapNode[] = []; for (const machine of Object.values(machines)) { let node = nodes[machine.id] || null; @@ -88,15 +86,22 @@ const MapPageWrapper = (props) => { communications = []; } let running = false; - let agentID: string | null = null; - let parentID: string | null = null; - let agentStartTime: Date = new Date(0); - if (node !== null && machine.id in agents) { - let agent = agents[machine.id]; - running = isAgentRunning(agent); - agentID = agent.id; - parentID = agent.parent_id; - agentStartTime = new Date(agent.start_time); + let agentIDs: string[] = []; + let parentIDs: string[] = []; + let lastAgentStartTime: Date = new Date(0); + if (node !== null ) { + const nodeAgents = agents.filter(a => a.machine_id === machine.id); + nodeAgents.forEach((nodeAgent) => { + if(!running){ + running = isAgentRunning(nodeAgent); + } + agentIDs.push(nodeAgent.id); + parentIDs.push(nodeAgent.parent_id); + let agentStartTime = new Date(nodeAgent.start_time); + if(agentStartTime > lastAgentStartTime){ + lastAgentStartTime = agentStartTime; + } + }); } let propagatedTo = wasMachinePropagated(machine, propagationEvents); @@ -110,9 +115,9 @@ const MapPageWrapper = (props) => { machine.hostname, machine.island, propagatedTo, - agentStartTime, - agentID, - parentID + lastAgentStartTime, + agentIDs, + parentIDs )); } diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/ExploitationTimeline.tsx b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/ExploitationTimeline.tsx new file mode 100644 index 00000000000..02a59bd72c6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/ExploitationTimeline.tsx @@ -0,0 +1,156 @@ +import _ from 'lodash'; +import React, {useEffect, useState} from 'react'; +import MapNode from '../../types/MapNode'; +import {OverlayTrigger, Tooltip} from 'react-bootstrap'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons/faQuestionCircle'; +import IslandHttpClient, {APIEndpoint} from '../../IslandHttpClient'; +import LoadingIcon from '../../ui-components/LoadingIcon'; + + +type ExploitationAttempt = { + source: string; + success: boolean; + timestamp: Date; + exploiter_name: string; +} + +type ExploitationEvent = { + source: string; + success: boolean; + timestamp: number; + exploiter_name: string; + target: string; +} + +const ExploitationTimeline = (props: { node: MapNode, allNodes: MapNode[] }) => { + + const [exploitationAttempts, setExploitationAttempts] = useState([]); + const [loadingEvents, setLoadingEvents] = useState(true); + const [updateTimer, setUpdateTimer] = useState(setInterval(() => {})); + + const getExploitationAttempts = (node: MapNode) => { + let url_args = {'type': 'ExploitationEvent'}; + return IslandHttpClient.get(APIEndpoint.agentEvents, url_args) + .then(res => res.body).then(events => { + return parseEvents(events, node); + }) + } + + function updateAttemptsFromServer(){ + let node = props.node; + return getExploitationAttempts(props.node).then((attempts) => { + if(node === props.node){ + setExploitationAttempts(attempts); + }} + ); + } + + useEffect(() => { + let oneSecond = 1000; + clearInterval(updateTimer); + setLoadingEvents(true); + updateAttemptsFromServer().then(() => setLoadingEvents(false)); + setUpdateTimer(setInterval(() => { + updateAttemptsFromServer(); + }, oneSecond * 5)); + + return () => clearInterval(updateTimer); + }, [props.node]) + + function parseEvents(events: ExploitationEvent[], node: MapNode): ExploitationAttempt[] { + let exploitationAttempts = []; + let filteredEvents = events.filter(event => node.hasIp(event.target)) + for (const event of Object.values(filteredEvents)) { + let iface = node.networkInterfaces.find(iface => iface.includes(event.target)) + if (iface !== undefined) { + let timestampInMilliseconds: number = event.timestamp * 1000; + exploitationAttempts.push({ + source: getSourceNodeLabel(event.source), + success: event.success, + timestamp: new Date(timestampInMilliseconds), + exploiterName: event.exploiter_name + }); + } + } + return exploitationAttempts; + } + + function getAttemptList(): any { + if(exploitationAttempts.length === 0){ + return (
  • No exploits were attempted on this node yet.
  • ); + } else { + return aggregateExploitationAttempts(_.sortBy(exploitationAttempts, 'timestamp')) + .map(data => { + const {data: attempt, count: count} = data; + return ( +
  • +
    +
    {count < 100 ? count : '99+'}
    +
    +
    {new Date(attempt.timestamp).toLocaleString()}
    +
    {attempt.source}
    +
    {attempt.exploiterName}
    +
    +
  • + ); + }) + } + } + + function getSourceNodeLabel(agentId: string): string { + try{ + return props.allNodes.filter(node => node.agentIds.includes(agentId))[0].getLabel() + } catch { + return 'Unknown' + } + } + + return ( +
    +

    + Exploit Timeline  + {generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')} +

    + {loadingEvents ? : +
      + {getAttemptList()} +
    + } +
    + ) +} + + +function generateToolTip(text) { + return ( + {text}} + delay={{show: 250, hide: 400}}> + + + ); +} + +function aggregateExploitationAttempts(attempts) { + let aggregatedAttempts = []; + + for (const attempt of attempts) { + let len = aggregatedAttempts.length; + if (len > 0 && areEventsIdentical(attempt, aggregatedAttempts[len - 1].data)) { + aggregatedAttempts[len - 1].count++; + } else { + aggregatedAttempts.push({data: _.cloneDeep(attempt), count: 1}); + } + } + + return aggregatedAttempts; +} + +function areEventsIdentical(event_one, event_two) { + return ((event_one.source === event_two.source) && + (event_one.exploiter_name === event_two.exploiter_name) && + (event_one.success === event_two.success)) +} + +export default ExploitationTimeline diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/ExploitionTimeline.tsx b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/ExploitionTimeline.tsx deleted file mode 100644 index 60afc602855..00000000000 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/ExploitionTimeline.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import _ from 'lodash'; -import React, {useEffect, useState} from 'react'; -import MapNode from '../../types/MapNode'; -import {OverlayTrigger, Tooltip} from 'react-bootstrap'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons/faQuestionCircle'; -import IslandHttpClient, {APIEndpoint} from '../../IslandHttpClient'; -import LoadingIcon from '../../ui-components/LoadingIcon'; - - -export type ExploitationAttempt = { - source: string; - success: boolean; - timestamp: Date; - exploiter_name: string; -} - -export type ExploitationEvent = { - source: string; - success: boolean; - timestamp: number; - exploiter_name: string; - target: string; -} - -const ExploitionTimeline = (props: { node: MapNode, allNodes: MapNode[] }) => { - - const [exploitationAttempts, setExploitationAttempts] = useState([]); - const [loadingEvents, setLoadingEvents] = useState(true); - - function getExploitationEvents() { - let url_args = {'type': 'ExploitationEvent'}; - IslandHttpClient.get(APIEndpoint.agentEvents, url_args) - .then(res => res.body).then(events => { - setExploitationAttempts(parseEvents(events, props.node)); - setLoadingEvents(false); - }) - } - - useEffect(() => { - getExploitationEvents(); - let oneSecond = 1000; - const interval = setInterval(() => { - getExploitationEvents(); - }, oneSecond); - - return () => clearInterval(interval) - }, [props.node]) - - function parseEvents(events: ExploitationEvent[], node: MapNode): ExploitationAttempt[] { - let exploitationAttempts = []; - let filteredEvents = events.filter(event => node.hasIp(event.target)) - for (const event of Object.values(filteredEvents)) { - let iface = node.networkInterfaces.find(iface => iface.includes(event.target)) - if (iface !== undefined) { - let timestampInMilliseconds: number = event.timestamp * 1000; - exploitationAttempts.push({ - source: getSourceNode(event.source).getLabel(), - success: event.success, - timestamp: new Date(timestampInMilliseconds), - exploiter_name: event.exploiter_name - }); - } - } - return exploitationAttempts; - } - - function getSourceNode(agentId: string): MapNode{ - return props.allNodes.filter(node => node.agentId === agentId)[0] - } - - return ( -
    -

    - Exploit Timeline  - {generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')} -

    - {loadingEvents ? : -
      - {_.sortBy(exploitationAttempts, 'timestamp').map(attempt => -
    • -
      -
      {new Date(attempt.timestamp).toLocaleString()}
      -
      {attempt.source}
      -
      {attempt.exploiter_name}
      -
    • - )} -
    - } -
    - ) -} - -function generateToolTip(text) { - return ( - {text}} - delay={{show: 250, hide: 400}}> - - - ); -} - -export default ExploitionTimeline diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/NodePreviewPane.tsx b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/NodePreviewPane.tsx index a73ec5f9ca4..dc0c13652f2 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/NodePreviewPane.tsx +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/NodePreviewPane.tsx @@ -5,7 +5,7 @@ import { AgentLogDownloadButton, IslandLogDownloadButton } from '../../ui-components/LogDownloadButtons'; -import ExploitionTimeline from './ExploitionTimeline'; +import ExploitationTimeline from './ExploitationTimeline'; import MapNode from '../../types/MapNode'; @@ -80,18 +80,18 @@ const NodePreviewPane = (props: any) => { ); } - function nodeInfo(node: MapNode) { + function nodeInfo() { return (
    - {osRow(node)} - {node.agentId ? statusRow(node) : ''} - {ipsRow(node)} - {downloadLogsRow(node)} + {osRow(props.item)} + {props.item.agentId ? statusRow(props.item) : ''} + {ipsRow(props.item)} + {downloadLogsRow(props.item)}
    - +
    ); } @@ -105,7 +105,7 @@ const NodePreviewPane = (props: any) => { if (props.item.island) { info = islandAssetInfo(); } else { - info = nodeInfo(props.item); + info = nodeInfo(); } break; } diff --git a/monkey/monkey_island/cc/ui/src/components/types/MapNode.tsx b/monkey/monkey_island/cc/ui/src/components/types/MapNode.tsx index 9e1dc0f3425..750a1c82185 100644 --- a/monkey/monkey_island/cc/ui/src/components/types/MapNode.tsx +++ b/monkey/monkey_island/cc/ui/src/components/types/MapNode.tsx @@ -1,3 +1,5 @@ +import _ from 'lodash'; + export enum OS { unknown = "unknown", linux = "linux", @@ -45,8 +47,8 @@ export default class MapNode { public island: boolean = false, public propagatedTo: boolean = false, public agentStartTime: Date = new Date(0), - public agentId: string | null = null, - public parentId: string | null = null) { + public agentIds: string[] = [], + public parentIds: string[] = []) { } getGroupOperatingSystem(): OS { @@ -63,8 +65,8 @@ export default class MapNode { group_components.push('island'); } - if (this.agentId) { - if (!this.island && !this.parentId) { + if (this.agentIds) { + if (!this.island && _.isEmpty(this.parentIds)) { group_components.push('manual'); } else { @@ -119,13 +121,6 @@ export function getMachineIp(machine: Machine): string { return interfaceIp(machine.network_interfaces[0]); } -export function getMachineLabel(machine: Machine): string { - if (machine.hostname) { - return machine.hostname; - } - return machine.network_interfaces[0]; -} - export enum NodeGroup { clean_unknown = "clean_unknown", clean_linux = "clean_linux", diff --git a/monkey/monkey_island/cc/ui/src/components/utils/ServerUtils.tsx b/monkey/monkey_island/cc/ui/src/components/utils/ServerUtils.tsx index 8be10570e8d..5ecc6143490 100644 --- a/monkey/monkey_island/cc/ui/src/components/utils/ServerUtils.tsx +++ b/monkey/monkey_island/cc/ui/src/components/utils/ServerUtils.tsx @@ -30,7 +30,7 @@ export function arrayToObject(array: object[], key: string): Record return array.reduce((prev, curr) => ({...prev, [curr[key]]: curr}), {}); } -function getAllAgents() { +export function getAllAgents() { return IslandHttpClient.get(APIEndpoint.agents) .then(res => { return res.body; diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 00c1320b844..e3ebf5367ab 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -290,18 +290,21 @@ body { } .timeline li { - margin-left: 1.5em; position: relative; overflow: visible; margin-bottom: 1em; } +.timeline .event-count { + text-align: center; +} + .timeline .bullet { - width: 16px; - height: 16px; + height: 25px; + width: 35px; + left: -1.3em; background: #ccc; position: absolute; - right: 100%; top: 0; bottom: 0; margin: 2px 0.5em; @@ -309,6 +312,15 @@ body { border-radius: 10px; } +.timeline .timeline-content { + margin-left: 1.5em; +} + +.exploit-timeline svg.loading-icon { + position: absolute; + margin: 0; +} + .timeline .bullet.bad { background: #d30d09; }