Skip to content

Commit

Permalink
Added the ability for hunt expiry to be modified (#3088)
Browse files Browse the repository at this point in the history
There is a new modify hunt GUI dialog as well as being able to modify
the expiry with the VQL hunt_update() function.
  • Loading branch information
scudette authored Nov 10, 2023
1 parent d22c46e commit 0f2e5a2
Show file tree
Hide file tree
Showing 21 changed files with 268 additions and 124 deletions.
27 changes: 18 additions & 9 deletions api/proto/hunts.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/proto/hunts.proto
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ message HuntMutation {
string description = 3;
Hunt.State state = 4;
uint64 start_time = 5;
uint64 expires = 7;

// A mutation can directly assign an existing flow to the
// hunt. This allows a flow to be rerun and added to the hunt
Expand Down
4 changes: 2 additions & 2 deletions gui/velociraptor/src/components/flows/flows-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,12 @@ export class SaveCollectionDialog extends React.PureComponent {
<Modal.Body>
{T("ArtifactFavorites", artifacts)}
<VeloForm
param={{name: "Name", description: T("New Favorite name")}}
param={{name: T("Name"), description: T("New Favorite name")}}
value={this.state.name}
setValue={x=>this.setState({name:x})}
/>
<VeloForm
param={{name: "Description",
param={{name: T("Description"),
description: T("Describe this favorite")}}
value={this.state.description}
setValue={x=>this.setState({description:x})}
Expand Down
110 changes: 109 additions & 1 deletion gui/velociraptor/src/components/hunts/hunt-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { withRouter } from "react-router-dom";
import { formatColumns } from "../core/table.jsx";

import VeloForm from '../forms/form.jsx';

import NewHuntWizard from './new-hunt.jsx';
import DeleteNotebookDialog from '../notebooks/notebook-delete.jsx';
import ExportNotebook from '../notebooks/export-notebook.jsx';
Expand All @@ -25,6 +27,94 @@ import api from '../core/api-service.jsx';
import {CancelToken} from 'axios';


class ModifyHuntDialog extends React.Component {
static contextType = UserConfig;

static propTypes = {
hunt: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
onResolve: PropTypes.func.isRequired,
}

state = {
description: "",
expires: ""
}

componentDidMount = () => {
this.source = CancelToken.source();
}

componentWillUnmount() {
this.source.cancel();
}

getExpiryEpoch = ()=>{
let expires = this.props.hunt.expires / 1000000;
if(this.state.expires) {
expires = Date.parse(this.state.expires) / 1000;
}
return expires;
}

modifyHunt = ()=>{
let hunt_id = this.props.hunt &&
this.props.hunt.hunt_id;

let description = this.state.description || this.props.hunt.hunt_description;

if (!hunt_id) { return; };

api.post("v1/ModifyHunt", {
hunt_description: description,
expires: this.getExpiryEpoch() * 1000000,
hunt_id: hunt_id,
}, this.source.token).then((response) => {
this.props.onResolve();
});
}

render() {
let description = this.state.description || this.props.hunt.hunt_description;
let expires = this.getExpiryEpoch();
let now = Date.now() / 1000;

return <Modal show={true}
onHide={this.props.onCancel} >
<Modal.Header closeButton>
<Modal.Title>{T("Modify Hunt")}</Modal.Title>
</Modal.Header>

<Modal.Body>
<VeloForm
param={{name: T("Description"), description: T("Hunt description")}}
value={description}
setValue={x=>this.setState({description:x})}
/>
<VeloForm
param={{name: T("Expiry"), type: "timestamp",
description: T("Time hunt will expire")}}
value={expires}
setValue={x=>this.setState({expires:x})}
/>
</Modal.Body>

<Modal.Footer>
<Button variant="secondary"
onClick={this.props.onCancel}>
{T("Close")}
</Button>
<Button variant="primary"
disabled={expires < now}
onClick={this.modifyHunt}>
{T("Run it!")}
</Button>
</Modal.Footer>
</Modal>;
}
}


class HuntList extends React.Component {
static contextType = UserConfig;

Expand Down Expand Up @@ -76,6 +166,7 @@ class HuntList extends React.Component {
showDeleteNotebook: false,
showCopyWizard: false,
showNotebookUploadsDialog: false,
showModifyHuntDialog: false,

filter: "",
}
Expand Down Expand Up @@ -270,7 +361,15 @@ class HuntList extends React.Component {
onResolve={this.setCollectionRequest}
/>
}

{ this.state.showModifyHuntDialog &&
<ModifyHuntDialog
onResolve={()=>{
this.props.updateHunts();
this.setState({showModifyHuntDialog: false});
}}
onCancel={()=>this.setState({showModifyHuntDialog: false})}
hunt={this.props.selected_hunt}/>
}
{this.state.showRunHuntDialog &&
<Modal show={this.state.showRunHuntDialog}
onHide={() => this.setState({ showRunHuntDialog: false })} >
Expand Down Expand Up @@ -350,6 +449,15 @@ class HuntList extends React.Component {
<FontAwesomeIcon icon="plus" />
<span className="sr-only">{T("New Hunt")}</span>
</Button>
<Button data-tooltip={T("Modify Hunt")}
data-position="right"
className="btn-tooltip"
disabled={!this.props.selected_hunt}
onClick={() => this.setState({ showModifyHuntDialog: true })}
variant="default">
<FontAwesomeIcon icon="wrench" />
<span className="sr-only">{T("Modify Hunt")}</span>
</Button>
<Button data-tooltip={T("Run Hunt")}
data-position="right"
className="btn-tooltip"
Expand Down
5 changes: 4 additions & 1 deletion services/hunt_dispatcher/hunt_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,10 +405,13 @@ func (self *HuntDispatcher) checkForExpiry(
now := uint64(utils.GetTime().Now().UnixNano() / 1000)

self.ApplyFuncOnHunts(func(hunt_obj *api_proto.Hunt) error {
if now > hunt_obj.Expires {
if hunt_obj.State == api_proto.Hunt_RUNNING &&
now > hunt_obj.Expires {

self.MutateHunt(ctx, config_obj,
&api_proto.HuntMutation{
HuntId: hunt_obj.HuntId,
State: api_proto.Hunt_STOPPED,
Stats: &api_proto.HuntStats{Stopped: true},
})
}
Expand Down
6 changes: 3 additions & 3 deletions services/hunt_dispatcher/modify.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ func (self *HuntDispatcher) ModifyHunt(
// We can not modify the hunt directly, instead we send a
// mutation to the hunt manager on the master.
mutation := &api_proto.HuntMutation{
HuntId: hunt_modification.HuntId,
Description: hunt_modification.HuntDescription,
HuntId: hunt_modification.HuntId,
}

// Is the description changed?
if hunt_modification.HuntDescription != "" {
if hunt_modification.HuntDescription != "" || hunt_modification.Expires > 0 {
mutation.Description = hunt_modification.HuntDescription
mutation.Expires = hunt_modification.Expires

// Archive the hunt.
} else if hunt_modification.State == api_proto.Hunt_ARCHIVED {
Expand Down
6 changes: 6 additions & 0 deletions services/hunt_manager/hunt_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ func (self *HuntManager) processMutation(
modification = services.HuntPropagateChanges
}

if mutation.Expires > 0 {
hunt_obj.Expires = mutation.Expires

modification = services.HuntPropagateChanges
}

// Hunt is restarted, notify all connected clients
if mutation.StartTime > 0 {
hunt_obj.StartTime = mutation.StartTime
Expand Down
2 changes: 1 addition & 1 deletion vql/common/clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (self ClockPlugin) Call(
time.Duration(arg.PeriodMs)*time.Second/1000

if !utils.IsNil(arg.StartTime) {
start, err := functions.TimeFromAny(scope, arg.StartTime)
start, err := functions.TimeFromAny(ctx, scope, arg.StartTime)
if err != nil {
scope.Log("clock: %v", err)
return
Expand Down
10 changes: 7 additions & 3 deletions vql/functions/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,22 @@ func (self _Timestamp) Call(ctx context.Context, scope vfilter.Scope,
}
}

result, err := TimeFromAny(scope, arg.Epoch)
result, err := TimeFromAny(ctx, scope, arg.Epoch)
if err != nil {
return vfilter.Null{}
}

return result
}

func TimeFromAny(scope vfilter.Scope, timestamp vfilter.Any) (time.Time, error) {
func TimeFromAny(ctx context.Context,
scope vfilter.Scope, timestamp vfilter.Any) (time.Time, error) {
sec := int64(0)
dec := int64(0)
switch t := timestamp.(type) {
case vfilter.LazyExpr:
return TimeFromAny(ctx, scope, t.ReduceWithScope(ctx, scope))

case float64:
sec_f, dec_f := math.Modf(t)
sec = int64(sec_f)
Expand All @@ -212,7 +216,7 @@ func TimeFromAny(scope vfilter.Scope, timestamp vfilter.Any) (time.Time, error)
// It might really be an int encoded as a string.
int_time, ok := utils.ToInt64(t)
if ok {
return TimeFromAny(scope, int_time)
return TimeFromAny(ctx, scope, int_time)
}

return ParseTimeFromString(scope, t)
Expand Down
16 changes: 8 additions & 8 deletions vql/networking/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ func (self *UploadFunction) Call(ctx context.Context,
return vfilter.Null{}
}

mtime, err := functions.TimeFromAny(scope, arg.Mtime)
mtime, err := functions.TimeFromAny(ctx, scope, arg.Mtime)
if err != nil {
mtime = stat.ModTime()
}

atime, _ := functions.TimeFromAny(scope, arg.Atime)
ctime, _ := functions.TimeFromAny(scope, arg.Ctime)
btime, _ := functions.TimeFromAny(scope, arg.Btime)
atime, _ := functions.TimeFromAny(ctx, scope, arg.Atime)
ctime, _ := functions.TimeFromAny(ctx, scope, arg.Ctime)
btime, _ := functions.TimeFromAny(ctx, scope, arg.Btime)

upload_response, err := uploader.Upload(
ctx, scope, arg.File,
Expand Down Expand Up @@ -211,14 +211,14 @@ func (self *UploadDirectoryFunction) Call(ctx context.Context,
}

// Stat only has a single time.
mtime, err := functions.TimeFromAny(scope, arg.Mtime)
mtime, err := functions.TimeFromAny(ctx, scope, arg.Mtime)
if err != nil {
mtime = stat.ModTime()
}

atime, _ := functions.TimeFromAny(scope, arg.Atime)
ctime, _ := functions.TimeFromAny(scope, arg.Ctime)
btime, _ := functions.TimeFromAny(scope, arg.Btime)
atime, _ := functions.TimeFromAny(ctx, scope, arg.Atime)
ctime, _ := functions.TimeFromAny(ctx, scope, arg.Ctime)
btime, _ := functions.TimeFromAny(ctx, scope, arg.Btime)

upload_response, err := uploader.Upload(
ctx, scope, arg.File,
Expand Down
2 changes: 1 addition & 1 deletion vql/protocols/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

func parseTime(ctx context.Context, scope types.Scope, args *ordereddict.Dict,
value interface{}) (interface{}, error) {
result, err := functions.TimeFromAny(scope, value)
result, err := functions.TimeFromAny(ctx, scope, value)
if err != nil {
return time.Time{}, nil
}
Expand Down
4 changes: 2 additions & 2 deletions vql/server/flows/monitoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (self MonitoringPlugin) Call(
}

if !utils.IsNil(arg.StartTime) {
start, err := functions.TimeFromAny(scope, arg.StartTime)
start, err := functions.TimeFromAny(ctx, scope, arg.StartTime)
if err == nil {
err = reader.SeekToTime(start)
if err != nil {
Expand All @@ -104,7 +104,7 @@ func (self MonitoringPlugin) Call(
}

if !utils.IsNil(arg.EndTime) {
end, err := functions.TimeFromAny(scope, arg.EndTime)
end, err := functions.TimeFromAny(ctx, scope, arg.EndTime)
if err == nil {
reader.SetMaxTime(end)
}
Expand Down
2 changes: 1 addition & 1 deletion vql/server/hunts/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (self *ScheduleHuntFunction) Call(ctx context.Context,

var expires uint64
if !utils.IsNil(arg.Expires) {
expiry_time, err := functions.TimeFromAny(scope, arg.Expires.Reduce(ctx))
expiry_time, err := functions.TimeFromAny(ctx, scope, arg.Expires.Reduce(ctx))
if err != nil {
scope.Log("hunt: expiry time invalid: %v", err)
return vfilter.Null{}
Expand Down
Loading

0 comments on commit 0f2e5a2

Please sign in to comment.