// actionReader retrieves an action from the API using its numerical ID // and enters prompt mode to analyze it func actionReader(input string, cli client.Client) (err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("actionReader() -> %v", e) } }() inputArr := strings.Split(input, " ") if len(inputArr) < 2 { panic("wrong order format. must be 'action <actionid>'") } aid, err := strconv.ParseFloat(inputArr[1], 64) if err != nil { panic(err) } a, _, err := cli.GetAction(aid) if err != nil { panic(err) } investigators := investigatorsStringFromAction(a.Investigators, 80) fmt.Println("Entering action reader mode. Type \x1b[32;1mexit\x1b[0m or press \x1b[32;1mctrl+d\x1b[0m to leave. \x1b[32;1mhelp\x1b[0m may help.") fmt.Printf("Action: '%s'.\nLaunched by '%s' on '%s'.\nStatus '%s'.\n", a.Name, investigators, a.StartTime, a.Status) a.PrintCounters() prompt := fmt.Sprintf("\x1b[31;1maction %d>\x1b[0m ", uint64(aid)%1000) for { // completion var symbols = []string{"command", "copy", "counters", "details", "exit", "grep", "help", "investigators", "json", "list", "all", "found", "notfound", "pretty", "r", "results", "times"} readline.Completer = func(query, ctx string) []string { var res []string for _, sym := range symbols { if strings.HasPrefix(sym, query) { res = append(res, sym) } } return res } input, err := readline.String(prompt) if err == io.EOF { break } if err != nil { fmt.Println("error: ", err) break } orders := strings.Split(strings.TrimSpace(input), " ") switch orders[0] { case "command": err = commandReader(input, cli) if err != nil { panic(err) } case "copy": err = actionLauncher(a, cli) if err != nil { panic(err) } goto exit case "counters": a, _, err = cli.GetAction(aid) if err != nil { panic(err) } a.PrintCounters() case "details": actionPrintDetails(a) case "exit": fmt.Printf("exit\n") goto exit case "help": fmt.Printf(`The following orders are available: command <id> jump to command reader mode for command <id> copy enter action launcher mode using current action as template counters display the counters of the action details display the details of the action, including status & times exit exit this mode (also works with ctrl+d) help show this help investigators print the list of investigators that signed the action json show the json of the action list <show> returns the list of commands with their status <show>: * set to "all" to get all results (default) * set to "found" to only display positive results * set to "notfound" for negative results list can be followed by a 'filter' pipe: ex: ls | grep server1.(dom1|dom2) | grep -v example.net r refresh the action (get latest version from upstream) results <show> <render> display results of all commands <show>: * set to "all" to get all results (default) * set to "found" to only display positive results * set to "notfound" for negative results <render>: * set to "text" to print results in console (default) * set to "map" to generate an open a google map times show the various timestamps of the action `) case "investigators": for _, i := range a.Investigators { fmt.Println(i.Name, "- Key ID:", i.PGPFingerprint) } case "json": var ajson []byte ajson, err = json.MarshalIndent(a, "", " ") if err != nil { panic(err) } fmt.Printf("%s\n", ajson) case "list": err = actionPrintList(aid, orders, cli) if err != nil { panic(err) } case "r": a, _, err = cli.GetAction(aid) if err != nil { panic(err) } fmt.Println("reloaded") case "results": show := "all" if len(orders) > 1 { switch orders[1] { case "all", "found", "notfound": show = orders[1] default: panic("invalid show '" + orders[2] + "'") } } render := "text" if len(orders) > 2 { switch orders[2] { case "map", "text": render = orders[2] default: panic("invalid rendering '" + orders[2] + "'") } } err = cli.PrintActionResults(a, show, render) if err != nil { panic(err) } case "times": fmt.Printf("Valid from '%s' until '%s'\nStarted on '%s'\n"+ "Last updated '%s'\nFinished on '%s'\n", a.ValidFrom, a.ExpireAfter, a.StartTime, a.LastUpdateTime, a.FinishTime) case "": break default: fmt.Printf("Unknown order '%s'. You are in action reader mode. Try `help`.\n", orders[0]) } readline.AddHistory(input) } exit: fmt.Printf("\n") return }
func main() { var ( conf client.Configuration cli client.Client err error op mig.Operation a mig.Action migrc, show, render, target, expiration, afile string modargs []string modRunner interface{} ) defer func() { if e := recover(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) } }() homedir := client.FindHomedir() fs := flag.NewFlagSet("mig flag", flag.ContinueOnError) fs.Usage = continueOnFlagError fs.StringVar(&migrc, "c", homedir+"/.migrc", "alternative configuration file") fs.StringVar(&show, "show", "found", "type of results to show") fs.StringVar(&render, "render", "text", "results rendering mode") fs.StringVar(&target, "t", fmt.Sprintf("status='%s' AND mode='daemon'", mig.AgtStatusOnline), "action target") fs.StringVar(&expiration, "e", "300s", "expiration") fs.StringVar(&afile, "i", "/path/to/file", "Load action from file") // if first argument is missing, or is help, print help // otherwise, pass the remainder of the arguments to the module for parsing // this client is agnostic to module parameters if len(os.Args) < 2 || os.Args[1] == "help" || os.Args[1] == "-h" || os.Args[1] == "--help" { usage() } if len(os.Args) < 2 || os.Args[1] == "-V" { fmt.Println(version) os.Exit(0) } // when reading the action from a file, go directly to launch if os.Args[1] == "-i" { err = fs.Parse(os.Args[1:]) if err != nil { panic(err) } if afile == "/path/to/file" { panic("-i flag must take an action file path as argument") } a, err = mig.ActionFromFile(afile) if err != nil { panic(err) } fmt.Fprintf(os.Stderr, "[info] launching action from file, all flags are ignored\n") goto readytolaunch } // arguments parsing works as follow: // * os.Args[1] must contain the name of the module to launch. we first verify // that a module exist for this name and then continue parsing // * os.Args[2:] contains both global options and module parameters. We parse the // whole []string to extract global options, and module parameters will be left // unparsed in fs.Args() // * fs.Args() with the module parameters is passed as a string to the module parser // which will return a module operation to store in the action op.Module = os.Args[1] if _, ok := modules.Available[op.Module]; !ok { panic("Unknown module " + op.Module) } // -- Ugly hack Warning -- // Parse() will fail on the first flag that is not defined, but in our case module flags // are defined in the module packages and not in this program. Therefore, the flag parse error // is expected. Unfortunately, Parse() writes directly to stderr and displays the error to // the user, which confuses them. The right fix would be to prevent Parse() from writing to // stderr, since that's really the job of the calling program, but in the meantime we work around // it by redirecting stderr to null before calling Parse(), and put it back to normal afterward. // for ref, issue is at https://github.com/golang/go/blob/master/src/flag/flag.go#L793 fs.SetOutput(os.NewFile(uintptr(87592), os.DevNull)) err = fs.Parse(os.Args[2:]) fs.SetOutput(nil) if err != nil { // ignore the flag not defined error, which is expected because // module parameters are defined in modules and not in main if len(err.Error()) > 30 && err.Error()[0:29] == "flag provided but not defined" { // requeue the parameter that failed modargs = append(modargs, err.Error()[31:]) } else { // if it's another error, panic panic(err) } } for _, arg := range fs.Args() { modargs = append(modargs, arg) } modRunner = modules.Available[op.Module].Runner() if _, ok := modRunner.(modules.HasParamsParser); !ok { fmt.Fprintf(os.Stderr, "[error] module '%s' does not support command line invocation\n", op.Module) os.Exit(2) } op.Parameters, err = modRunner.(modules.HasParamsParser).ParamsParser(modargs) if err != nil || op.Parameters == nil { panic(err) } a.Operations = append(a.Operations, op) for _, arg := range os.Args[1:] { a.Name += arg + " " } a.Target = target readytolaunch: // instanciate an API client conf, err = client.ReadConfiguration(migrc) if err != nil { panic(err) } cli, err = client.NewClient(conf, "cmd-"+version) if err != nil { panic(err) } // set the validity 60 second in the past to deal with clock skew a.ValidFrom = time.Now().Add(-60 * time.Second).UTC() period, err := time.ParseDuration(expiration) if err != nil { panic(err) } a.ExpireAfter = a.ValidFrom.Add(period) // add extra 60 seconds taken for clock skew a.ExpireAfter = a.ExpireAfter.Add(60 * time.Second).UTC() asig, err := cli.SignAction(a) if err != nil { panic(err) } a = asig // evaluate target before launch, give a change to cancel before going out to agents agents, err := cli.EvaluateAgentTarget(a.Target) if err != nil { panic(err) } fmt.Fprintf(os.Stderr, "\x1b[33m%d agents will be targeted. ctrl+c to cancel. launching in \x1b[0m", len(agents)) for i := 5; i > 0; i-- { time.Sleep(1 * time.Second) fmt.Fprintf(os.Stderr, "\x1b[33m%d\x1b[0m ", i) } fmt.Fprintf(os.Stderr, "\x1b[33mGO\n\x1b[0m") // launch and follow a, err = cli.PostAction(a) if err != nil { panic(err) } c := make(chan os.Signal, 1) done := make(chan bool, 1) signal.Notify(c, os.Interrupt) go func() { err = cli.FollowAction(a) if err != nil { panic(err) } done <- true }() select { case <-c: fmt.Fprintf(os.Stderr, "stop following action. agents may still be running. printing available results:\n") goto printresults case <-done: goto printresults } printresults: err = cli.PrintActionResults(a, show, render) if err != nil { panic(err) } }