@@ -595,6 +625,7 @@ export default class NotebookCellRenderer extends React.Component {
>
{ selected &&
_.map(this.state.cell.messages, (msg, idx) => {
diff --git a/gui/velociraptor/src/components/notebooks/notebook-renderer.js b/gui/velociraptor/src/components/notebooks/notebook-renderer.js
index 1dbc015e2f6..b8eca282ae9 100644
--- a/gui/velociraptor/src/components/notebooks/notebook-renderer.js
+++ b/gui/velociraptor/src/components/notebooks/notebook-renderer.js
@@ -171,6 +171,7 @@ export default class NotebookRenderer extends React.Component {
selected_cell_id={this.state.selected_cell_id}
setSelectedCellId={this.setSelectedCellId}
notebook_id={this.props.notebook.notebook_id}
+ notebook_metadata={this.props.notebook}
cell_metadata={cell_md} key={idx}
upCell={this.upCell}
downCell={this.downCell}
diff --git a/gui/velociraptor/src/components/notebooks/notebook-report-renderer.js b/gui/velociraptor/src/components/notebooks/notebook-report-renderer.js
index a1c884a146b..d3e4c38cf50 100644
--- a/gui/velociraptor/src/components/notebooks/notebook-report-renderer.js
+++ b/gui/velociraptor/src/components/notebooks/notebook-report-renderer.js
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import parse from 'html-react-parser';
import VeloTable from '../core/table.js';
+import TimelineRenderer from "../timeline/timeline.js";
+import VeloLineChart from '../artifacts/line-charts.js';
import NotebookTableRenderer from './notebook-table-renderer.js';
@@ -10,6 +12,7 @@ export default class NotebookReportRenderer extends React.Component {
static propTypes = {
refresh: PropTypes.func,
cell: PropTypes.object,
+ notebook_id: PropTypes.string,
};
render() {
@@ -37,6 +40,15 @@ export default class NotebookReportRenderer extends React.Component {
};
}
+ if (domNode.name === "grr-timeline") {
+ return (
+
+ );
+ };
+
if (domNode.name === "grr-csv-viewer") {
try {
let params = JSON.parse(domNode.attribs.params);
@@ -50,6 +62,23 @@ export default class NotebookReportRenderer extends React.Component {
return domNode;
}
};
+
+ if (domNode.name === "grr-line-chart") {
+ // Figure out where the data is: attribs.value is
+ // something like data['table2']
+ let re = /'([^']+)'/;
+ let match = re.exec(domNode.attribs.value);
+ let data = this.state.data[match[1]];
+ let rows = JSON.parse(data.Response);
+ let params = JSON.parse(domNode.attribs.params);
+
+ return (
+
+ );
+ };
+
return domNode;
}
});
diff --git a/gui/velociraptor/src/components/notebooks/timelines.js b/gui/velociraptor/src/components/notebooks/timelines.js
new file mode 100644
index 00000000000..30180d2e7a1
--- /dev/null
+++ b/gui/velociraptor/src/components/notebooks/timelines.js
@@ -0,0 +1,267 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import _ from 'lodash';
+import axios from 'axios';
+import api from '../core/api-service.js';
+
+import parse from 'html-react-parser';
+import Modal from 'react-bootstrap/Modal';
+import CreatableSelect from 'react-select/creatable';
+import Select from 'react-select';
+import Button from 'react-bootstrap/Button';
+import Form from 'react-bootstrap/Form';
+import Col from 'react-bootstrap/Col';
+import Row from 'react-bootstrap/Row';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+
+const POLL_TIME = 2000;
+
+// Adds a new timeline cell below this one.
+export class AddTimelineDialog extends React.Component {
+ static propTypes = {
+ notebook_metadata: PropTypes.object.isRequired,
+ addCell: PropTypes.func,
+ closeDialog: PropTypes.func.isRequired,
+ }
+
+ state = {
+ timeline: "",
+ }
+
+ addTimeline = ()=>{
+ if (this.state.timeline) {
+ this.props.addCell('{{ Timeline "' + this.state.timeline + '" }}', "Markdown");
+ }
+ this.props.closeDialog();
+ }
+
+ render() {
+ let options = _.map(this.props.notebook_metadata.timelines, x=>{
+ return {value: x, label: x, isFixed: true, color: "#00B8D9"};
+ });
+ return (
+
+
+ Add Timeline
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+// Adds the current VQL cell to a timeline
+export class AddVQLCellToTimeline extends React.Component {
+ static propTypes = {
+ cell: PropTypes.object,
+ notebook_metadata: PropTypes.object.isRequired,
+ closeDialog: PropTypes.func.isRequired,
+ }
+
+ state = {
+ // Super timeline - Can be created
+ timeline: "",
+ time_column: "",
+
+ // name for new child timeline
+ name: "",
+
+ loading: false,
+ }
+
+ componentDidMount = () => {
+ this.source = axios.CancelToken.source();
+ this.getTables();
+ }
+
+ componentWillUnmount() {
+ this.source.cancel();
+ }
+
+ getTables = ()=>{
+ let tags = [];
+
+ parse(this.props.cell.output, {
+ replace: (domNode) => {
+ if (domNode.name === "grr-csv-viewer") {
+ try {
+ tags.push(JSON.parse(domNode.attribs.params));
+ } catch(e) { }
+ };
+ return domNode;
+ }
+ });
+ if (!tags) {
+ return;
+ }
+
+ let params = tags[0];
+ params.rows = 1;
+ api.get("v1/GetTable", params, this.source.token).then((response) => {
+ if (response.cancel) {
+ return;
+ }
+
+ if(response && response.data && response.data.columns) {
+ // Filter all columns that look like a time
+ let columns = [];
+ let rows = response.data.rows;
+ if (!rows) {
+ return;
+ }
+ _.each(response.data.rows[0].cell, (x, idx)=>{
+ if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(x)){
+ console.log(x);
+ columns.push(response.data.columns[idx]);
+ };
+ });
+
+ this.setState({columns: columns});
+ }
+ });
+ };
+
+ addTimeline = ()=>{
+ let env = {};
+ _.each(this.props.notebook_metadata.env, x=>{
+ env[x.key] = x.value;
+ });
+
+ _.each(this.props.cell.env, x=>{
+ env[x.key] = x.value;
+ });
+
+ api.post("v1/CollectArtifact", {
+ client_id: "server",
+ artifacts: ["Server.Utils.AddTimeline"],
+ specs: [{artifact: "Server.Utils.AddTimeline",
+ parameters:{"env": [
+ {"key": "NotebookId", "value": this.props.notebook_metadata.notebook_id},
+ {"key": "Timeline", "value": this.state.timeline},
+ {"key": "ChildName", "value": this.state.name},
+ {"key": "Key", "value": this.state.time_column},
+ {"key": "Query", "value": this.props.cell.input},
+ {"key": "RemoveLimit", "value": "Y"},
+ {"key": "Env", "value": JSON.stringify(env)},
+ ]},
+ }],
+ }).then(response=>{
+ // Hold onto the flow id.
+ this.setState({
+ loading: true,
+ lastOperationId: response.data.flow_id,
+ });
+
+ // Start polling for flow completion.
+ this.interval = setInterval(() => {
+ api.get("v1/GetFlowDetails", {
+ client_id: "server",
+ flow_id: this.state.lastOperationId,
+ }).then((response) => {
+ let context = response.data.context;
+ if (context.state === "RUNNING") {
+ return;
+ }
+
+ // Done! Close the dialog.
+ clearInterval(this.interval);
+ this.interval = undefined;
+ this.props.closeDialog();
+ this.setState({loading: false});
+ });
+ }, POLL_TIME);
+
+ });
+ }
+
+ render() {
+ let options = _.map(this.props.notebook_metadata.timelines, x=>{
+ return {value: x, label: x, isFixed: true, color: "#00B8D9"};
+ });
+
+ let column_options = _.map(this.state.columns, x=>{
+ return {value: x, label: x, isFixed: true, color: "#00B8D9"};
+ });
+ return (
+
+
+ Add Timeline
+
+
+
+ Super Timeline
+
+ this.setState({timeline: e && e.value})}
+ placeholder="Timeline name"
+ />
+
+
+
+
+ Timeline Name
+
+ this.setState(
+ {name: e.currentTarget.value})} />
+
+
+
+ Time column
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/gui/velociraptor/src/components/timeline/timeline.css b/gui/velociraptor/src/components/timeline/timeline.css
new file mode 100644
index 00000000000..12758ef330a
--- /dev/null
+++ b/gui/velociraptor/src/components/timeline/timeline.css
@@ -0,0 +1,84 @@
+.timeline-value-item {
+ margin-right: 2ex;
+}
+
+.timeline-value {
+ display: inline-flex;
+}
+
+.timeline-table-item {
+ color: var(--color-timeline-table-shown);
+ background: var(--color-timeline-table-shown);
+}
+
+.super-timeline .form-check {
+ line-height: normal;
+}
+
+.super-timeline .react-calendar-timeline .rct-header-root {
+ background: var(--color-timeline-header);
+}
+
+.super-timeline .react-calendar-timeline .rct-dateHeader {
+ background: var(--color-timeline-header);
+ color: var(--color-default-font);
+}
+
+.super-timeline .react-calendar-timeline .rct-dateHeader-primary {
+ color: var(--color-default-font);
+}
+
+.super-timeline .hidden-header {
+ display: none;
+}
+
+.super-timeline td {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+.super-timeline .velo-table {
+ max-height: 50vh;
+ overflow-y: auto;
+}
+
+td.timeline-time {
+ width: 15em;
+ overflow-x: hidden;
+}
+
+.timeline-item-1 {
+ background: var(--color-timeline-1);
+ color: var(--color-default-font);
+ }
+
+.timeline-item-2 {
+ background: var(--color-timeline-2);
+ color: var(--color-default-font);
+}
+
+.timeline-item-3 {
+ background: var(--color-timeline-3);
+ color: var(--color-default-font);
+}
+
+.timeline-item-4 {
+ background: var(--color-timeline-4);
+}
+
+.timeline-item-5 {
+ background: var(--color-timeline-5);
+}
+
+.timeline-item-6 {
+ background: var(--color-timeline-6);
+}
+
+.timeline-item-7 {
+ background: var(--color-timeline-7);
+}
+
+.timeline-marker {
+ background-color: var(--color-default-font);
+ width: 5px;
+}
diff --git a/gui/velociraptor/src/components/timeline/timeline.js b/gui/velociraptor/src/components/timeline/timeline.js
new file mode 100644
index 00000000000..bec065d58fa
--- /dev/null
+++ b/gui/velociraptor/src/components/timeline/timeline.js
@@ -0,0 +1,379 @@
+import "./timeline.css";
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import _ from 'lodash';
+import Timeline, {
+ TimelineMarkers,
+ CustomMarker,
+} from 'react-calendar-timeline';
+import api from '../core/api-service.js';
+import axios from 'axios';
+import { PrepareData } from '../core/table.js';
+import VeloTimestamp from "../utils/time.js";
+import VeloValueRenderer from '../utils/value.js';
+import Form from 'react-bootstrap/Form';
+
+// make sure you include the timeline stylesheet or the timeline will not be styled
+import 'react-calendar-timeline/lib/Timeline.css';
+import moment from 'moment';
+import Button from 'react-bootstrap/Button';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import BootstrapTable from 'react-bootstrap-table-next';
+import ButtonGroup from 'react-bootstrap/ButtonGroup';
+import Navbar from 'react-bootstrap/Navbar';
+
+class TimelineValueRenderer extends Component {
+ static propTypes = {
+ value: PropTypes.object,
+ }
+ state = {
+ expanded: false,
+ }
+ render() {
+ return (
+
+ { this.state.expanded ?
+
+
+
+
+
+
+ :
+
+
+ { _.map(this.props.value, (v, k) => {
+ return {k}: {v};
+ })}
+
+ }
+
+ );
+ }
+}
+
+class TimelineTableRenderer extends Component {
+ static propTypes = {
+ rows: PropTypes.array,
+ timelines: PropTypes.object,
+ }
+
+ getTimelineClass = (name) => {
+ let timelines = this.props.timelines.timelines;
+ for(let i=0;i;
+ }
+
+ let rows = this.props.rows;
+ let columns = [
+ {dataField: '_id', hidden: true},
+ {dataField: 'Time',
+ text: "Time",
+ classes: "timeline-time",
+ formatter: (cell, row, rowIndex) => {
+ return
+
+
;
+ }},
+ {dataField: 'Data',
+ text: "Data",
+ formatter: (cell, row, rowIndex) => {
+ return
;
+ }},
+ ];
+
+ // Add an id field for react ordering.
+ for (var j=0; j
+
+
+ );
+ }
+}
+
+
+export default class TimelineRenderer extends React.Component {
+ static propTypes = {
+ name: PropTypes.string,
+ notebook_id: PropTypes.string,
+ params: PropTypes.string,
+ }
+
+ componentDidMount = () => {
+ this.source = axios.CancelToken.source();
+ this.fetchRows();
+ }
+
+ componentWillUnmount() {
+ this.source.cancel();
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (!_.isEqual(prevState.version, this.state.version)) {
+ return true;
+ }
+
+ if (!_.isEqual(prevState.start_time, this.state.start_time)) {
+ this.fetchRows();
+ return true;
+ };
+
+ if (!_.isEqual(prevState.row_count, this.state.row_count)) {
+ this.fetchRows();
+ return true;
+ };
+
+ return false;
+ }
+
+ state = {
+ start_time: 0,
+ table_start: 0,
+ table_end: 0,
+ loading: true,
+ disabled: {},
+ version: 0,
+ row_count: 10,
+ };
+
+ fetchRows = () => {
+ let skip_components = [];
+ _.map(this.state.disabled, (v,k)=>{
+ if(v) {
+ skip_components.push(k);
+ };
+ });
+
+ let params = {
+ type: "TIMELINE",
+ timeline: this.props.name,
+ start_time: this.state.start_time * 1000000,
+ rows: this.state.row_count,
+ skip_components: skip_components,
+ notebook_id: this.props.notebook_id,
+ };
+
+ let url = this.props.url || "v1/GetTable";
+
+ this.source.cancel();
+ this.source = axios.CancelToken.source();
+
+ this.setState({loading: true});
+
+ api.get(url, params, this.source.token).then((response) => {
+ if (response.cancel) {
+ return;
+ }
+
+ let pageData = PrepareData(response.data);
+ this.setState({
+ table_start: response.data.start_time / 1000000,
+ table_end: response.data.end_time / 1000000,
+ columns: pageData.columns,
+ rows: pageData.rows,
+ version: Date(),
+ });
+ });
+ };
+
+ groupRenderer = ({ group }) => {
+ if (group.id < 0) {
+ return