From 45d4819fdcc389cc95c4d68d6eb58e4f906b52e5 Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Thu, 3 Dec 2020 10:01:36 +1000 Subject: [PATCH] Implemented cancellation for GUI (#789) Instrumenting api calls can be used to inject latency. This is required to see what the GUI does when api calls are very latent. Started to implement API call cancellations to ensure that usage under latent conditions is possible. --- api/api.go | 74 +++++++++++++++++++ api/instrument.go | 34 +++++++++ api/notebooks.go | 19 +++++ .../definitions/Admin/Client/Upgrade.yaml | 19 ++++- flows/api.go | 3 +- .../src/components/core/api-service.js | 4 + .../src/components/core/paged-table.js | 14 +++- .../src/components/hunts/hunt-clients.js | 13 +++- .../src/components/hunts/hunt-inspector.js | 6 +- .../src/components/hunts/hunt-notebook.js | 8 +- .../src/components/hunts/hunts.js | 41 +++++++--- .../notebooks/notebook-cell-renderer.js | 33 +++++++-- .../components/notebooks/notebook-renderer.js | 25 ++++++- .../src/components/utils/number.css | 3 + .../src/components/utils/number.js | 21 ++++++ 15 files changed, 284 insertions(+), 33 deletions(-) create mode 100644 api/instrument.go create mode 100644 gui/velociraptor/src/components/utils/number.css create mode 100644 gui/velociraptor/src/components/utils/number.js diff --git a/api/api.go b/api/api.go index e31f234830a..71bb04eb230 100644 --- a/api/api.go +++ b/api/api.go @@ -24,6 +24,8 @@ import ( "fmt" "net" "net/http" + "os" + "strconv" "strings" "sync" "time" @@ -72,6 +74,9 @@ type ApiServer struct { func (self *ApiServer) CancelFlow( ctx context.Context, in *api_proto.ApiFlowRequest) (*api_proto.StartFlowResponse, error) { + + defer Instrument("CancelFlow")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.COLLECT_CLIENT @@ -108,6 +113,8 @@ func (self *ApiServer) ArchiveFlow( in *api_proto.ApiFlowRequest) (*api_proto.StartFlowResponse, error) { user_name := GetGRPCUserInfo(self.config, ctx).Name + defer Instrument("ArchiveFlow")() + permissions := acls.COLLECT_CLIENT if in.ClientId == "server" { permissions = acls.COLLECT_SERVER @@ -140,6 +147,8 @@ func (self *ApiServer) GetReport( ctx context.Context, in *api_proto.GetReportRequest) (*api_proto.GetReportResponse, error) { + defer Instrument("GetReport")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -167,6 +176,8 @@ func (self *ApiServer) CollectArtifact( ctx context.Context, in *flows_proto.ArtifactCollectorArgs) (*flows_proto.ArtifactCollectorResponse, error) { + defer Instrument("CollectArtifact")() + result := &flows_proto.ArtifactCollectorResponse{Request: in} creator := GetGRPCUserInfo(self.config, ctx).Name @@ -234,6 +245,8 @@ func (self *ApiServer) CreateHunt( ctx context.Context, in *api_proto.Hunt) (*api_proto.StartFlowResponse, error) { + defer Instrument("CreateHunt")() + // Log this event as an Audit event. in.Creator = GetGRPCUserInfo(self.config, ctx).Name in.HuntId = flows.GetNewHuntId() @@ -270,6 +283,8 @@ func (self *ApiServer) ModifyHunt( ctx context.Context, in *api_proto.Hunt) (*empty.Empty, error) { + defer Instrument("ModifyHunt")() + // Log this event as an Audit event. in.Creator = GetGRPCUserInfo(self.config, ctx).Name @@ -300,6 +315,8 @@ func (self *ApiServer) ListHunts( ctx context.Context, in *api_proto.ListHuntsRequest) (*api_proto.ListHuntsResponse, error) { + defer Instrument("ListHunts")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -323,6 +340,8 @@ func (self *ApiServer) GetHunt( return &api_proto.Hunt{}, nil } + defer Instrument("GetHunt")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -343,6 +362,8 @@ func (self *ApiServer) GetHuntResults( ctx context.Context, in *api_proto.GetHuntResultsRequest) (*api_proto.GetTableResponse, error) { + defer Instrument("GetHuntResults")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -372,6 +393,8 @@ func (self *ApiServer) ListClients( ctx context.Context, in *api_proto.SearchClientsRequest) (*api_proto.SearchClientsResponse, error) { + defer Instrument("ListClients")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS @@ -448,6 +471,9 @@ func (self *ApiServer) ListClients( func (self *ApiServer) NotifyClients( ctx context.Context, in *api_proto.NotificationRequest) (*empty.Empty, error) { + + defer Instrument("NotifyClients")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.COLLECT_CLIENT perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -473,6 +499,8 @@ func (self *ApiServer) LabelClients( ctx context.Context, in *api_proto.LabelClientsRequest) (*api_proto.APIResponse, error) { + defer Instrument("LabelClients")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.LABEL_CLIENT perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -514,6 +542,8 @@ func (self *ApiServer) GetFlowDetails( ctx context.Context, in *api_proto.ApiFlowRequest) (*api_proto.FlowDetails, error) { + defer Instrument("GetFlowDetails")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -530,6 +560,8 @@ func (self *ApiServer) GetFlowRequests( ctx context.Context, in *api_proto.ApiFlowRequest) (*api_proto.ApiFlowRequestDetails, error) { + defer Instrument("GetFlowRequests")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -549,6 +581,8 @@ func (self *ApiServer) GetUserUITraits( result := NewDefaultUserObject(self.config) user_info := GetGRPCUserInfo(self.config, ctx) + defer Instrument("GetUserUITraits")() + result.Username = user_info.Name result.InterfaceTraits.Picture = user_info.Picture result.InterfaceTraits.Permissions, _ = acls.GetEffectivePolicy(self.config, @@ -567,6 +601,8 @@ func (self *ApiServer) SetGUIOptions( in *api_proto.SetGUIOptionsRequest) (*empty.Empty, error) { user_info := GetGRPCUserInfo(self.config, ctx) + defer Instrument("SetGUIOptions")() + return &empty.Empty{}, users.SetUserOptions(self.config, user_info.Name, in) } @@ -591,6 +627,8 @@ func (self *ApiServer) VFSListDirectory( ctx context.Context, in *flows_proto.VFSListRequest) (*flows_proto.VFSListResponse, error) { + defer Instrument("VFSListDirectory")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -608,6 +646,8 @@ func (self *ApiServer) VFSStatDirectory( ctx context.Context, in *flows_proto.VFSListRequest) (*flows_proto.VFSListResponse, error) { + defer Instrument("VFSStatDirectory")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -625,6 +665,8 @@ func (self *ApiServer) VFSStatDownload( ctx context.Context, in *flows_proto.VFSStatDownloadRequest) (*flows_proto.VFSDownloadInfo, error) { + defer Instrument("VFSStatDownload")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -643,6 +685,8 @@ func (self *ApiServer) VFSRefreshDirectory( in *api_proto.VFSRefreshDirectoryRequest) ( *flows_proto.ArtifactCollectorResponse, error) { + defer Instrument("VFSRefreshDirectory")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.COLLECT_CLIENT perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -661,6 +705,8 @@ func (self *ApiServer) VFSGetBuffer( in *api_proto.VFSFileBuffer) ( *api_proto.VFSFileBuffer, error) { + defer Instrument("VFSGetBuffer")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -679,6 +725,8 @@ func (self *ApiServer) GetTable( ctx context.Context, in *api_proto.GetTableRequest) (*api_proto.GetTableResponse, error) { + defer Instrument("GetTable")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -724,6 +772,8 @@ func (self *ApiServer) GetArtifacts( in *api_proto.GetArtifactsRequest) ( *artifacts_proto.ArtifactDescriptors, error) { + defer Instrument("GetArtifacts")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -769,6 +819,8 @@ func (self *ApiServer) GetArtifactFile( in *api_proto.GetArtifactRequest) ( *api_proto.GetArtifactResponse, error) { + defer Instrument("GetArtifactFile")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -793,6 +845,8 @@ func (self *ApiServer) SetArtifactFile( in *api_proto.SetArtifactRequest) ( *api_proto.APIResponse, error) { + defer Instrument("SetArtifactFile")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.ARTIFACT_WRITER @@ -969,6 +1023,8 @@ func (self *ApiServer) GetServerMonitoringState( in *empty.Empty) ( *flows_proto.ArtifactCollectorArgs, error) { + defer Instrument("GetServerMonitoringState")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.READ_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -986,6 +1042,8 @@ func (self *ApiServer) SetServerMonitoringState( in *flows_proto.ArtifactCollectorArgs) ( *flows_proto.ArtifactCollectorArgs, error) { + defer Instrument("SetServerMonitoringState")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.SERVER_ADMIN perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -1002,6 +1060,8 @@ func (self *ApiServer) GetClientMonitoringState( ctx context.Context, in *empty.Empty) ( *flows_proto.ClientEventTable, error) { + defer Instrument("GetClientMonitoringState")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.SERVER_ADMIN perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -1019,6 +1079,8 @@ func (self *ApiServer) SetClientMonitoringState( in *flows_proto.ClientEventTable) ( *empty.Empty, error) { + defer Instrument("SetClientMonitoringState")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.SERVER_ADMIN perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -1042,6 +1104,8 @@ func (self *ApiServer) SetClientMonitoringState( func (self *ApiServer) CreateDownloadFile(ctx context.Context, in *api_proto.CreateDownloadRequest) (*api_proto.CreateDownloadResponse, error) { + defer Instrument("CreateDownloadFile")() + user_name := GetGRPCUserInfo(self.config, ctx).Name permissions := acls.PREPARE_RESULTS perm, err := acls.CheckAccess(self.config, user_name, permissions) @@ -1206,6 +1270,16 @@ func StartMonitoringService( } logger := logging.GetLogger(config_obj, &logging.FrontendComponent) + + env_inject_time, pres := os.LookupEnv("VELOCIRAPTOR_INJECT_API_SLEEP") + if pres { + logger.Info("Injecting delays for API calls since VELOCIRAPTOR_INJECT_API_SLEEP is set (only used for testing).") + result, err := strconv.ParseInt(env_inject_time, 0, 64) + if err == nil { + inject_time = int(result) + } + } + bind_addr := fmt.Sprintf("%s:%d", config_obj.Monitoring.BindAddress, config_obj.Monitoring.BindPort) diff --git a/api/instrument.go b/api/instrument.go new file mode 100644 index 00000000000..2fd8d1d60a1 --- /dev/null +++ b/api/instrument.go @@ -0,0 +1,34 @@ +package api + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + apiHistorgram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "gui_api_latency", + Help: "Latency to server API calls.", + Buckets: prometheus.LinearBuckets(0.01, 0.05, 10), + }, + []string{"api"}, + ) + + inject_time = 0 +) + +func Instrument(api string) func() time.Duration { + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + apiHistorgram.WithLabelValues(api).Observe(v) + })) + + // Instrument a delay in API calls. + if inject_time > 0 { + time.Sleep(time.Duration(inject_time) * time.Millisecond) + } + + return timer.ObserveDuration +} diff --git a/api/notebooks.go b/api/notebooks.go index c0ac33d3e98..619b5db4584 100644 --- a/api/notebooks.go +++ b/api/notebooks.go @@ -45,6 +45,8 @@ func (self *ApiServer) GetNotebooks( ctx context.Context, in *api_proto.NotebookCellRequest) (*api_proto.Notebooks, error) { + defer Instrument("GetNotebooks")() + // Empty creators are called internally. user_name := GetGRPCUserInfo(self.config, ctx).Name user_record, err := users.GetUser(self.config, user_name) @@ -151,6 +153,8 @@ func (self *ApiServer) NewNotebook( ctx context.Context, in *api_proto.NotebookMetadata) (*api_proto.NotebookMetadata, error) { + defer Instrument("NewNotebook")() + user_name := GetGRPCUserInfo(self.config, ctx).Name user_record, err := users.GetUser(self.config, user_name) if err != nil { @@ -223,6 +227,8 @@ func (self *ApiServer) NewNotebookCell( ctx context.Context, in *api_proto.NotebookCellRequest) (*api_proto.NotebookMetadata, error) { + defer Instrument("NewNotebookCell")() + if !strings.HasPrefix(in.NotebookId, "N.") { return nil, errors.New("Invalid NoteboookId") } @@ -317,6 +323,8 @@ func (self *ApiServer) UpdateNotebook( ctx context.Context, in *api_proto.NotebookMetadata) (*api_proto.NotebookMetadata, error) { + defer Instrument("UpdateNotebook")() + if !strings.HasPrefix(in.NotebookId, "N.") { return nil, errors.New("Invalid NoteboookId") } @@ -387,6 +395,8 @@ func (self *ApiServer) GetNotebookCell( ctx context.Context, in *api_proto.NotebookCellRequest) (*api_proto.NotebookCell, error) { + defer Instrument("GetNotebookCell")() + if !strings.HasPrefix(in.NotebookId, "N.") { return nil, errors.New("Invalid NoteboookId") } @@ -461,6 +471,8 @@ func (self *ApiServer) UpdateNotebookCell( ctx context.Context, in *api_proto.NotebookCellRequest) (*api_proto.NotebookCell, error) { + defer Instrument("UpdateNotebookCell")() + if !strings.HasPrefix(in.NotebookId, "N.") { return nil, errors.New("Invalid NoteboookId") } @@ -643,6 +655,8 @@ func (self *ApiServer) CancelNotebookCell( ctx context.Context, in *api_proto.NotebookCellRequest) (*empty.Empty, error) { + defer Instrument("CancelNotebookCell")() + if !strings.HasPrefix(in.NotebookId, "N.") { return nil, errors.New("Invalid NoteboookId") } @@ -670,6 +684,9 @@ func (self *ApiServer) CancelNotebookCell( func (self *ApiServer) UploadNotebookAttachment( ctx context.Context, in *api_proto.NotebookFileUploadRequest) (*api_proto.NotebookFileUploadResponse, error) { + + defer Instrument("UploadNotebookAttachment")() + user_name := GetGRPCUserInfo(self.config, ctx).Name user_record, err := users.GetUser(self.config, user_name) if err != nil { @@ -713,6 +730,8 @@ func (self *ApiServer) CreateNotebookDownloadFile( ctx context.Context, in *api_proto.NotebookExportRequest) (*empty.Empty, error) { + defer Instrument("CreateNotebookDownloadFile")() + user_name := GetGRPCUserInfo(self.config, ctx).Name user_record, err := users.GetUser(self.config, user_name) if err != nil { diff --git a/artifacts/definitions/Admin/Client/Upgrade.yaml b/artifacts/definitions/Admin/Client/Upgrade.yaml index 92a25b132ba..da0a3422303 100644 --- a/artifacts/definitions/Admin/Client/Upgrade.yaml +++ b/artifacts/definitions/Admin/Client/Upgrade.yaml @@ -13,14 +13,27 @@ description: | tools: - name: WindowsMSI +parameters: + - name: SleepDuration + default: 600 + description: | + The MSI file is typically very large and we do not want to + overwhelm the server so we stagger the download over this many + seconds. + sources: - precondition: SELECT OS From info() where OS = 'windows' query: | LET bin <= SELECT * FROM Artifact.Generic.Utils.FetchBinary( - ToolName="WindowsMSI", IsExecutable=FALSE) + ToolName="WindowsMSI", IsExecutable=FALSE, + SleepDuration=SleepDuration) // Call the binary and return all its output in a single row. - SELECT * FROM execve( - argv=["msiexec.exe", "/i", bin[0].FullPath], length=10000000) + // If we fail to download the binary we do not run the command. + SELECT * FROM foreach(row=bin, + query={ + SELECT * FROM execve( + argv=["msiexec.exe", "/i", FullPath], length=10000000) + }) diff --git a/flows/api.go b/flows/api.go index 0ece3324206..5d5b5e83977 100644 --- a/flows/api.go +++ b/flows/api.go @@ -196,7 +196,8 @@ func getAvailableDownloadFiles(config_obj *config_proto.Config, } for _, item := range files { - if strings.HasSuffix(item.Name(), ".lock") { + if strings.HasSuffix(item.Name(), ".lock") || + !item.Mode().IsRegular() { continue } diff --git a/gui/velociraptor/src/components/core/api-service.js b/gui/velociraptor/src/components/core/api-service.js index d6baedd4540..acecbc48886 100644 --- a/gui/velociraptor/src/components/core/api-service.js +++ b/gui/velociraptor/src/components/core/api-service.js @@ -13,6 +13,10 @@ if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { let api_handlers = base_path + "/api/"; const handle_error = err=>{ + if (axios.isCancel(err)) { + return {data: {}, cancel: true}; + }; + let data = err.response && err.response.data; data = data || err.message; diff --git a/gui/velociraptor/src/components/core/paged-table.js b/gui/velociraptor/src/components/core/paged-table.js index 1d0a4ac4c9e..a4e967875ca 100644 --- a/gui/velociraptor/src/components/core/paged-table.js +++ b/gui/velociraptor/src/components/core/paged-table.js @@ -5,6 +5,8 @@ import _ from 'lodash'; import 'react-bootstrap-table2-paginator/dist/react-bootstrap-table2-paginator.min.css'; import ToolkitProvider from 'react-bootstrap-table2-toolkit'; +import axios from 'axios'; + import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -108,9 +110,15 @@ class VeloPagedTable extends Component { } componentDidMount = () => { + this.source = axios.CancelToken.source(); this.fetchRows(); } + componentWillUnmount() { + this.source.cancel(); + } + + componentDidUpdate(prevProps, prevState, snapshot) { if (!_.isEqual(prevProps.params, this.props.params)) { this.setState({start_row: 0, toggles: {}, columns: []}); @@ -163,7 +171,11 @@ class VeloPagedTable extends Component { let url = this.props.url || "v1/GetTable"; this.setState({loading: true}); - api.get(url, params).then((response) => { + api.get(url, params, this.source.token).then((response) => { + if (response.cancel) { + return; + } + let pageData = PrepareData(response.data); let toggles = Object.assign({}, this.state.toggles); let columns = pageData.columns; diff --git a/gui/velociraptor/src/components/hunts/hunt-clients.js b/gui/velociraptor/src/components/hunts/hunt-clients.js index 03a44ebae2d..a40b7ac1242 100644 --- a/gui/velociraptor/src/components/hunts/hunt-clients.js +++ b/gui/velociraptor/src/components/hunts/hunt-clients.js @@ -4,6 +4,7 @@ import VeloPagedTable from '../core/paged-table.js'; import VeloTimestamp from "../utils/time.js"; import ClientLink from '../clients/client-link.js'; import FlowLink from '../flows/flow-link.js'; +import NumberFormatter from '../utils/number.js'; export default class HuntClients extends React.Component { static propTypes = { @@ -30,8 +31,16 @@ export default class HuntClients extends React.Component { }, FlowId: (cell, row) => { return ; - } - + }, + Duration: (cell, row) => { + return ; + }, + TotalBytes: (cell, row) => { + return ; + }, + TotalRows: (cell, row) => { + return ; + }, }; return ( diff --git a/gui/velociraptor/src/components/hunts/hunt-inspector.js b/gui/velociraptor/src/components/hunts/hunt-inspector.js index 4bbc8759ca9..9a35eae57b2 100644 --- a/gui/velociraptor/src/components/hunts/hunt-inspector.js +++ b/gui/velociraptor/src/components/hunts/hunt-inspector.js @@ -8,7 +8,7 @@ import HuntOverview from './hunt-overview.js'; import HuntRequest from './hunt-request.js'; import HuntClients from './hunt-clients.js'; import HuntNotebook from './hunt-notebook.js'; - +import Spinner from '../utils/spinner.js'; import { withRouter } from "react-router-dom"; @@ -28,6 +28,10 @@ class HuntInspector extends React.Component { } render() { + if (this.props.hunt && this.props.hunt.loading) { + return ; + } + if (!this.props.hunt || !this.props.hunt.hunt_id) { return
Please select a hunt above
; } diff --git a/gui/velociraptor/src/components/hunts/hunt-notebook.js b/gui/velociraptor/src/components/hunts/hunt-notebook.js index 6677921fc16..fb907b34c05 100644 --- a/gui/velociraptor/src/components/hunts/hunt-notebook.js +++ b/gui/velociraptor/src/components/hunts/hunt-notebook.js @@ -35,7 +35,7 @@ export default class HuntNotebook extends React.Component { } componentWillUnmount() { - this.source.cancel("unmounted"); + this.source.cancel(); clearInterval(this.interval); } @@ -63,7 +63,7 @@ export default class HuntNotebook extends React.Component { this.setState({loading: true}); api.get("v1/GetNotebooks", { notebook_id: notebook_id, - }).then(response=>{ + }, this.source.token).then(response=>{ let notebooks = response.data.items || []; if (notebooks.length > 0) { @@ -80,7 +80,7 @@ export default class HuntNotebook extends React.Component { public: true, }; - api.post('v1/NewNotebook', request).then((response) => { + api.post('v1/NewNotebook', request, this.source.token).then((response) => { let cell_metadata = response.data && response.data.cell_metadata; if (_.isEmpty(cell_metadata)) { return; @@ -91,7 +91,7 @@ export default class HuntNotebook extends React.Component { type: "VQL", cell_id: cell_metadata[0].cell_id, input: this.getCellVQL(this.props.hunt), - }).then((response) => { + }, this.source.token).then((response) => { this.fetchNotebooks(); }); }); diff --git a/gui/velociraptor/src/components/hunts/hunts.js b/gui/velociraptor/src/components/hunts/hunts.js index 9c58f64c63f..ab06dea8ec5 100644 --- a/gui/velociraptor/src/components/hunts/hunts.js +++ b/gui/velociraptor/src/components/hunts/hunts.js @@ -2,7 +2,7 @@ import React from 'react'; import SplitPane from 'react-split-pane'; import HuntList from './hunt-list.js'; import HuntInspector from './hunt-inspector.js'; - +import _ from 'lodash'; import api from '../core/api-service.js'; import axios from 'axios'; @@ -30,6 +30,7 @@ class VeloHunts extends React.Component { componentDidMount = () => { this.source = axios.CancelToken.source(); + this.get_hunts_source = axios.CancelToken.source(); this.interval = setInterval(this.fetchHunts, POLL_TIME); this.fetchHunts(); } @@ -54,11 +55,18 @@ class VeloHunts extends React.Component { loadFullHunt = (hunt) => { this.setState({selected_hunt: hunt}); + this.get_hunts_source.cancel(); + this.get_hunts_source = axios.CancelToken.source(); + this.setState({full_selected_hunt: {loading: true}}); api.get("v1/GetHunt", { hunt_id: hunt.hunt_id, - }).then((response) => { - this.setState({full_selected_hunt: response.data}); + }, this.get_hunts_source.token).then((response) => { + if(_.isEmpty(response.data)) { + this.setState({full_selected_hunt: {loading: true}}); + } else { + this.setState({full_selected_hunt: response.data}); + } }); } @@ -71,25 +79,34 @@ class VeloHunts extends React.Component { offset: 0, }).then((response) => { let hunts = response.data.items || []; + // If the router specifies a selected flow id, we select it. - for(var i=0;i { - this.setState({full_selected_hunt: response.data}); + if(_.isEmpty(response.data)) { + this.setState({full_selected_hunt: {loading: true}}); + } else { + this.setState({full_selected_hunt: response.data}); + } + }); } } diff --git a/gui/velociraptor/src/components/notebooks/notebook-cell-renderer.js b/gui/velociraptor/src/components/notebooks/notebook-cell-renderer.js index 1e714565d50..b749800c729 100644 --- a/gui/velociraptor/src/components/notebooks/notebook-cell-renderer.js +++ b/gui/velociraptor/src/components/notebooks/notebook-cell-renderer.js @@ -24,6 +24,7 @@ import Completer from '../artifacts/syntax.js'; import { getHuntColumns } from '../hunts/hunt-list.js'; import VeloTimestamp from "../utils/time.js"; +import axios from 'axios'; import api from '../core/api-service.js'; const cell_types = ["Markdown", "VQL"]; @@ -53,15 +54,24 @@ class AddCellFromHunt extends React.PureComponent { } componentDidMount = () => { + this.source = axios.CancelToken.source(); api.get("v1/ListHunts", { count: 100, offset: 0, - }).then((response) => { + }, this.source.token).then((response) => { + if (response.cancel) { + return; + } + let hunts = response.data.items || []; this.setState({hunts: hunts}); }); } + componentWillUnmount = () => { + this.source.cancel(); + } + render() { const selectRow = { mode: "radio", @@ -152,9 +162,14 @@ export default class NotebookCellRenderer extends React.Component { } componentDidMount() { + this.source = axios.CancelToken.source(); this.fetchCellContents(); } + componentWillUnmount() { + this.source.cancel(); + } + componentDidUpdate = (prevProps, prevState, rootNode) => { // Do not update the editor if we are currently editing it - // otherwise it will wipe the text. @@ -185,7 +200,11 @@ export default class NotebookCellRenderer extends React.Component { api.get("v1/GetNotebookCell", { notebook_id: this.props.notebook_id, cell_id: this.props.cell_metadata.cell_id, - }).then((response) => { + }, this.source.token).then((response) => { + if (response.cancel) { + return; + } + let cell = response.data; this.setState({cell: cell, input: cell.input}); }); @@ -235,7 +254,11 @@ export default class NotebookCellRenderer extends React.Component { type: this.state.cell.type || "Markdown", currently_editing: false, input: this.state.cell.input, - }).then( (response) => { + }, this.source.token).then( (response) => { + if (response.cancel) { + return; + } + this.setState({cell: response.data, currently_editing: false}); }); }; @@ -249,7 +272,7 @@ export default class NotebookCellRenderer extends React.Component { api.post("v1/CancelNotebookCell", { notebook_id: this.props.notebook_id, cell_id: this.state.cell.cell_id, - }); + }, this.source.token); } pasteEvent = (e) => { @@ -271,7 +294,7 @@ export default class NotebookCellRenderer extends React.Component { }; api.post( - 'v1/UploadNotebookAttachment', request + 'v1/UploadNotebookAttachment', request, this.source.token ).then((response) => { this.state.ace.insert("\n!["+blob.name+"]("+response.data.url+")\n"); }, function failure(response) { diff --git a/gui/velociraptor/src/components/notebooks/notebook-renderer.js b/gui/velociraptor/src/components/notebooks/notebook-renderer.js index 67320466fda..d42f48a3c1b 100644 --- a/gui/velociraptor/src/components/notebooks/notebook-renderer.js +++ b/gui/velociraptor/src/components/notebooks/notebook-renderer.js @@ -6,6 +6,7 @@ import Spinner from '../utils/spinner.js'; import _ from 'lodash'; import api from '../core/api-service.js'; +import axios from 'axios'; export default class NotebookRenderer extends React.Component { static propTypes = { @@ -23,6 +24,14 @@ export default class NotebookRenderer extends React.Component { } + componentDidMount = () => { + this.source = axios.CancelToken.source(); + } + + componentWillUnmount() { + this.source.cancel(); + } + upCell = (cell_id) => { let cell_metadata = this.props.notebook.cell_metadata; let changed = false; @@ -42,7 +51,9 @@ export default class NotebookRenderer extends React.Component { if (changed) { this.props.notebook.cell_metadata = new_cells; this.setState({loading: true}); - api.post('v1/UpdateNotebook', this.props.notebook).then(response=>{ + api.post('v1/UpdateNotebook', + this.props.notebook, + this.source.token).then(response=>{ this.props.fetchNotebooks(); this.setState({loading: false}); }, (response) => { @@ -72,7 +83,9 @@ export default class NotebookRenderer extends React.Component { if (changed) { this.props.notebook.cell_metadata = new_cells; this.setState({loading: true}); - api.post('v1/UpdateNotebook', this.props.notebook).then(response=>{ + api.post('v1/UpdateNotebook', + this.props.notebook, + this.source.token).then(response=>{ this.props.fetchNotebooks(); this.setState({loading: false}); }, function failure(response) { @@ -103,7 +116,9 @@ export default class NotebookRenderer extends React.Component { if (changed) { this.props.notebook.cell_metadata = new_cells; this.setState({loading: true}); - api.post('v1/UpdateNotebook', this.props.notebook).then(response=>{ + api.post('v1/UpdateNotebook', + this.props.notebook, + this.source.token).then(response=>{ this.props.fetchNotebooks(); this.setState({loading: false}); }, function failure(response) { @@ -129,7 +144,9 @@ export default class NotebookRenderer extends React.Component { } this.setState({loading: true}); - api.post('v1/NewNotebookCell', request).then((response) => { + api.post('v1/NewNotebookCell', + request, + this.source.token).then((response) => { this.props.fetchNotebooks(); this.setState({selected_cell_id: response.data.latest_cell_id, loading: false}); diff --git a/gui/velociraptor/src/components/utils/number.css b/gui/velociraptor/src/components/utils/number.css new file mode 100644 index 00000000000..5c484ff7cb7 --- /dev/null +++ b/gui/velociraptor/src/components/utils/number.css @@ -0,0 +1,3 @@ +.numeric { + text-align: right; +} diff --git a/gui/velociraptor/src/components/utils/number.js b/gui/velociraptor/src/components/utils/number.js new file mode 100644 index 00000000000..77e8518d164 --- /dev/null +++ b/gui/velociraptor/src/components/utils/number.js @@ -0,0 +1,21 @@ +import "./number.css"; + +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; + +export default class NumberFormatter extends React.Component { + static propTypes = { + value: PropTypes.any, + }; + + render() { + return ( +
+ { _.isNumber(this.props.value) && + JSON.stringify(this.props.value) + } +
+ ); + } +};