/
create.go
357 lines (327 loc) · 10.9 KB
/
create.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package investigation
import (
"github.com/coinbase/dexter/cli/cliutil"
"github.com/coinbase/dexter/engine"
"github.com/coinbase/dexter/engine/helpers"
"github.com/coinbase/dexter/facts"
"github.com/coinbase/dexter/tasks"
"github.com/c-bata/go-prompt"
"github.com/fatih/color"
"github.com/spf13/cobra"
"fmt"
"os"
"strconv"
)
var titleColor = color.New(color.FgHiGreen, color.Bold)
// A selectionWithArgs is a map from a task name to a set of arguments
type selectionWithArgs map[string][]string
// Map ordering is random, so to keep track of an ordered list
// of tasks, a map is created with integer keys. The selectionWithArgs
// values will themselves only ever have one key, as each index
// should only describe one task, but structuring things this
// way makes some operations easier.
type orderedSelectionWithArgs map[int]selectionWithArgs
func createInvestigation(cmd *cobra.Command, args []string) {
// Print an interesting welcome message
welcomeMessage := ` _ _
| | | |
__| | _____ _| |_ ___ _ __
/ _ |/ _ \ \/ / __/ _ \ '__|
| (_| | __/> <| || __/ |
\__,_|\___/_/\_\\__\___|_|
`
titleColor.Println(welcomeMessage)
// Create a new investigation struct, interacting with the user where required for each field
id := helpers.NewDexterID()
investigation := engine.Investigation{
ID: id,
TaskList: collectTasks(),
Scope: collectFacts(id),
KillContainers: cliutil.AskYesNo(color.HiCyanString("Terminate containers in scope after tasks complete?"), false),
KillHost: cliutil.AskYesNo(color.HiCyanString("Terminate hosts in scope after tasks compelte?"), false),
RecipientNames: cliutil.SelectFromList(engine.LoadInvestigatorNames(), "Which investigators should be able to access this report?", true, true),
Issuer: engine.Signature{Name: engine.LocalInvestigatorName()},
}
// Sign the investigation, prompting the user to decrypt their key
color.Yellow("The investigation will now be signed...")
investigation.Sign(helpers.LoadLocalKey(cliutil.CollectPassword))
// Upload the investigation to S3, reporting any errors
err := investigation.Upload()
if err != nil {
color.HiRed("error uploading investigation: " + err.Error())
os.Exit(1)
} else {
titleColor.Println("Investigation Uploaded!")
}
}
// Drop into a command line loop to collect tasks to include in this investigation
func collectTasks() selectionWithArgs {
color.HiCyan("Select tasks to run in this investigation, for more information try 'help'")
numberedTasks := orderedSelectionWithArgs{}
for {
task, args := collectTask()
switch task {
case "exit":
os.Exit(0)
case "help":
printTaskHelp()
case "ls":
listIncludedItems(numberedTasks)
case "rm":
removeItemsByNumber(numberedTasks, args)
case "":
if len(numberedTasks) == 0 {
color.HiRed("please select at least one task")
continue
} else {
return unorder(numberedTasks)
}
default:
numberedTasks = addNewTask(task, args, numberedTasks)
}
}
}
// Drop into a command line loop to collect tasks to include in this investigation
func collectFacts(salt string) selectionWithArgs {
color.HiCyan("Select facts to scope this investigation, for more information try 'help'")
numberedFacts := orderedSelectionWithArgs{}
for {
task, args := collectFact()
switch task {
case "exit":
os.Exit(0)
case "help":
printFactHelp()
case "ls":
listFacts(numberedFacts)
case "rm":
removeItemsByNumber(numberedFacts, args)
default:
if task == "" {
return unorder(numberedFacts)
}
numberedFacts = addNewFact(task, args, numberedFacts, salt)
}
}
}
// Add a new fact to the current ordered list, printing any errors that make the selection invalid
func addNewFact(task string, args []string, numberedFacts orderedSelectionWithArgs, salt string) orderedSelectionWithArgs {
if check, ok := facts.Facts[task]; ok {
if len(args) < check.MinimumArguments {
color.HiRed("not enough arguments, required: %d, provided: %d", check.MinimumArguments, len(args))
return numberedFacts
}
if dedupFail(task, args, unorder(numberedFacts)) {
color.HiRed("identical task and arguments already added")
return numberedFacts
}
if check.Private {
for i, arg := range args {
args[i] = facts.Hash(arg, salt)
}
}
numberedFacts[findSlot(numberedFacts)] = selectionWithArgs{
task: args,
}
color.Green("ADDED: " + helpers.StringWithArgs(task, args, check.Private))
} else {
if task == "" {
return numberedFacts
}
color.HiRed("unknown fact: %s", task)
}
return numberedFacts
}
// Add a new task to the current ordered list, printing any errors that make the selection invalid
func addNewTask(task string, args []string, numberedTasks orderedSelectionWithArgs) orderedSelectionWithArgs {
if t, ok := tasks.Tasks[task]; ok {
if len(args) < t.MinimumArguments {
color.HiRed("not enough arguments, required: %d, provided: %d", t.MinimumArguments, len(args))
return numberedTasks
}
if dedupFail(task, args, unorder(numberedTasks)) {
color.HiRed("identical task and arguments already added")
return numberedTasks
}
numberedTasks[findSlot(numberedTasks)] = selectionWithArgs{
task: args,
}
color.Green("ADDED: " + helpers.StringWithArgs(task, args, false))
} else {
if task == "" {
return numberedTasks
}
color.HiRed("unknown task: %s", task)
}
return numberedTasks
}
// List the tasks in order from lowest number to highest number
func listIncludedItems(numberedTasks orderedSelectionWithArgs) {
keys := getOrderedKeys(numberedTasks)
for _, num := range keys {
t := numberedTasks[num]
fmt.Print("[" + color.HiCyanString(strconv.Itoa(num)) + "]: ")
for _, str := range helpers.TaskStrings(t) {
color.HiYellow(str)
}
}
}
func listFacts(numberedTasks orderedSelectionWithArgs) {
keys := getOrderedKeys(numberedTasks)
for _, num := range keys {
t := numberedTasks[num]
fmt.Print("[" + color.HiCyanString(strconv.Itoa(num)) + "]: ")
for k, v := range t { // there will only be one of these
checker, ok := facts.Get(k)
if !ok {
color.HiRed("attempted to print fact that doesn't exist, should not be possible. Different versions of Dexter?")
} else {
color.HiYellow(helpers.StringWithArgs(k, v, checker.Private))
}
}
}
}
// Remove all the tasks specified by number
func removeItemsByNumber(numberedTasks orderedSelectionWithArgs, args []string) {
for _, numstr := range args {
num, err := strconv.Atoi(numstr)
if err != nil {
color.HiRed("bad selection, not an int")
continue
}
if _, ok := numberedTasks[num]; !ok {
color.HiRed("bad selection, task doesn't exist")
continue
}
strs := helpers.TaskStrings(numberedTasks[num])
if len(strs) != 1 {
delete(numberedTasks, num)
continue
}
color.Red("DELETED: " + strs[0])
delete(numberedTasks, num)
}
}
// Given the numbered tasks map, return the keys in numerical order
func getOrderedKeys(numberedTasks orderedSelectionWithArgs) []int {
ordered := []int{}
i := 0
for len(ordered) != len(numberedTasks) {
if _, ok := numberedTasks[i]; ok {
ordered = append(ordered, i)
}
i += 1
}
return ordered
}
// Check if a new task name and args already exists in a set of tasks
func dedupFail(newTask string, newArgs []string, set selectionWithArgs) bool {
for task, args := range set {
if task == newTask && argsEqual(args, newArgs) {
return true
}
}
return false
}
// Compare two string slices, respecting order
func argsEqual(args []string, cmp []string) bool {
if len(args) != len(cmp) {
return false
}
for i, val := range args {
if cmp[i] != val {
return false
}
}
return true
}
// Return the lowest value key available in the map
func findSlot(tasks orderedSelectionWithArgs) int {
i := 0
for {
if _, ok := tasks[i]; !ok {
return i
}
i += 1
}
}
// Take a map from int to task, and return just the tasks
func unorder(tasks orderedSelectionWithArgs) selectionWithArgs {
unordered := map[string][]string{}
for _, task := range tasks {
for k, v := range task {
unordered[k] = v
}
}
return unordered
}
// Print the command line and get user input back for task selection
func collectTask() (string, []string) {
selections := []prompt.Suggest{}
for k, v := range tasks.Tasks {
if k == "example-task" {
continue
}
selections = append(selections, prompt.Suggest{Text: k, Description: v.Description})
}
selections = append(selections, prompt.Suggest{Text: "help", Description: "print usage information for this promp"})
completer := func(d prompt.Document) []prompt.Suggest {
return prompt.FilterHasPrefix(selections, d.GetWordBeforeCursor(), true)
}
input := cliutil.SplitArguments(prompt.Input("task [done] > ", completer))
if len(input) < 1 {
return "", []string{}
}
return input[0], input[1:]
}
// Print the command line and get user input back for task selection
func collectFact() (string, []string) {
selections := []prompt.Suggest{}
for k, v := range facts.Facts {
if k == "example-fact" {
continue
}
selections = append(selections, prompt.Suggest{Text: k, Description: v.Description})
}
selections = append(selections, prompt.Suggest{Text: "help", Description: "print usage information for this promp"})
completer := func(d prompt.Document) []prompt.Suggest {
return prompt.FilterHasPrefix(selections, d.GetWordBeforeCursor(), true)
}
input := cliutil.SplitArguments(prompt.Input("fact [done] > ", completer))
if len(input) < 1 {
return "", []string{}
}
return input[0], input[1:]
}
// Print a help message for using the task selection CLI
func printTaskHelp() {
promptColor := color.New(color.FgWhite)
taskColor := color.New(color.FgHiYellow)
argColor := color.New(color.FgGreen)
color.White("\nDexter task selection:")
color.White("\nStart typing a task name, and supported tasks will be autocompleted.")
color.White("Add whitespace-separated arguments, if needed, like this:")
promptColor.Print("\n\ttask [done] > ")
taskColor.Print("my-task ")
argColor.Println("arg1 arg2 arg3")
color.White("\n'ls' will show all currently added tasks")
color.White("'rm' will let you remove a task from this investigation\n")
color.White("\nWhen you are done, enter an empty line to exit task selection")
color.White("'exit' will cancel this investigation")
}
// Print a help message for using the fact selection CLI
func printFactHelp() {
promptColor := color.New(color.FgWhite)
taskColor := color.New(color.FgHiYellow)
argColor := color.New(color.FgGreen)
color.White("\nDexter fact selection:")
color.White("\nStart typing a fact name, and supported facts will be autocompleted.")
color.White("Add whitespace-separated arguments, if needed, like this:")
promptColor.Print("\n\tfact [done] > ")
taskColor.Print("my-task ")
argColor.Println("arg1 arg2 arg3")
color.White("\n'ls' will show all currently added facts")
color.White("'rm' will let you remove a fact from this investigation\n")
color.White("\nWhen you are done, enter an empty line to exit factc selection")
color.White("'exit' will cancel this investigation")
}