fleet/cmd/fleetctl/goquery.go

134 lines
3.2 KiB
Go

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
},
}
}