diff --git a/gui/velociraptor/src/App.jsx b/gui/velociraptor/src/App.jsx index 39530550176..11a0c27025b 100644 --- a/gui/velociraptor/src/App.jsx +++ b/gui/velociraptor/src/App.jsx @@ -31,7 +31,6 @@ import LoginPage from './components/welcome/login.jsx'; import LogoffPage from './components/welcome/logoff.jsx'; import KeyboardHelp from './components/core/keyboard-help.jsx'; import { UserSettings } from './components/core/user.jsx'; -import { ContextMenuPopup } from './components/utils/context.jsx'; import { Switch, Route, withRouter } from "react-router-dom"; import { Join } from './components/utils/paths.jsx'; import SecretManager from './components/secrets/secrets.jsx'; @@ -249,7 +248,6 @@ class App extends Component { - ); diff --git a/gui/velociraptor/src/components/core/paged-table.jsx b/gui/velociraptor/src/components/core/paged-table.jsx index 6aa5c5b0727..0f9cd370915 100644 --- a/gui/velociraptor/src/components/core/paged-table.jsx +++ b/gui/velociraptor/src/components/core/paged-table.jsx @@ -621,7 +621,11 @@ class VeloPagedTable extends Component { } defaultFormatter = (cell, row, rowIndex) => { - return ; + let row_data = {}; + _.each(this.activeColumns(), x=>{ + row_data[x] = row[x]; + }); + return ; } getColumnRenderer = column => { diff --git a/gui/velociraptor/src/components/timeline/timeline.jsx b/gui/velociraptor/src/components/timeline/timeline.jsx index 721827521f2..448e31bc07a 100644 --- a/gui/velociraptor/src/components/timeline/timeline.jsx +++ b/gui/velociraptor/src/components/timeline/timeline.jsx @@ -48,6 +48,18 @@ const FixedColumns = { "Message": 1, }; +const ms_to_ns = (t)=>{ + return (t || 0) * 1000000; +}; + +const ns_to_ms = (t)=>{ + return (t || 0) / 1000000; +}; + +const sec_to_ms = t=>{ + return (t || 0) * 1000; +} + class DeleteComponentDialog extends Component { static propTypes = { notebook_id: PropTypes.string, @@ -532,7 +544,7 @@ export default class TimelineRenderer extends React.Component { return true; } - if (!_.isEqual(prevState.start_time, this.state.start_time)) { + if (!_.isEqual(prevState.start_time_ms, this.state.start_time_ms)) { this.fetchRows(); return true; }; @@ -560,9 +572,9 @@ export default class TimelineRenderer extends React.Component { state = { start_time_iso: "", - start_time: 0, - table_start: 0, - table_end: 0, + start_time_ms: 0, + table_start_ms: 0, + table_end_ms: 0, loading: true, disabled: {}, version: 0, @@ -575,15 +587,15 @@ export default class TimelineRenderer extends React.Component { }; setStartTime = ts_ms=>{ - let ts = ToStandardTime(ts_ms); + let ts = new Date(ts_ms); let timezone = this.context.traits.timezone || "UTC"; this.setState({ - start_time: ts_ms, + start_time_ms: ts_ms, start_time_iso: FormatRFC3339(ts, timezone), }); } - fetchRows = (go_to_start_time) => { + fetchRows = (go_to_start_time_ms) => { let skip_components = []; _.map(this.state.disabled, (v,k)=>{ if(v) { @@ -591,9 +603,9 @@ export default class TimelineRenderer extends React.Component { }; }); - let start_time = (go_to_start_time || this.state.start_time) * 1000000; - if (start_time < 1000000000) { - start_time = 0; + let start_time_ms = this.state.start_time_ms || 0; + if (go_to_start_time_ms) { + start_time_ms = go_to_start_time_ms; } let transform = this.state.transform || {}; @@ -601,7 +613,7 @@ export default class TimelineRenderer extends React.Component { let params = { type: "TIMELINE", timeline: this.props.name, - start_time: start_time, + start_time: ms_to_ns(start_time_ms), rows: this.state.row_count, skip_components: skip_components, notebook_id: this.props.notebook_id, @@ -620,13 +632,16 @@ export default class TimelineRenderer extends React.Component { if (response.cancel) { return; } - let start_time = (response.data.start_time / 1000000) || 0; + let start_time_ms = ns_to_ms(response.data.start_time); let pageData = PrepareData(response.data); let timelines = response.data.timelines; + if (_.isEmpty(pageData.rows)) { + return; + } this.setState({ - table_start: start_time, - table_end: response.data.end_time / 1000000 || 0, + table_start_ms: start_time_ms, + table_end_ms: ns_to_ms(response.data.end_time), columns: pageData.columns, rows: pageData.rows, version: Date(), @@ -636,18 +651,17 @@ export default class TimelineRenderer extends React.Component { // If the visible table is outside the view port, adjust // the view port. if (this.state.visibleTimeStart === 0 || - start_time > this.state.visibleTimeEnd || - start_time < this.state.visibleTimeStart) { + start_time_ms > this.state.visibleTimeEnd || + start_time_ms < this.state.visibleTimeStart) { let diff = (this.state.visibleTimeEnd - this.state.visibleTimeStart) || (60 * 60 * 10000); - let visibleTimeStart = start_time - diff * 0.1; - let visibleTimeEnd = start_time + diff * 0.9; + let visibleTimeStart = start_time_ms - diff * 0.1; + let visibleTimeEnd = start_time_ms + diff * 0.9; this.setState({visibleTimeStart: visibleTimeStart, visibleTimeEnd: visibleTimeEnd}); - this.setStartTime(start_time); - + this.setStartTime(start_time_ms); } this.updateToggles(pageData.rows); @@ -672,8 +686,8 @@ export default class TimelineRenderer extends React.Component { }; nextPage = ()=>{ - if (this.state.table_end > 0) { - this.setStartTime(this.state.table_end + 1); + if (this.state.table_end_ms > 0) { + this.setStartTime(this.state.table_end_ms + 1); } } @@ -719,8 +733,8 @@ export default class TimelineRenderer extends React.Component { let timelines = this.state.timelines || []; let last_event = 0; for(let i=0;i largest) { - largest = end; + if (end_ms > largest) { + largest = end_ms; } // Handle the annotation timeline specifically if (timeline.id === "Annotation") { items.push({ id: i+1, group: timeline.id, - start_time: this.toLocalTZ(start), - end_time: this.toLocalTZ(end), + start_time: this.toLocalTZ(start_ms), + end_time: this.toLocalTZ(end_ms), canMove: false, canResize: false, canChangeGroup: false, @@ -819,8 +833,8 @@ export default class TimelineRenderer extends React.Component { items.push({ id: i+1, group: timeline.id, - start_time: this.toLocalTZ(start), - end_time: this.toLocalTZ(end), + start_time: this.toLocalTZ(start_ms), + end_time: this.toLocalTZ(end_ms), canMove: false, canResize: false, canChangeGroup: false, @@ -869,7 +883,9 @@ export default class TimelineRenderer extends React.Component { { this.renderColumnSelector() } @@ -942,7 +958,7 @@ export default class TimelineRenderer extends React.Component { > + date={this.toLocalTZ(this.state.start_time_ms)} > { ({ styles, date }) => { styles.backgroundColor = undefined; styles.width = undefined; diff --git a/gui/velociraptor/src/components/utils/annotations.jsx b/gui/velociraptor/src/components/utils/annotations.jsx new file mode 100644 index 00000000000..a42fdd2e959 --- /dev/null +++ b/gui/velociraptor/src/components/utils/annotations.jsx @@ -0,0 +1,213 @@ +import _ from 'lodash'; + +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'react-bootstrap/Modal'; +import T from '../i8n/i8n.jsx'; +import {CancelToken} from 'axios'; +import VeloValueRenderer from '../utils/value.jsx'; +import Button from 'react-bootstrap/Button'; +import { sprintf } from 'sprintf-js'; +import api from '../core/api-service.jsx'; +import Select from 'react-select'; +import Col from 'react-bootstrap/Col'; +import Form from 'react-bootstrap/Form'; +import Row from 'react-bootstrap/Row'; + +const timestampRegex = /\d{4}-\d{2}-\d{2}[T ]\d{1,2}:\d{2}:\d{2}/; + +import { ToStandardTime } from '../utils/time.jsx'; + +export default class AnnotateDialog extends Component { + static propTypes = { + row: PropTypes.object, + onClose: PropTypes.func.isRequired, + } + + componentDidMount = () => { + this.source = CancelToken.source(); + this.fetchGlobalTimelines(); + } + + state = { + collapsed: true, + global_timelines: [], + note: "", + notebook_id: "", + title: "", + name: "", + super_timeline: "", + time_column: "", + message_column: "", + } + + fetchGlobalTimelines = ()=>{ + api.get("v1/GetNotebooks", { + count: 100, + offset: 0, + }, this.source.token).then(response=>{ + if (response.cancel) { + return; + } + + if(response && response.data && response.data.items) { + let timelines = []; + _.each(response.data.items, x=>{ + _.each(x.timelines, y=>{ + timelines.push({notebook_id: x.notebook_id, + title: x.name, + name: y}); + }); + }); + this.setState({global_timelines: timelines}); + } + }); + } + + isTimestamp = x=>{ + if (timestampRegex.test(x)) { + return true; + } + + return false; + } + + annotateRow = ()=>{ + let timestamp = ""; + if (this.state.time_column) { + timestamp = this.props.row[this.state.time_column]; + } + + let ts = ToStandardTime(timestamp || "1980-01-01T00:00:00Z"); + let event = Object.assign({}, this.props.row); + if (this.state.message_column) { + event.Message = this.props.row[this.state.message_column]; + } + api.post("v1/AnnotateTimeline", { + notebook_id: this.state.notebook_id, + super_timeline: this.state.super_timeline, + timestamp: ts.getTime() * 1000000, + note: this.state.note, + event_json: JSON.stringify(event), + }, this.source.token).then(response=>{ + this.props.onClose(); + }); + } + + render() { + let clsname = this.state.collapsed ? "compact": ""; + + let global_timeline_options = _.map(this.state.global_timelines, x=>{ + return {value: x.name, + label: sprintf(T("%s (notebook %s)"), x.name, x.title), + notebook_id: x.notebook_id, + isFixed: true, + color: "#00B8D9"}; + }); + + let time_column_options = []; + _.each(this.props.row, (v, k)=>{ + if(this.isTimestamp(v)) { + time_column_options.push( {value: k, label: k, isFixed: true}); + }; + }); + + let message_column_options = _.map(this.props.row, (v, k)=>{ + return {value: k, label: k, isFixed: true}; + }); + + return + + {T("Annotate Row")} + + + + + {T("Global Timeline")} + + + this.setState({time_column: e && e.value})} + placeholder={T("Time Column")} + spellCheck="false" + /> + + + + {T("Message column")} + +