From 02edb7d9680489a6986a2ef13067364a834a69f6 Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Mon, 27 Mar 2023 12:26:31 +1000 Subject: [PATCH] Bugfix: Flow Deletion did not remove uploaded bulk files. (#2589) (#2590) Also * User GUI missing scrollbar for many users. * Added the ability to filter hunts by username. --- api/proto/hunts.pb.go | 94 +- api/proto/hunts.proto | 2 + .../Admin/System/CompressUploads.yaml | 41 - .../definitions/Server/Utils/DeleteFlow.yaml | 2 +- .../src/components/hunts/hunt-list.jsx | 974 +++++++++--------- .../src/components/hunts/hunts.jsx | 11 +- .../src/components/users/user-inspector.jsx | 14 +- .../src/components/users/user.css | 12 + gui/velociraptor/src/index.jsx | 6 +- services/hunt_dispatcher/list.go | 5 + services/launcher/delete.go | 10 + .../flows/fixtures/TestEnumerateFlow.golden | 7 + 12 files changed, 614 insertions(+), 564 deletions(-) delete mode 100644 artifacts/definitions/Admin/System/CompressUploads.yaml diff --git a/api/proto/hunts.pb.go b/api/proto/hunts.pb.go index 6e2e2769e79..fa658ad890e 100644 --- a/api/proto/hunts.pb.go +++ b/api/proto/hunts.pb.go @@ -620,8 +620,9 @@ type ListHuntsRequest struct { Offset uint64 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"` Count uint64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` // If specified we return a partial structure. - Summary bool `protobuf:"varint,4,opt,name=summary,proto3" json:"summary,omitempty"` - IncludeArchived bool `protobuf:"varint,3,opt,name=include_archived,json=includeArchived,proto3" json:"include_archived,omitempty"` + Summary bool `protobuf:"varint,4,opt,name=summary,proto3" json:"summary,omitempty"` + IncludeArchived bool `protobuf:"varint,3,opt,name=include_archived,json=includeArchived,proto3" json:"include_archived,omitempty"` + UserFilter string `protobuf:"bytes,5,opt,name=user_filter,json=userFilter,proto3" json:"user_filter,omitempty"` } func (x *ListHuntsRequest) Reset() { @@ -684,6 +685,13 @@ func (x *ListHuntsRequest) GetIncludeArchived() bool { return false } +func (x *ListHuntsRequest) GetUserFilter() string { + if x != nil { + return x.UserFilter + } + return "" +} + type ListHuntsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1177,7 +1185,7 @@ var file_hunts_proto_rawDesc = []byte{ 0x65, 0x12, 0x32, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x85, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xa6, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, @@ -1185,45 +1193,47 @@ var file_hunts_proto_rawDesc = []byte{ 0x61, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, - 0x63, 0x6c, 0x75, 0x64, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x22, 0x36, 0x0a, - 0x11, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x21, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x05, - 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x29, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, - 0x22, 0x7a, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, - 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, - 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, - 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x46, 0x0a, 0x0e, - 0x46, 0x6c, 0x6f, 0x77, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1b, - 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x66, - 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6c, - 0x6f, 0x77, 0x49, 0x64, 0x22, 0xf0, 0x01, 0x0a, 0x0c, 0x48, 0x75, 0x6e, 0x74, 0x4d, 0x75, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x26, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x48, 0x75, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, - 0x12, 0x35, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x6c, 0x6f, - 0x77, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x61, 0x73, 0x73, - 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, - 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c, - 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, 0x72, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x36, + 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x52, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x29, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, + 0x64, 0x22, 0x7a, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x48, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, + 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x46, 0x0a, + 0x0e, 0x46, 0x6c, 0x6f, 0x77, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, + 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, + 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, + 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x22, 0xf0, 0x01, 0x0a, 0x0c, 0x48, 0x75, 0x6e, 0x74, 0x4d, 0x75, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, + 0x26, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x48, 0x75, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x35, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x6c, + 0x6f, 0x77, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x61, 0x73, + 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x31, 0x5a, 0x2f, 0x77, 0x77, 0x77, 0x2e, + 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x64, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, + 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x72, 0x61, 0x70, 0x74, 0x6f, + 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/api/proto/hunts.proto b/api/proto/hunts.proto index 9606713ac2a..b21cb7f9a62 100644 --- a/api/proto/hunts.proto +++ b/api/proto/hunts.proto @@ -159,6 +159,8 @@ message ListHuntsRequest { // If specified we return a partial structure. bool summary = 4; bool include_archived = 3; + + string user_filter = 5; } message ListHuntsResponse { diff --git a/artifacts/definitions/Admin/System/CompressUploads.yaml b/artifacts/definitions/Admin/System/CompressUploads.yaml deleted file mode 100644 index cadb4583094..00000000000 --- a/artifacts/definitions/Admin/System/CompressUploads.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: Admin.System.CompressUploads -description: | - Compresses all uploaded files. - - When artifacts collect files they are normally stored on the server - uncompressed. This artifact watches all completed flows and - compresses the files in the file store when the flow completes. This - is very useful for cloud based deployments with limited storage - space or when collecting large files. - - In order to run this artifact you would normally run it as part of - an artifact acquisition process: - - ``` - $ velociraptor --config /etc/server.config.yaml artifacts acquire Admin.System.CompressUploads - ``` - - Note that there is nothing special about compressed files - you can - also just run `find` and `gzip` in the file store. Velociraptor will - automatically decompress the file when displaying it in the GUI - text/hexdump etc. - -type: SERVER_EVENT - -parameters: - - name: blacklistCompressionFilename - type: regex - description: Filenames which match this regex will be excluded from compression. - default: 'ntuser.dat$' - -sources: - - query: | - LET files = SELECT ClientId, - Flow.session_id as Flow, - Flow.uploaded_files as Files - FROM watch_monitoring(artifact='System.Flow.Completion') - WHERE Files and not Files =~ blacklistCompressionFilename - - SELECT ClientId, Flow, Files, - compress(path=Files) as CompressedFiles - FROM files diff --git a/artifacts/definitions/Server/Utils/DeleteFlow.yaml b/artifacts/definitions/Server/Utils/DeleteFlow.yaml index 848d56dbc16..74a5e4e346f 100644 --- a/artifacts/definitions/Server/Utils/DeleteFlow.yaml +++ b/artifacts/definitions/Server/Utils/DeleteFlow.yaml @@ -25,5 +25,5 @@ parameters: sources: - query: | - SELECT Type, VFSPath + SELECT Type, Data.VFSPath AS VFSPath, Error FROM delete_flow(flow_id=FlowId, client_id=ClientId, really_do_it=ReallyDoIt) diff --git a/gui/velociraptor/src/components/hunts/hunt-list.jsx b/gui/velociraptor/src/components/hunts/hunt-list.jsx index 7366ec06666..87dc8eba767 100644 --- a/gui/velociraptor/src/components/hunts/hunt-list.jsx +++ b/gui/velociraptor/src/components/hunts/hunt-list.jsx @@ -19,486 +19,520 @@ import NewHuntWizard from './new-hunt.jsx'; import DeleteNotebookDialog from '../notebooks/notebook-delete.jsx'; import ExportNotebook from '../notebooks/export-notebook.jsx'; import T from '../i8n/i8n.jsx'; +import UserConfig from '../core/user.jsx'; import api from '../core/api-service.jsx'; import axios from 'axios'; class HuntList extends React.Component { - static propTypes = { - selected_hunt: PropTypes.object, - - // Contain a list of hunt metadata objects - each summary of - // the hunt. - hunts: PropTypes.array, - setSelectedHunt: PropTypes.func, - updateHunts: PropTypes.func, - }; - - componentDidMount = () => { - this.source = axios.CancelToken.source(); - - let action = this.props.match && this.props.match.params && - this.props.match.params.hunt_id; - if (action === "new") { - let name = this.props.match && this.props.match.params && - this.props.match.params.tab; - - this.setState({ - showCopyWizard: true, - full_selected_hunt: { - start_request: { - artifacts: [name], - }, - }, - }); - this.props.history.push("/hunts"); - } - } - - componentWillUnmount() { - this.source.cancel(); - } - - state = { - showWizard: false, - showRunHuntDialog: false, - showArchiveHuntDialog: false, - showDeleteHuntDialog: false, - showExportNotebook: false, - showDeleteNotebook: false, - showCopyWizard: false, - showNotebookUploadsDialog: false, - } - - // Launch the hunt. - setCollectionRequest = (request) => { - api.post('v1/CreateHunt', request, this.source.token).then((response) => { - // Keep the wizard up until the server confirms the - // creation worked. - this.setState({ - showWizard: false, - showCopyWizard: false - }); - - // Refresh the hunts list when the creation is done. - this.props.updateHunts(); - }); - } - - startHunt = () => { - let hunt_id = this.props.selected_hunt && - this.props.selected_hunt.hunt_id; - - if (!hunt_id) { return; }; - - api.post("v1/ModifyHunt", { - state: "RUNNING", - hunt_id: hunt_id, - }, this.source.token).then((response) => { - this.props.updateHunts(); - this.setState({ showRunHuntDialog: false }); - }); - } - - stopHunt = () => { - let hunt_id = this.props.selected_hunt && - this.props.selected_hunt.hunt_id; - - if (!hunt_id) { return; }; - - api.post("v1/ModifyHunt", { - state: "PAUSED", - hunt_id: hunt_id, - }, this.source.token).then((response) => { - this.props.updateHunts(); - - // Start Cancelling all in flight collections in the - // background. - api.post("v1/CollectArtifact", { - client_id: "server", - artifacts: ["Server.Utils.CancelHunt"], - specs: [{ - artifact: "Server.Utils.CancelHunt", - parameters: { - env: [ - { key: "HuntId", value: hunt_id }, - ] - } - }], - }, this.source.token); - }); - } - - archiveHunt = () => { - let hunt_id = this.props.selected_hunt && - this.props.selected_hunt.hunt_id; - - if (!hunt_id) { return; }; - - api.post("v1/ModifyHunt", { - state: "ARCHIVED", - hunt_id: hunt_id, - }, this.source.token).then((response) => { - this.props.updateHunts(); - this.setState({ showArchiveHuntDialog: false }); - }); - } - - updateHunt = (row) => { - let hunt_id = row.hunt_id; - - if (!hunt_id) { return; }; - - api.post("v1/ModifyHunt", { - hunt_description: row.hunt_description || " ", - hunt_id: hunt_id, - }, this.source.token).then((response) => { - this.props.updateHunts(); - }); - } - - deleteHunt = () => { - let hunt_id = this.props.selected_hunt && - this.props.selected_hunt.hunt_id; - - if (!hunt_id) { return; }; - - // First stop the hunt then delete all the files. - api.post("v1/ModifyHunt", { - state: "ARCHIVED", - hunt_id: hunt_id, - }, this.source.token).then((response) => { - this.props.updateHunts(); - this.setState({ showDeleteHuntDialog: false }); - - // Start delete collections in the background. It may take - // a while. - api.post("v1/CollectArtifact", { - client_id: "server", - artifacts: ["Server.Hunts.CancelAndDelete"], - specs: [{ - artifact: "Server.Hunts.CancelAndDelete", - parameters: { - env: [ - { key: "HuntId", value: hunt_id }, - { key: "DeleteAllFiles", value: "Y" }, - ] - } - }], - }, this.source.token); - }); - } - - setFullScreen = () => { - if (this.props.selected_hunt) { - this.props.history.push( - "/fullscreen/hunts/" + - this.props.selected_hunt.hunt_id + "/notebook"); - } - } - - // this.props.selected_hunt is only a hunt summary so we need to - // fetch the full hunt so we can copy it. - copyHunt = () => { - let hunt_id = this.props.selected_hunt && - this.props.selected_hunt.hunt_id; - - if (!hunt_id || hunt_id[0] !== "H") { - return; - } - - api.get("v1/GetHunt", { - hunt_id: hunt_id, - }, this.source.token).then((response) => { - if (response.cancel) return; - - if (!_.isEmpty(response.data)) { - this.setState({ - full_selected_hunt: response.data, - showCopyWizard: true, - }); - } - }); - } - - render() { - let columns = getHuntColumns(); - let selected_hunt = this.props.selected_hunt && this.props.selected_hunt.hunt_id; - const selectRow = { - mode: "radio", - clickToSelect: true, - clickToEdit: true, - hideSelectColumn: true, - classes: "row-selected", - onSelect: (row) => { - this.props.setSelectedHunt(row); - }, - selected: [selected_hunt], - }; - - let state = this.props.selected_hunt && this.props.selected_hunt.state; - if (this.props.selected_hunt.stats && this.props.selected_hunt.stats.stopped) { - state = 'STOPPED'; - } - - let tab = this.props.match && this.props.match.params && this.props.match.params.tab; - return ( - <> - {this.state.showWizard && - this.setState({ showWizard: false })} - onResolve={this.setCollectionRequest} - /> - } - {this.state.showCopyWizard && - this.setState({ showCopyWizard: false })} - onResolve={this.setCollectionRequest} - /> - } - - {this.state.showRunHuntDialog && - this.setState({ showRunHuntDialog: false })} > - - {T("Run this hunt?")} - - - -

{T("Are you sure you want to run this hunt?")}

-
- - - - - -
- } - {this.state.showDeleteNotebook && - { - this.setState({ showDeleteNotebook: false }); - this.props.updateHunts(); - }} /> - } - - {this.state.showNotebookUploadsDialog && - this.setState({ showNotebookUploadsDialog: false })} - /> - } - - {this.state.showExportNotebook && - this.setState({ showExportNotebook: false })} /> - } - - {this.state.showDeleteHuntDialog && - this.setState({ showDeleteHuntDialog: false })} > - - {T("Permanently delete this hunt?")} - - - - {T("DeleteHuntDialog")} - - - - - - - - } - - - - - - - - - - {tab === "notebook" && - - - - - - - - - - - - } - - -
- { - this.updateHunt(row); - }, - blurToSave: true, - })} - /> - {_.isEmpty(this.props.hunts) && -
{T("No hunts exist in the system. You can start a new hunt by clicking the New Hunt button above.")}
} -
- - ); - } + static contextType = UserConfig; + + static propTypes = { + selected_hunt: PropTypes.object, + + // Contain a list of hunt metadata objects - each summary of + // the hunt. + hunts: PropTypes.array, + setSelectedHunt: PropTypes.func, + updateHunts: PropTypes.func, + }; + + componentDidMount = () => { + this.source = axios.CancelToken.source(); + + let action = this.props.match && this.props.match.params && + this.props.match.params.hunt_id; + if (action === "new") { + let name = this.props.match && this.props.match.params && + this.props.match.params.tab; + + this.setState({ + showCopyWizard: true, + full_selected_hunt: { + start_request: { + artifacts: [name], + }, + }, + }); + this.props.history.push("/hunts"); + } + } + + componentWillUnmount() { + this.source.cancel(); + } + + state = { + showWizard: false, + showRunHuntDialog: false, + showArchiveHuntDialog: false, + showDeleteHuntDialog: false, + showExportNotebook: false, + showDeleteNotebook: false, + showCopyWizard: false, + showNotebookUploadsDialog: false, + + filter: "", + } + + // Launch the hunt. + setCollectionRequest = (request) => { + api.post('v1/CreateHunt', request, this.source.token).then((response) => { + // Keep the wizard up until the server confirms the + // creation worked. + this.setState({ + showWizard: false, + showCopyWizard: false + }); + + // Refresh the hunts list when the creation is done. + this.props.updateHunts(this.state.filter); + }); + } + + startHunt = () => { + let hunt_id = this.props.selected_hunt && + this.props.selected_hunt.hunt_id; + + if (!hunt_id) { return; }; + + api.post("v1/ModifyHunt", { + state: "RUNNING", + hunt_id: hunt_id, + }, this.source.token).then((response) => { + this.props.updateHunts(); + this.setState({ showRunHuntDialog: false }); + }); + } + + stopHunt = () => { + let hunt_id = this.props.selected_hunt && + this.props.selected_hunt.hunt_id; + + if (!hunt_id) { return; }; + + api.post("v1/ModifyHunt", { + state: "PAUSED", + hunt_id: hunt_id, + }, this.source.token).then((response) => { + this.props.updateHunts(); + + // Start Cancelling all in flight collections in the + // background. + api.post("v1/CollectArtifact", { + client_id: "server", + artifacts: ["Server.Utils.CancelHunt"], + specs: [{ + artifact: "Server.Utils.CancelHunt", + parameters: { + env: [ + { key: "HuntId", value: hunt_id }, + ] + } + }], + }, this.source.token); + }); + } + + archiveHunt = () => { + let hunt_id = this.props.selected_hunt && + this.props.selected_hunt.hunt_id; + + if (!hunt_id) { return; }; + + api.post("v1/ModifyHunt", { + state: "ARCHIVED", + hunt_id: hunt_id, + }, this.source.token).then((response) => { + this.props.updateHunts(); + this.setState({ showArchiveHuntDialog: false }); + }); + } + + updateHunt = (row) => { + let hunt_id = row.hunt_id; + + if (!hunt_id) { return; }; + + api.post("v1/ModifyHunt", { + hunt_description: row.hunt_description || " ", + hunt_id: hunt_id, + }, this.source.token).then((response) => { + this.props.updateHunts(); + }); + } + + deleteHunt = () => { + let hunt_id = this.props.selected_hunt && + this.props.selected_hunt.hunt_id; + + if (!hunt_id) { return; }; + + // First stop the hunt then delete all the files. + api.post("v1/ModifyHunt", { + state: "ARCHIVED", + hunt_id: hunt_id, + }, this.source.token).then((response) => { + this.props.updateHunts(); + this.setState({ showDeleteHuntDialog: false }); + + // Start delete collections in the background. It may take + // a while. + api.post("v1/CollectArtifact", { + client_id: "server", + artifacts: ["Server.Hunts.CancelAndDelete"], + specs: [{ + artifact: "Server.Hunts.CancelAndDelete", + parameters: { + env: [ + { key: "HuntId", value: hunt_id }, + { key: "DeleteAllFiles", value: "Y" }, + ] + } + }], + }, this.source.token); + }); + } + + setFullScreen = () => { + if (this.props.selected_hunt) { + this.props.history.push( + "/fullscreen/hunts/" + + this.props.selected_hunt.hunt_id + "/notebook"); + } + } + + // this.props.selected_hunt is only a hunt summary so we need to + // fetch the full hunt so we can copy it. + copyHunt = () => { + let hunt_id = this.props.selected_hunt && + this.props.selected_hunt.hunt_id; + + if (!hunt_id || hunt_id[0] !== "H") { + return; + } + + api.get("v1/GetHunt", { + hunt_id: hunt_id, + }, this.source.token).then((response) => { + if (response.cancel) return; + + if (!_.isEmpty(response.data)) { + this.setState({ + full_selected_hunt: response.data, + showCopyWizard: true, + }); + } + }); + } + + render() { + let columns = getHuntColumns(); + let selected_hunt = this.props.selected_hunt && + this.props.selected_hunt.hunt_id; + let username = this.context && + this.context.traits && this.context.traits.username; + const selectRow = { + mode: "radio", + clickToSelect: true, + clickToEdit: true, + hideSelectColumn: true, + classes: "row-selected", + onSelect: (row) => { + this.props.setSelectedHunt(row); + }, + selected: [selected_hunt], + }; + + let state = this.props.selected_hunt && this.props.selected_hunt.state; + if (this.props.selected_hunt.stats && this.props.selected_hunt.stats.stopped) { + state = 'STOPPED'; + } + + let tab = this.props.match && this.props.match.params && this.props.match.params.tab; + return ( + <> + {this.state.showWizard && + this.setState({ showWizard: false })} + onResolve={this.setCollectionRequest} + /> + } + {this.state.showCopyWizard && + this.setState({ showCopyWizard: false })} + onResolve={this.setCollectionRequest} + /> + } + + {this.state.showRunHuntDialog && + this.setState({ showRunHuntDialog: false })} > + + {T("Run this hunt?")} + + + +

{T("Are you sure you want to run this hunt?")}

+
+ + + + + +
+ } + {this.state.showDeleteNotebook && + { + this.setState({ showDeleteNotebook: false }); + this.props.updateHunts(this.state.filter); + }} /> + } + + {this.state.showNotebookUploadsDialog && + this.setState({ showNotebookUploadsDialog: false })} + /> + } + + {this.state.showExportNotebook && + this.setState({ showExportNotebook: false })} /> + } + + {this.state.showDeleteHuntDialog && + this.setState({ showDeleteHuntDialog: false })} > + + {T("Permanently delete this hunt?")} + + + + {T("DeleteHuntDialog")} + + + + + + + + } + + + + + + + + + { !this.state.filter ? + + : + + } + + {tab === "notebook" && + + + + + + + + + + + + } + + +
+ { + this.updateHunt(row); + }, + blurToSave: true, + })} + /> + {_.isEmpty(this.props.hunts) && +
{T("No hunts exist in the system. You can start a new hunt by clicking the New Hunt button above.")}
} +
+ + ); + } }; export default withRouter(HuntList); export function getHuntColumns() { - return formatColumns([ - { - dataField: "state", text: T("State"), - formatter: (cell, row) => { - let stopped = row.stats && row.stats.stopped; - - if (stopped || cell === "STOPPED") { - return
-
; - } - if (cell === "RUNNING") { - return
-
; - } - if (cell === "PAUSED") { - return
-
; - } - return
-
; - } - }, - { dataField: "hunt_id", text: T("Hunt ID") }, - { - dataField: "hunt_description", text: T("Description"), - sort: true, filtered: true, editable: true - }, - { - dataField: "create_time", text: T("Created"), - type: "timestamp", sort: true - }, - { - dataField: "start_time", text: T("Started"), - type: "timestamp", sort: true - }, - { - dataField: "expires", - text: T("Expires"), sort: true, - type: "timestamp" - }, - { dataField: "stats.total_clients_scheduled", text: T("Scheduled") }, - { dataField: "creator", text: T("Creator") }, - ]); + return formatColumns([ + { + dataField: "state", text: T("State"), + formatter: (cell, row) => { + let stopped = row.stats && row.stats.stopped; + + if (stopped || cell === "STOPPED") { + return
+
; + } + if (cell === "RUNNING") { + return
+
; + } + if (cell === "PAUSED") { + return
+
; + } + return
+
; + } + }, + { dataField: "hunt_id", text: T("Hunt ID") }, + { + dataField: "hunt_description", text: T("Description"), + sort: true, filtered: true, editable: true + }, + { + dataField: "create_time", text: T("Created"), + type: "timestamp", sort: true + }, + { + dataField: "start_time", text: T("Started"), + type: "timestamp", sort: true + }, + { + dataField: "expires", + text: T("Expires"), sort: true, + type: "timestamp" + }, + { dataField: "stats.total_clients_scheduled", text: T("Scheduled") }, + { dataField: "creator", text: T("Creator") }, + ]); } diff --git a/gui/velociraptor/src/components/hunts/hunts.jsx b/gui/velociraptor/src/components/hunts/hunts.jsx index 8bf212b474f..a3bda666cf3 100644 --- a/gui/velociraptor/src/components/hunts/hunts.jsx +++ b/gui/velociraptor/src/components/hunts/hunts.jsx @@ -29,6 +29,8 @@ class VeloHunts extends React.Component { hunts: [], loading: true, + + filter: "", } componentDidMount = () => { @@ -76,7 +78,13 @@ class VeloHunts extends React.Component { }); } - fetchHunts = () => { + fetchHunts = (filter) => { + if (!_.isUndefined(filter) && filter !== this.state.filter) { + this.setState({filter: filter}); + } else { + filter = this.state.filter; + } + let selected_hunt_id = this.props.match && this.props.match.params && this.props.match.params.hunt_id; @@ -89,6 +97,7 @@ class VeloHunts extends React.Component { count: 2000, offset: 0, summary: true, + user_filter: filter, }, this.list_hunts_source.token).then((response) => { if (response.cancel) return; diff --git a/gui/velociraptor/src/components/users/user-inspector.jsx b/gui/velociraptor/src/components/users/user-inspector.jsx index 6794447a51f..d49f983f2f3 100644 --- a/gui/velociraptor/src/components/users/user-inspector.jsx +++ b/gui/velociraptor/src/components/users/user-inspector.jsx @@ -368,7 +368,7 @@ class UsersOverview extends Component { - + @@ -386,7 +386,7 @@ class UsersOverview extends Component { variant="outline-default" as="button"> - {T("Update User Password")} + {T("Update User Password")} } @@ -423,7 +423,7 @@ class UsersOverview extends Component { - +
@@ -440,7 +440,7 @@ class UsersOverview extends Component { variant="outline-default" as="button"> - {T("Assign user to Orgs")} + {T("Assign user to Orgs")} @@ -504,7 +504,7 @@ class OrgsOverview extends UsersOverview { return ( - +
@@ -535,7 +535,7 @@ class OrgsOverview extends UsersOverview { - +
{T("Orgs")}
diff --git a/gui/velociraptor/src/components/users/user.css b/gui/velociraptor/src/components/users/user.css index e2819549d74..3095f42c63a 100644 --- a/gui/velociraptor/src/components/users/user.css +++ b/gui/velociraptor/src/components/users/user.css @@ -48,3 +48,15 @@ padding-right: 20px; display: inline; } + +.user-list { + max-height: calc(100vh - 180px); + overflow-y: auto; + overflow-x: hidden; +} + +.org-list { + max-height: calc(100vh - 180px); + overflow-y: auto; + overflow-x: hidden; +} diff --git a/gui/velociraptor/src/index.jsx b/gui/velociraptor/src/index.jsx index 23a1fc6a0c6..eb72d7e4a8f 100644 --- a/gui/velociraptor/src/index.jsx +++ b/gui/velociraptor/src/index.jsx @@ -22,7 +22,8 @@ import { faHome, faCrosshairs, faWrench, faEye, faServer, faBook, faLaptop, faCompressAlt, faBackward, faMedkit, faVirusSlash, faBookmark, faHeart, faFileCode, faFlag, faTrashAlt, faClock, faLock, faLockOpen, faCloud, faCloudDownloadAlt, faUserEdit, faFilter, faSortAlphaUp, faSortAlphaDown, - faInfo, faBug, faUser, faList, faIndent, faTextHeight, faBars + faInfo, faBug, faUser, faList, faIndent, faTextHeight, faBars, + faUserLargeSlash } from '@fortawesome/free-solid-svg-icons'; library.add(faHome, faCrosshairs, faWrench, faEye, faServer, faBook, faLaptop, @@ -37,7 +38,8 @@ library.add(faHome, faCrosshairs, faWrench, faEye, faServer, faBook, faLaptop, faForward, faCalendarAlt, faCompressAlt, faBackward, faMedkit, faVirusSlash, faBookmark, faHeart, faFileCode, faFlag, faTrashAlt, faClock, faLock, faLockOpen, faCloud, faCloudDownloadAlt, faUserEdit, faFilter, faBug, - faSortAlphaUp, faSortAlphaDown, faInfo, faUser, faList, faIndent, faTextHeight, faBars + faSortAlphaUp, faSortAlphaDown, faInfo, faUser, faList, faIndent, + faTextHeight, faBars, faUserLargeSlash ); ReactDOM.render( diff --git a/services/hunt_dispatcher/list.go b/services/hunt_dispatcher/list.go index ab82d9618bc..f89b3550269 100644 --- a/services/hunt_dispatcher/list.go +++ b/services/hunt_dispatcher/list.go @@ -61,6 +61,11 @@ func (self *HuntDispatcher) ListHunts( items := make([]*api_proto.Hunt, 0, end) err = dispatcher.ApplyFuncOnHunts( func(hunt *api_proto.Hunt) error { + if in.UserFilter != "" && + in.UserFilter != hunt.Creator { + return nil + } + // Only show non-archived hunts. if in.IncludeArchived || hunt.State != api_proto.Hunt_ARCHIVED { diff --git a/services/launcher/delete.go b/services/launcher/delete.go index 7b7d3dfb1ef..364e59e6d0b 100644 --- a/services/launcher/delete.go +++ b/services/launcher/delete.go @@ -50,6 +50,14 @@ func (self *Launcher) DeleteFlow( file_store_factory, flow_path_manager.UploadMetadata()) if err == nil { for row := range reader.Rows(ctx) { + components, pres := row.GetStrings("_Components") + if pres { + pathspec := path_specs.NewUnsafeFilestorePath( + components...).SetType(api.PATH_TYPE_FILESTORE_ANY) + r.emit_fs("Upload", pathspec) + continue + } + upload, pres := row.GetString("vfs_path") if pres { // Each row is the full filestore path of the upload. @@ -60,6 +68,7 @@ func (self *Launcher) DeleteFlow( r.emit_fs("Upload", pathspec) } } + reader.Close() } // Order results to facilitate deletion - container deletion @@ -91,6 +100,7 @@ func (self *Launcher) DeleteFlow( SetType(api.PATH_TYPE_FILESTORE_JSON_INDEX)) r.emit_ds("CollectionContext", flow_path_manager.Path()) r.emit_ds("Task", flow_path_manager.Task()) + r.emit_ds("Stats", flow_path_manager.Stats()) // Walk the flow's datastore and filestore db, err := datastore.GetDB(config_obj) diff --git a/vql/server/flows/fixtures/TestEnumerateFlow.golden b/vql/server/flows/fixtures/TestEnumerateFlow.golden index 3334a4f9b4a..e9cc607d0c5 100644 --- a/vql/server/flows/fixtures/TestEnumerateFlow.golden +++ b/vql/server/flows/fixtures/TestEnumerateFlow.golden @@ -41,6 +41,13 @@ }, "error": "" }, + { + "type": "Stats", + "data": { + "VFSPath": "ds:/clients/C.123/collections/F.1234/stats.json.db" + }, + "error": "" + }, { "type": "Notebook", "data": {
{T("Users")}