Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
39f2b4a
UI: Partially implement the new exploits timeline in the preview pane
shreyamalviya Oct 18, 2022
987d77f
UI: Add exploitsTimeline state in PreviewPane.js
shreyamalviya Oct 19, 2022
1bba0a5
UI: Remove no longer relevant comment in PreviewPane.js
shreyamalviya Oct 19, 2022
bebfd64
UI: Add type for ExploitationEvent
cakekoa Oct 19, 2022
e1535fa
UI: Fetch ExploitaitonEvents
cakekoa Oct 19, 2022
76071ca
UI: Add interfaceIp function
cakekoa Oct 19, 2022
a493666
UI: Add getMachineLabel function
cakekoa Oct 19, 2022
c0fd0b5
UI: Add exploitation attempts to MapNode
cakekoa Oct 19, 2022
cddafab
UI: Display exploitation attempts
cakekoa Oct 19, 2022
102a695
UI: Update mapnodes on change to exploitation events
cakekoa Oct 19, 2022
3062b59
UI: Remove arrayToObject() call in getExploitationEvents() in MapPage…
shreyamalviya Oct 20, 2022
6402a74
UI: Convert timestamp to milliseconds before setting in exploitationA…
shreyamalviya Oct 20, 2022
ecc42ff
UI: Sort exploit attempts by time in exploits timeline
shreyamalviya Oct 20, 2022
974e6f7
UI: Add scrollbar to the exploitation timeline
VakarisZ Oct 20, 2022
6a183b6
Project: Add entries for preview pane changes
shreyamalviya Oct 20, 2022
a1c1022
UI: Return empty div from getExploitsTimeline() if no exploitation at…
shreyamalviya Oct 20, 2022
423b5fa
UI: Split exploitation timeline into a separate component
VakarisZ Oct 20, 2022
783712c
UI: Refactor PreviewPane to typescript
VakarisZ Oct 20, 2022
edf95b2
UI: Fixup log download buttons
VakarisZ Oct 20, 2022
bafc3e2
UI: Rename previewPane to NodePreviewPane
VakarisZ Oct 20, 2022
eadd965
UI: Move data fetching to ExploitionTimeline.tsx
VakarisZ Oct 21, 2022
8672ef1
UI: Fix source item display in the ExploitationTimeline.tsx
VakarisZ Oct 21, 2022
0dbf3ac
UI: Aggregate exploitation events in preview pane on map page
shreyamalviya Oct 20, 2022
da2dca0
UI: Rename 'exploition' -> 'exploitation'
shreyamalviya Oct 20, 2022
ef4ef45
UI: Make list key unique
cakekoa Oct 20, 2022
beb4478
UI: Simplify aggregation logic
cakekoa Oct 20, 2022
b85554e
UI: Fix bugs in exploitation timeline aggregation
VakarisZ Oct 21, 2022
18e5b77
UI: Fixup event aggregation
VakarisZ Oct 21, 2022
e9d061e
UI: Use props instead of parameter in NodePreviewPane.tsx
VakarisZ Oct 24, 2022
70cdaec
UI: Fix race condition bugs in ExploitationTimeline.tsx
VakarisZ Oct 24, 2022
e58c974
UI: Fix unknown exploitation source bug in timeline
VakarisZ Oct 24, 2022
d7c5f10
Merge branch '2430-aggregate-events-in-preview-pane' into 2430-exploi…
mssalvatore Oct 24, 2022
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
- `/api/machines` endpoint. #2362
- GET method to `/api/agent-events`. #2405
- `/api/nodes` endpoint. #2334
- Scrollbar to preview pane's exploit timeline in the map page. #2455

### Changed
- Reset workflow. Now it's possible to delete data gathered by agents without
Expand Down Expand Up @@ -72,6 +73,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
- "/api/monkey-control/stop-all-agents" to "/api/agent-signals/terminate-all-agents". #2261
- Format of scanned machines table in the security report. #2267
- "Local network scan" option to "Scan Agent's networks". #2299
- Information displayed in the preview pane in the map page. #2455

### Removed
- VSFTPD exploiter. #1533
Expand Down
42 changes: 25 additions & 17 deletions monkey/monkey_island/cc/ui/src/components/map/MapPageWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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,
CommunicationType,
Communications,
CommunicationType,
getMachineIp,
Machine,
Node
Expand All @@ -14,6 +14,7 @@ import _ from 'lodash';
import generateGraph, {Graph} from './GraphCreator';

const MapPageWrapper = (props) => {

function getPropagationEvents() {
let url_args = {'type': 'PropagationEvent', 'success': true};
return IslandHttpClient.get(APIEndpoint.agentEvents, url_args)
Expand All @@ -23,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 @@ -35,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 All @@ -57,7 +58,6 @@ const MapPageWrapper = (props) => {
}
}, [mapNodes]);


useEffect(() => {
setMapNodes(buildMapNodes());
}, [nodes, machines, propagationEvents]);
Expand Down Expand Up @@ -86,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 @@ -108,10 +115,11 @@ const MapPageWrapper = (props) => {
machine.hostname,
machine.island,
propagatedTo,
agentStartTime,
agentID,
parentID
lastAgentStartTime,
agentIDs,
parentIDs
));

}

return mapNodes;
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
Loading