diff --git a/drakcore/drakcore/frontend/package-lock.json b/drakcore/drakcore/frontend/package-lock.json index 8714382be..e43fd686b 100644 --- a/drakcore/drakcore/frontend/package-lock.json +++ b/drakcore/drakcore/frontend/package-lock.json @@ -8463,6 +8463,11 @@ "p-is-promise": "^2.0.0" } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -11230,6 +11235,25 @@ "warning": "^3.0.0" } }, + "react-virtualized-auto-sizer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz", + "integrity": "sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==" + }, + "react-window": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz", + "integrity": "sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==", + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, + "react-window-infinite-loader": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-window-infinite-loader/-/react-window-infinite-loader-1.0.5.tgz", + "integrity": "sha512-IcPIq8lADK3zsAcqoLqQGyduicqR6jWkiK2VUX5sKSI9X/rou6OWlOEexnGyujdNTG7hSG8OVBFEhLSDs4qrxg==" + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", diff --git a/drakcore/drakcore/frontend/package.json b/drakcore/drakcore/frontend/package.json index 78fc0f991..8110f0d7c 100644 --- a/drakcore/drakcore/frontend/package.json +++ b/drakcore/drakcore/frontend/package.json @@ -14,7 +14,10 @@ "react-d3-tree": "^1.16.1", "react-dom": "^16.13.1", "react-router-dom": "^5.2.0", - "react-scripts": "^3.4.1" + "react-scripts": "^3.4.1", + "react-virtualized-auto-sizer": "^1.0.2", + "react-window": "^1.8.5", + "react-window-infinite-loader": "^1.0.5" }, "scripts": { "start": "react-scripts start", diff --git a/drakcore/drakcore/frontend/src/AnalysisApicall.js b/drakcore/drakcore/frontend/src/AnalysisApicall.js index b067f4104..55e4325d9 100644 --- a/drakcore/drakcore/frontend/src/AnalysisApicall.js +++ b/drakcore/drakcore/frontend/src/AnalysisApicall.js @@ -1,8 +1,10 @@ import React from "react"; import { Component } from "react"; +import { FixedSizeList as List } from "react-window"; +import OptionPicker from "./OptionPicker"; +import AutoSizer from "react-virtualized-auto-sizer"; import "./App.css"; import api from "./api"; -import OptionPicker from "./OptionPicker"; class AnalysisApicall extends Component { constructor(props) { @@ -13,9 +15,13 @@ class AnalysisApicall extends Component { this.state = { calls: null, processList: [], + filter: '', + filteredResults: [], }; this.pidChanged = this.pidChanged.bind(this); + this.filterChanged = this.filterChanged.bind(this); + this.computeFiltered = this.computeFiltered.bind(this); } async pidChanged(new_pid) { @@ -24,11 +30,33 @@ class AnalysisApicall extends Component { const res = await api.getApiCalls(this.analysis_id, new_pid); const calls = res.data.split("\n").map(JSON.parse); this.setState({ calls }); + this.computeFiltered(this.state.filter); } catch (e) { this.setState({ calls: [] }); } } + computeFiltered(filter) { + if (filter === '') { + this.setState({ filteredResults: this.state.calls }); + return; + } + try { + let regex = new RegExp(filter, 'gi'); + this.setState({ + filteredResults: this.state.calls.filter((elem) => + regex.test(elem.method) + ), + }); + } catch { } + } + + filterChanged(event) { + const newFilter = event.target.value; + this.setState({filter: newFilter}); + this.computeFiltered(newFilter); + } + async componentDidMount() { const analysis = this.props.match.params.analysis; const process_tree = await api.getProcessTree(analysis); @@ -37,14 +65,11 @@ class AnalysisApicall extends Component { let result = []; process_tree.forEach((proc) => { - result.push({ - key: proc.pid, - value: `${proc.pid} – ${proc.procname || "unnamed process"}`, - }); + result.push({ key: proc.pid, value: `${proc.pid} – ${proc.procname || "unnamed process"}` }); result.push(...treeFlatten(proc.children)); }); - result.sort((a, b) => a.pid - b.pid); + result.sort((a, b) => a.key - b.key); return result; } @@ -82,33 +107,62 @@ class AnalysisApicall extends Component { </div> ); } else { - let tableContent = this.state.calls.map((entry, i) => ( - <tr key={i}> - <td>{entry.timestamp}</td> - <td> - <code>{entry.method}</code> - </td> - <td> - {entry.arguments.map((arg, i) => ( - <div key={i} className="badge-outline-primary badge mr-1"> - {arg} - </div> - ))} - </td> - </tr> - )); - content = ( - <table className="table table-centered apicallTable"> - <thead> - <tr> - <th>Timestamp</th> - <th>Method</th> - <th>Arguments</th> - </tr> - </thead> - <tbody>{tableContent}</tbody> - </table> + // let tableContent = this.state.calls.map((entry, i) => ( + // <tr key={i}> + // <td>{entry.timestamp}</td> + // <td> + // <code>{entry.method}</code> + // </td> + // <td> + // {entry.arguments.map((arg, i) => ( + // <div key={i} className="badge-outline-primary badge mr-1"> + // {arg} + // </div> + // ))} + // </td> + // </tr> + // )); + + const Row = ({ data, index, style }) => { + const entry = data[index]; + const args = entry.arguments.join(", "); + return ( + <div style={style} className="d-flex flex-row align-content-stretch"> + <div className="p-1 d-none d-md-block">{entry.timestamp}</div> + <div className="p-1"> + <code> + {entry.method}({args}) = ? + </code> + </div> + </div> + ); + }; + + let tableContent = ( + <List + itemData={this.state.filteredResults} + height={600} + itemCount={this.state.filteredResults ? this.state.filteredResults.length : 0} + itemSize={28} + > + {Row} + </List> ); + + content = tableContent; + + // content = ( + // <table className="table table-centered apicallTable"> + // <thead> + // <tr> + // <th>Timestamp</th> + // <th>Method</th> + // <th>Arguments</th> + // </tr> + // </thead> + // <tbody>{tableContent}</tbody> + // </table> + // ); } return ( @@ -119,12 +173,22 @@ class AnalysisApicall extends Component { <div className="card tilebox-one"> <div className="card-body"> - <OptionPicker - defaultSelection={url_pid} - data={this.state.processList} - onChange={this.pidChanged} - className="mb-1" - /> + <div className="row mb-2"> + <div className="col-9"> + <OptionPicker + defaultSelection={url_pid} + data={this.state.processList} + onChange={(pid) => this.pidChanged(parseInt(pid))} + /> + </div> + <input + type="text" + className="form-control col-3" + placeholder="Search API calls..." + value={this.state.filter} + onChange={this.filterChanged} + /> + </div> {content} </div> </div> diff --git a/drakcore/drakcore/frontend/src/AnalysisMain.js b/drakcore/drakcore/frontend/src/AnalysisMain.js index 361d6dc5c..2d2c33d09 100644 --- a/drakcore/drakcore/frontend/src/AnalysisMain.js +++ b/drakcore/drakcore/frontend/src/AnalysisMain.js @@ -163,7 +163,7 @@ class AnalysisMain extends Component { try { const res_graph = await api.getGraph(this.analysisID); if (res_graph.data) { - this.setState({ graphState: "loaded", graph: res_graph.data }); + //this.setState({ graphState: "loaded", graph: res_graph.data }); } else { this.setState({ graphState: "missing" }); } diff --git a/drakcore/drakcore/frontend/src/index.js b/drakcore/drakcore/frontend/src/index.js index c24e9d807..a85313303 100644 --- a/drakcore/drakcore/frontend/src/index.js +++ b/drakcore/drakcore/frontend/src/index.js @@ -2,4 +2,5 @@ import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; + ReactDOM.render(<App />, document.getElementById("root")); diff --git a/drakrun/drakrun/main.py b/drakrun/drakrun/main.py index 102779222..e5f348ea1 100644 --- a/drakrun/drakrun/main.py +++ b/drakrun/drakrun/main.py @@ -11,6 +11,7 @@ import zipfile import json import re +import functools from typing import Optional, List from stat import S_ISREG, ST_CTIME, ST_MODE, ST_SIZE @@ -78,6 +79,47 @@ def start_dnsmasq(vm_id: int, dns_server: str) -> Optional[subprocess.Popen]: ]) +def local_logs(method): + class LocalBuffer(logging.Handler): + FIELDS = ( + "levelname", + "message", + "created", + ) + + def __init__(self): + super().__init__() + self.buffer = [] + + def emit(self, record): + entry = {k: v for (k, v) in record.__dict__.items() if k in self.FIELDS} + self.buffer.append(entry) + + @functools.wraps(method) + def wrapper(self: Karton, *args, **kwargs): + handler = LocalBuffer() + try: + # Register new log handler + self.log.addHandler(handler) + method(self, *args, **kwargs) + except Exception: + self.log.exception("Analysis failed") + finally: + # Unregister local handler + self.log.removeHandler(handler) + try: + res = LocalResource("analysis_log.json", + json.dumps(handler.buffer), + bucket="drakrun") + task_uid = self.current_task.payload.get('override_uid') or self.current_task.uid + res._uid = f"{task_uid}/{res.name}" + res.upload(self.minio) + except Exception: + self.log.exception("Failed to upload analysis logs") + + return wrapper + + class DrakrunKarton(Karton): identity = "karton.drakrun-prod" filters = [ @@ -269,6 +311,7 @@ def get_profile_list() -> List[str]: return out + @local_logs def process(self): sample = self.current_task.get_resource("sample") self.log.info("hostname: {}".format(socket.gethostname()))