diff --git a/drakrun/drakrun/web/frontend/package.json b/drakrun/drakrun/web/frontend/package.json index de4d74fd0..5137ba29b 100644 --- a/drakrun/drakrun/web/frontend/package.json +++ b/drakrun/drakrun/web/frontend/package.json @@ -19,7 +19,16 @@ "react-scripts": "^5.0.1", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.8.5", - "react-window-infinite-loader": "^1.0.5" + "react-window-infinite-loader": "^1.0.5", + "react-graph-vis": "^1.0.7", + "web-vitals": "^2.1.4", + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "@mui/lab": "^5.0.0-alpha.173", + "@mui/material": "^5.16.7", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0" }, "scripts": { "start": "react-scripts start", diff --git a/drakrun/drakrun/web/frontend/src/InteractiveBehavioralGraph.js b/drakrun/drakrun/web/frontend/src/InteractiveBehavioralGraph.js new file mode 100644 index 000000000..d15fd5cea --- /dev/null +++ b/drakrun/drakrun/web/frontend/src/InteractiveBehavioralGraph.js @@ -0,0 +1,115 @@ +import React, { useState } from "react"; +import Button from "@mui/material/Button"; +import ProcessTree from "./Tree.js"; +import LabTabs from "./SideBar.js"; +import api from "./api"; +import "./behavioralGraph.css"; + +function sortByProcess(jsonLinesDict, key) { + let sortedDict = {}; + let pKey = ""; + for (let entry of jsonLinesDict) { + pKey = entry["PPID"] + "_" + entry["PID"]; + if (!(pKey in sortedDict)) { + sortedDict[pKey] = new Set(); + } + sortedDict[pKey].add(entry[key]); + } + return sortedDict; +} + +function addToReport(report, dict, key) { + for (let k of Object.keys(report["processes"])) { + // Add an empty entry to all processes in case some do not manipulate files/registry keys + report["processes"][k][key] = []; + } + for (let processKey of Object.keys(dict)) { + report["processes"][processKey][key] = [...dict[processKey]]; + } +} + +export function InteractiveGraph(analysisID) { + const [process, setProcess] = useState(false); + const [ttp, setTtp] = useState(false); + const [file, setFile] = useState(false); + const [registry, setRegistry] = useState(false); + const clearAllFilters = () => { + setProcess(false); + setTtp(false); + setFile(false); + setRegistry(false); + }; + + // Fetch necessary files: `report.json`, `filetracer.log`, and `regmon.log` + var report = await api.getReport(analysisID); + var files = await api.getLog(analysisID, "filetracer.log"); + var regkeys = await api.getLog(analysisID, "regmon.log"); + + // Get a dictionary from JSON lines for `filetracer.log` and `regmon.log` + files = files.request.response.split("\n").map((line) => { + try { + return JSON.parse(line); + } catch (e) { + console.log("Parsing JSON entry failed"); + } + return null; + }); + + regkeys = regkeys.request.response.split("\n").map((line) => { + try { + return JSON.parse(line); + } catch (e) { + console.log("Parsing JSON entry failed"); + } + return null; + }); + + // Sort `filetracer.log` and `regmon.log` by entry, then append them to the respective process in the report. + var filesPerProcess = sortByProcess(files, "FileName"); + addToReport(report, filesPerProcess, "files"); + var regkeyPerProcess = sortByProcess(regkeys, "Key"); + addToReport(report, regkeyPerProcess, "registry_keys"); + + return ( + <> + setProcess(process_id)} + /> +
+ { + clearAllFilters(); + setTtp(ttp_name); + }} + onSelectFile={(filename) => { + clearAllFilters(); + setFile(filename); + }} + onSelectRegistry={(regkey) => { + clearAllFilters(); + setRegistry(regkey); + }} + /> +
+ + + ); +} diff --git a/drakrun/drakrun/web/frontend/src/SideBar.js b/drakrun/drakrun/web/frontend/src/SideBar.js new file mode 100644 index 000000000..4bfef6d7f --- /dev/null +++ b/drakrun/drakrun/web/frontend/src/SideBar.js @@ -0,0 +1,184 @@ +import React, { useState } from "react"; +import Box from "@mui/material/Box"; +import Tab from "@mui/material/Tab"; +import TabContext from "@mui/lab/TabContext"; +import TabList from "@mui/lab/TabList"; +import TabPanel from "@mui/lab/TabPanel"; +import "./behavioralGraph.css"; + +function getTTPs(report, selectedProcessID) { + let ttps_list = new Set(); + if (selectedProcessID) { + // Get a list of TTPs for only the selected process + report["processes"][selectedProcessID]["ttps"].forEach((ttp) => { + ttp["att&ck"].map((attck_name) => ttps_list.add(attck_name)); + }); + } else { + // Get a list of all the TTPs found in the report + for (let process of Object.values(report["processes"])) { + process["ttps"].forEach((ttp) => { + ttp["att&ck"].map((attck_name) => ttps_list.add(attck_name)); + }); + } + } + + return [...ttps_list]; +} + +function getFiles(report, selectedProcessID) { + let files_list = new Set(); + if (selectedProcessID) { + // Get a list of TTPs for only the selected process + report["processes"][selectedProcessID]["files"].forEach((file) => { + files_list.add(file); + }); + } else { + // Get a list of all the TTPs found in the report + for (let process of Object.values(report["processes"])) { + process["files"].forEach((file) => { + files_list.add(file); + }); + } + } + return [...files_list]; +} + +function getRegistry(report, selectedProcessID) { + let regkeys_list = new Set(); + if (selectedProcessID) { + // Get a list of TTPs for only the selected process + report["processes"][selectedProcessID]["registry_keys"].forEach( + (regkey) => { + regkeys_list.add(regkey); + } + ); + } else { + // Get a list of all the TTPs found in the report + for (let process of Object.values(report["processes"])) { + process["registry_keys"].forEach((regkey) => { + regkeys_list.add(regkey); + }); + } + } + return [...regkeys_list]; +} + +export default function LabTabs({ + report, + selectedProcess, + onSelectTTP, + onSelectFile, + onSelectRegistry, +}) { + const ttpsList = getTTPs(report, selectedProcess).map((ttp) => ( + <> + { + onSelectTTP(ttp); + }} + > + {ttp} + +
+ + )); + const filesList = getFiles(report).map((file) => ( + <> + { + onSelectFile(file); + }} + > + {file} + +
+ + )); + const registryList = getRegistry(report).map((registry) => ( + <> + { + onSelectRegistry(registry); + }} + > + {registry} + +
+ + )); + const [value, setValue] = useState("1"); + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + return ( + + + + + + + + + + + {ttpsList}
+
+ + {filesList}
+
+ + {registryList}
+
+
+
+ ); +} diff --git a/drakrun/drakrun/web/frontend/src/Tree.js b/drakrun/drakrun/web/frontend/src/Tree.js new file mode 100644 index 000000000..2fcf96e07 --- /dev/null +++ b/drakrun/drakrun/web/frontend/src/Tree.js @@ -0,0 +1,137 @@ +import React from "react"; +import Graph from "react-graph-vis"; + +const options = { + nodes: { + shape: "box", + borderWidthSelected: 2, + color: "#1976d2", + widthConstraint: { + maximum: 300, + }, + }, + layout: { + improvedLayout: true, + hierarchical: { + levelSeparation: 900, + nodeSpacing: 80, + treeSpacing: 50, + sortMethod: "hubsize", + direction: "LR", + }, + }, + interaction: { hover: true }, + physics: { + enabled: false, + }, + edges: { + color: "white", + smooth: { + type: "cubicBezier", + forceDirection: "horizontal", + roundness: 0.6, + }, + }, +}; + +function generateNodes(report, TTP, file, registry) { + let selected_pids = Object.keys(report["processes"]); + let nodes = []; + + if (TTP) { + selected_pids = selected_pids.filter((pid) => { + let ttps_set = new Set(); + report["processes"][pid]["ttps"].forEach((ttp) => { + ttp["att&ck"].forEach((attck_name) => { + ttps_set.add(attck_name); + }); + }); + return ttps_set.has(TTP); + }); + } + if (file) { + selected_pids = selected_pids.filter((pid) => + report["processes"][pid]["files"].includes(file) + ); + } + if (registry) { + selected_pids = selected_pids.filter((pid) => + report["processes"][pid]["registry_keys"].includes(registry) + ); + } + + for (let pid of Object.keys(report["processes"])) { + if (selected_pids.includes(pid) && (TTP || file || registry)) { + // mark nodes that have the ttp/file/regkey at hand with a different color + nodes.push({ + id: pid, + label: report["processes"][pid]["procname"], + color: "#2ae300", + }); + } else { + // default blueish color + nodes.push({ + id: pid, + label: report["processes"][pid]["procname"], + color: "#1976d2", + }); + } + } + return nodes; +} + +export default function ProcessTree({ + report, + selectedTTP, + selectedFile, + selectedRegistry, + onSelectProcess, +}) { + const nodes = generateNodes( + report, + selectedTTP, + selectedFile, + selectedRegistry + ); + + const state = { + counter: 5, + graph: { + nodes: nodes, + edges: Object.keys(report["processes"]) + .map((parent) => + report["processes"][parent]["children"].map((child) => ({ + from: parent, + to: child, + })) + ) + .flat(), + }, + events: { + selectNode: ({ nodes }) => { + onSelectProcess(...nodes); + }, + deselectNode: () => { + onSelectProcess(false); + }, + }, + }; + const { graph, events } = state; + return ( + { + setTimeout(() => { + network.setOptions({ + layout: { + hierarchical: false, + }, + }); + }, 1000); + }} + /> + ); +} \ No newline at end of file diff --git a/drakrun/drakrun/web/frontend/src/api/index.js b/drakrun/drakrun/web/frontend/src/api/index.js index e570d7bc5..faee5441c 100644 --- a/drakrun/drakrun/web/frontend/src/api/index.js +++ b/drakrun/drakrun/web/frontend/src/api/index.js @@ -39,6 +39,9 @@ const Api = { async getApiCalls(analysis, pid) { return axios.get(`/processed/${analysis}/apicall/${pid}`); }, + async getReport(analysis){ + return axios.get(`/processed/${analysis}/report`) + }, async query(q) { return axios.get("/query", { params: { q: q } }); }, diff --git a/drakrun/drakrun/web/frontend/src/behavioralGraph.css b/drakrun/drakrun/web/frontend/src/behavioralGraph.css new file mode 100644 index 000000000..b230406e4 --- /dev/null +++ b/drakrun/drakrun/web/frontend/src/behavioralGraph.css @@ -0,0 +1,28 @@ +.rowC{ + position: absolute; + top: 0%; + bottom: 0%; + width:26%; + right: 0%; + background-color: 'black'; + z-index:10; + } + + tr:hover { + background-color: #2ae300; + } + tr { + cursor: pointer; + margin: 0; + padding: 0; + width: 100%; + font-size: 18px; + } + td { + width: 100%; + padding-top: 11px; + padding-bottom: 11px; + padding-right: 11px; + padding-left:11px; + color: white; + } \ No newline at end of file