зеркало из https://github.com/mozilla/mig.git
406 строки
10 KiB
Go
406 строки
10 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
//
|
|
// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr]
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bobappleyard/readline"
|
|
"mig.ninja/mig"
|
|
"mig.ninja/mig/client"
|
|
)
|
|
|
|
// 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":
|
|
tmpAction, err := getActionView(a)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
var ajson []byte
|
|
ajson, err = json.MarshalIndent(tmpAction, "", " ")
|
|
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 actionPrintShort(data interface{}) (idstr, name, target, datestr, invs, status string, sent int, err error) {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
err = fmt.Errorf("actionPrintShort() -> %v", e)
|
|
}
|
|
}()
|
|
a, err := client.ValueToAction(data)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
invs = investigatorsStringFromAction(a.Investigators, 23)
|
|
|
|
idstr = fmt.Sprintf("%.0f", a.ID)
|
|
if len(idstr) < 14 {
|
|
for i := len(idstr); i < 14; i++ {
|
|
idstr += " "
|
|
}
|
|
}
|
|
|
|
name = a.Name
|
|
if len(name) < 30 {
|
|
for i := len(name); i < 30; i++ {
|
|
name += " "
|
|
}
|
|
}
|
|
if len(name) > 30 {
|
|
name = name[0:27] + "..."
|
|
}
|
|
|
|
target = a.Target
|
|
if len(target) < 30 {
|
|
for i := len(target); i < 30; i++ {
|
|
target += " "
|
|
}
|
|
}
|
|
if len(target) > 30 {
|
|
target = target[0:27] + "..."
|
|
}
|
|
|
|
status = a.Status
|
|
if len(status) < 10 {
|
|
for i := len(status); i < 10; i++ {
|
|
status += " "
|
|
}
|
|
}
|
|
|
|
datestr = a.LastUpdateTime.UTC().Format(time.RFC3339)
|
|
if len(datestr) > 21 {
|
|
datestr = datestr[0:21]
|
|
}
|
|
if len(datestr) < 20 {
|
|
for i := len(datestr); i < 20; i++ {
|
|
datestr += " "
|
|
}
|
|
}
|
|
sent = a.Counters.Sent
|
|
return
|
|
}
|
|
|
|
func investigatorsStringFromAction(invlist []mig.Investigator, strlen int) (investigators string) {
|
|
for ctr, i := range invlist {
|
|
if ctr > 0 {
|
|
investigators += "; "
|
|
}
|
|
investigators += i.Name
|
|
}
|
|
if len(investigators) > strlen {
|
|
investigators = investigators[0:(strlen-3)] + "..."
|
|
}
|
|
if len(investigators) < strlen {
|
|
for i := len(investigators); i < strlen; i++ {
|
|
investigators += " "
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func actionPrintDetails(a mig.Action) {
|
|
fmt.Printf(`
|
|
ID %.0f
|
|
Name %s
|
|
Target %s
|
|
Desc author '%s <%s>'; revision '%.0f';
|
|
url '%s'
|
|
Threat type '%s'; level '%s'; family '%s'; reference '%s'
|
|
Status %s
|
|
Times valid from %s until %s
|
|
started %s; last updated %s; finished %s
|
|
duration: %s
|
|
`, a.ID, a.Name, a.Target, a.Description.Author, a.Description.Email, a.Description.Revision,
|
|
a.Description.URL, a.Threat.Type, a.Threat.Level, a.Threat.Family, a.Threat.Ref, a.Status,
|
|
a.ValidFrom, a.ExpireAfter, a.StartTime, a.LastUpdateTime, a.FinishTime, a.LastUpdateTime.Sub(a.StartTime).String())
|
|
fmt.Printf("Investigators ")
|
|
for _, i := range a.Investigators {
|
|
fmt.Println(i.Name, "- keyid:", i.PGPFingerprint)
|
|
}
|
|
fmt.Printf("Operations count=%d => ", len(a.Operations))
|
|
for _, op := range a.Operations {
|
|
fmt.Printf("%s; ", op.Module)
|
|
}
|
|
fmt.Printf("\n")
|
|
fmt.Printf("Counters sent=%d; done=%d; in flight=%d\n"+
|
|
" success=%d; cancelled=%d; expired=%d; failed=%d; timeout=%d\n",
|
|
a.Counters.Sent, a.Counters.Done, a.Counters.InFlight, a.Counters.Success,
|
|
a.Counters.Cancelled, a.Counters.Expired, a.Counters.Failed, a.Counters.TimeOut)
|
|
return
|
|
}
|
|
|
|
func actionPrintList(aid float64, orders []string, cli client.Client) (err error) {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
err = fmt.Errorf("actionPrintList() -> %v", e)
|
|
}
|
|
}()
|
|
show := "all"
|
|
has_filter := false
|
|
var filter []string
|
|
if len(orders) > 1 {
|
|
switch orders[1] {
|
|
case "all", "found", "notfound":
|
|
show = orders[1]
|
|
if len(orders) > 2 && orders[2] == "|" {
|
|
has_filter = true
|
|
filter = orders[2:]
|
|
}
|
|
case "|":
|
|
has_filter = true
|
|
filter = orders[1:]
|
|
}
|
|
}
|
|
cmds, err := searchCommands(aid, show, cli)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ctr := 0
|
|
if len(cmds) > 0 {
|
|
fmt.Println("---- Command ID ---- ---- Agent Name & ID----")
|
|
for _, cmd := range cmds {
|
|
str := fmt.Sprintf("%20.0f %s [%.0f]", cmd.ID, cmd.Agent.Name, cmd.Agent.ID)
|
|
if has_filter {
|
|
filtered, err := filterString(str, filter)
|
|
if err != nil {
|
|
fmt.Printf("Invalid filter '%s': '%v'\n", filter, err)
|
|
break
|
|
}
|
|
if filtered != "" {
|
|
fmt.Println(filtered)
|
|
ctr++
|
|
}
|
|
} else {
|
|
fmt.Println(str)
|
|
ctr++
|
|
}
|
|
}
|
|
}
|
|
switch show {
|
|
case "found":
|
|
fmt.Printf("%d agents have found things\n", ctr)
|
|
case "notfound":
|
|
fmt.Printf("%d agents have found nothing\n", ctr)
|
|
case "all":
|
|
fmt.Printf("%d agents have returned\n", ctr)
|
|
}
|
|
return
|
|
}
|
|
|
|
func searchCommands(aid float64, show string, cli client.Client) (cmds []mig.Command, err error) {
|
|
defer func() {
|
|
fmt.Printf("\n")
|
|
if e := recover(); e != nil {
|
|
err = fmt.Errorf("searchCommands() -> %v", e)
|
|
}
|
|
}()
|
|
base := fmt.Sprintf("search?type=command&actionid=%.0f", aid)
|
|
switch show {
|
|
case "found":
|
|
base += "&foundanything=true"
|
|
case "notfound":
|
|
base += "&foundanything=false"
|
|
}
|
|
offset := 0
|
|
// loop until all results have been retrieved using paginated queries
|
|
for {
|
|
fmt.Printf(".")
|
|
target := fmt.Sprintf("%s&limit=50&offset=%d", base, offset)
|
|
resource, err := cli.GetAPIResource(target)
|
|
// because we query using pagination, the last query will return a 404 with no result.
|
|
// When that happens, GetAPIResource returns an error which we do not report to the user
|
|
if resource.Collection.Error.Message == "no results found" {
|
|
err = nil
|
|
break
|
|
} else if err != nil {
|
|
panic(err)
|
|
}
|
|
for _, item := range resource.Collection.Items {
|
|
for _, data := range item.Data {
|
|
if data.Name != "command" {
|
|
continue
|
|
}
|
|
cmd, err := client.ValueToCommand(data.Value)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
}
|
|
// else increase limit and offset and continue
|
|
offset += 50
|
|
}
|
|
return
|
|
}
|