fleet/cmd/fleetctl/goquery.go
Victor Lyuboslavsky 0e040cc7b0
fleetctl now runs saved queries (#15667)
📺 Looom:
https://www.loom.com/share/1aec4616fa4449e7abac579084aef0ba?sid=0884f742-feb3-48bb-82dc-b7834bc9a6e1

Fixed fleetctl issue where it was creating a new query when running a
query by name, as opposed to using the existing saved query.
#15630

API change will be in a separate PR:
https://github.com/fleetdm/fleet/pull/15673

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
2023-12-15 12:55:39 -06:00

133 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, nil, []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
},
}
}