package main import ( "errors" "fmt" "strconv" "github.com/AbGuthrie/goquery/v2" gqconfig "github.com/AbGuthrie/goquery/v2/config" gqhosts "github.com/AbGuthrie/goquery/v2/hosts" gqmodels "github.com/AbGuthrie/goquery/v2/models" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/service" "github.com/urfave/cli/v2" ) type activeQuery struct { status string results []map[string]string } type goqueryClient struct { client *service.Client queryCounter int queries map[string]activeQuery // goquery passes the UUID, while we need the hostname (or ID) to // query against Fleet. Keep a mapping so that we know how to target // the host. hostnameByUUID map[string]string } func newGoqueryClient(fleetClient *service.Client) *goqueryClient { return &goqueryClient{ client: fleetClient, queryCounter: 0, queries: make(map[string]activeQuery), hostnameByUUID: make(map[string]string), } } func (c *goqueryClient) CheckHost(query string) (gqhosts.Host, error) { res, err := c.client.SearchTargets(query, nil, nil) if err != nil { return gqhosts.Host{}, err } var host *fleet.Host for _, h := range res.Hosts { // We allow hosts to be looked up by hostname in addition to UUID if query == h.UUID || query == h.Hostname || query == h.ComputerName { host = h break } } if host == nil { return gqhosts.Host{}, fmt.Errorf("host %s not found", query) } c.hostnameByUUID[host.UUID] = host.Hostname return gqhosts.Host{ UUID: host.UUID, ComputerName: host.ComputerName, Platform: host.Platform, Version: host.OsqueryVersion, }, nil } func (c *goqueryClient) ScheduleQuery(uuid, query string) (string, error) { c.queryCounter++ queryName := strconv.Itoa(c.queryCounter) hostname, ok := c.hostnameByUUID[uuid] if !ok { return "", errors.New("could not lookup host") } res, err := c.client.LiveQuery(query, []string{}, []string{hostname}) if err != nil { return "", err } c.queries[queryName] = activeQuery{status: "Pending"} // We need to start a separate thread due to goquery expecting // scheduling a query and retrieving results to be separate // operations. go func() { select { case hostResult := <-res.Results(): c.queries[queryName] = activeQuery{status: "Completed", results: hostResult.Rows} // Print an error case err := <-res.Errors(): c.queries[queryName] = activeQuery{status: "error: " + err.Error()} } }() gqhosts.AddQueryToHost(uuid, gqhosts.Query{Name: queryName, SQL: query}) return queryName, nil } func (c *goqueryClient) FetchResults(queryName string) (gqmodels.Rows, string, error) { res, ok := c.queries[queryName] if !ok { return nil, "", fmt.Errorf("Unknown query %s", queryName) } return res.results, res.status, nil } func goqueryCommand() *cli.Command { return &cli.Command{ Name: "goquery", Usage: "Start the goquery interface", Flags: []cli.Flag{ configFlag(), contextFlag(), yamlFlag(), debugFlag(), }, Action: func(c *cli.Context) error { fleet, err := clientFromCLI(c) if err != nil { return err } goquery.Run(newGoqueryClient(fleet), gqconfig.Config{}) return nil }, } }