Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aggregate events in preview pane on map page #2468

Merged
39 changes: 22 additions & 17 deletions monkey/monkey_island/cc/ui/src/components/map/MapPageWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,7 +24,7 @@ const MapPageWrapper = (props) => {
const [mapNodes, setMapNodes] = useState<MapNode[]>([]);
const [nodes, setNodes] = useState<Record<string, Node>>({});
const [machines, setMachines] = useState<Record<string, Machine>>({});
const [agents, setAgents] = useState<Record<string, Agent>>({});
const [agents, setAgents] = useState<Agent[]>([]);
const [propagationEvents, setPropagationEvents] = useState({});

const [graph, setGraph] = useState<Graph>({edges: [], nodes: []});
Expand All @@ -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));
}

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -110,9 +115,9 @@ const MapPageWrapper = (props) => {
machine.hostname,
machine.island,
propagatedTo,
agentStartTime,
agentID,
parentID
lastAgentStartTime,
agentIDs,
parentIDs
));

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ExploitationAttempt[]>([]);
const [loadingEvents, setLoadingEvents] = useState(true);
const [updateTimer, setUpdateTimer] = useState<NodeJS.Timeout>(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 (<li className={'timeline-content'}>No exploits were attempted on this node yet.</li>);
} else {
return aggregateExploitationAttempts(_.sortBy(exploitationAttempts, 'timestamp'))
.map(data => {
const {data: attempt, count: count} = data;
return (
<li key={`${attempt.timestamp}${String(Math.random())}`}>
<div className={'bullet ' + (attempt.success ? 'bad' : '')}>
<div className={'event-count'}>{count < 100 ? count : '99+'}</div></div>
<div className={'timeline-content'} >
<div>{new Date(attempt.timestamp).toLocaleString()}</div>
<div>{attempt.source}</div>
<div>{attempt.exploiterName}</div>
</div>
</li>
);
})
}
}

function getSourceNodeLabel(agentId: string): string {
try{
return props.allNodes.filter(node => node.agentIds.includes(agentId))[0].getLabel()
} catch {
return 'Unknown'
}
}

return (
<div className={'exploit-timeline'}>
<h4 style={{'marginTop': '2em'}}>
Exploit Timeline&nbsp;
{generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')}
</h4>
{loadingEvents ? <LoadingIcon/> :
<ul className="timeline">
{getAttemptList()}
</ul>
}
</div>
)
}


function generateToolTip(text) {
return (
<OverlayTrigger placement="top"
overlay={<Tooltip id="tooltip">{text}</Tooltip>}
delay={{show: 250, hide: 400}}>
<a><FontAwesomeIcon icon={faQuestionCircle} style={{'marginRight': '0.5em'}}/></a>
</OverlayTrigger>
);
}

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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
AgentLogDownloadButton,
IslandLogDownloadButton
} from '../../ui-components/LogDownloadButtons';
import ExploitionTimeline from './ExploitionTimeline';
import ExploitationTimeline from './ExploitationTimeline';
import MapNode from '../../types/MapNode';


Expand Down Expand Up @@ -80,18 +80,18 @@ const NodePreviewPane = (props: any) => {
);
}

function nodeInfo(node: MapNode) {
function nodeInfo() {
return (
<div>
<table className='table table-condensed'>
<tbody>
{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)}
</tbody>
</table>
<ExploitionTimeline node={node} allNodes={props.allNodes}/>
<ExploitationTimeline node={props.item} allNodes={props.allNodes}/>
</div>
);
}
Expand All @@ -105,7 +105,7 @@ const NodePreviewPane = (props: any) => {
if (props.item.island) {
info = islandAssetInfo();
} else {
info = nodeInfo(props.item);
info = nodeInfo();
}
break;
}
Expand Down
Loading