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) + } +
+ ); + } +};