fleet/cmd/fleetctl/get.go

894 lines
18 KiB
Go
Raw Normal View History

package main
import (
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"github.com/fleetdm/fleet/v4/secure"
"gopkg.in/guregu/null.v3"
2021-06-26 04:46:51 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/ghodss/yaml"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
const (
yamlFlagName = "yaml"
jsonFlagName = "json"
withQueriesFlagName = "with-queries"
expiredFlagName = "expired"
stdoutFlagName = "stdout"
)
type specGeneric struct {
Kind string `json:"kind"`
Version string `json:"apiVersion"`
Spec interface{} `json:"spec"`
}
func defaultTable(writer io.Writer) *tablewriter.Table {
w := writerOrStdout(writer)
table := tablewriter.NewWriter(w)
table.SetRowLine(true)
return table
}
func writerOrStdout(writer io.Writer) io.Writer {
var w io.Writer
w = os.Stdout
if writer != nil {
w = writer
}
return w
}
func yamlFlag() cli.Flag {
return &cli.BoolFlag{
Name: yamlFlagName,
Usage: "Output in yaml format",
}
}
func jsonFlag() cli.Flag {
return &cli.BoolFlag{
Name: jsonFlagName,
Usage: "Output in JSON format",
}
}
func printJSON(spec interface{}, writer io.Writer) error {
w := writerOrStdout(writer)
b, err := json.Marshal(spec)
if err != nil {
return err
}
fmt.Fprintf(w, "%s\n", b)
return nil
}
func printYaml(spec interface{}, writer io.Writer) error {
w := writerOrStdout(writer)
b, err := yaml.Marshal(spec)
if err != nil {
return err
}
fmt.Fprintf(w, "---\n%s", string(b))
return nil
}
func printLabel(c *cli.Context, label *fleet.LabelSpec) error {
spec := specGeneric{
Kind: fleet.LabelKind,
Version: fleet.ApiVersion,
Spec: label,
}
return printSpec(c, spec)
}
func printQuery(c *cli.Context, query *fleet.QuerySpec) error {
spec := specGeneric{
Kind: fleet.QueryKind,
Version: fleet.ApiVersion,
Spec: query,
}
return printSpec(c, spec)
}
func printPack(c *cli.Context, pack *fleet.PackSpec) error {
spec := specGeneric{
Kind: fleet.PackKind,
Version: fleet.ApiVersion,
Spec: pack,
}
return printSpec(c, spec)
}
func printSecret(c *cli.Context, secret *fleet.EnrollSecretSpec) error {
spec := specGeneric{
Kind: fleet.EnrollSecretKind,
Version: fleet.ApiVersion,
Spec: secret,
}
return printSpec(c, spec)
}
func printHost(c *cli.Context, host *service.HostResponse) error {
spec := specGeneric{
Kind: fleet.HostKind,
Version: fleet.ApiVersion,
Spec: host,
}
return printSpec(c, spec)
}
func printConfig(c *cli.Context, config *fleet.AppConfig) error {
spec := specGeneric{
Kind: fleet.AppConfigKind,
Version: fleet.ApiVersion,
Spec: config,
}
return printSpec(c, spec)
}
type UserRoles struct {
Roles map[string]UserRole `json:"roles"`
}
type TeamRole struct {
Team string `json:"team"`
Role string `json:"role"`
}
type UserRole struct {
GlobalRole *string `json:"global_role"`
Teams []TeamRole `json:"teams"`
}
func usersToUserRoles(users []fleet.User) UserRoles {
roles := make(map[string]UserRole)
for _, u := range users {
var teams []TeamRole
for _, t := range u.Teams {
teams = append(teams, TeamRole{
Team: t.Name,
Role: t.Role,
})
}
roles[u.Email] = UserRole{
GlobalRole: u.GlobalRole,
Teams: teams,
}
}
return UserRoles{Roles: roles}
}
func printUserRoles(c *cli.Context, users []fleet.User) error {
spec := specGeneric{
Kind: fleet.UserRolesKind,
Version: fleet.ApiVersion,
Spec: usersToUserRoles(users),
}
return printSpec(c, spec)
}
func printTeams(c *cli.Context, teams []fleet.Team) error {
for _, team := range teams {
spec := specGeneric{
Kind: fleet.TeamKind,
Version: fleet.ApiVersion,
Spec: map[string]interface{}{
"team": team,
},
}
if err := printSpec(c, spec); err != nil {
return err
}
}
return nil
}
func printSpec(c *cli.Context, spec specGeneric) error {
var err error
if c.Bool(jsonFlagName) {
err = printJSON(spec, c.App.Writer)
} else {
err = printYaml(spec, c.App.Writer)
}
return err
}
func getCommand() *cli.Command {
return &cli.Command{
Name: "get",
Usage: "Get/list resources",
Subcommands: []*cli.Command{
getQueriesCommand(),
getPacksCommand(),
getLabelsCommand(),
getHostsCommand(),
getEnrollSecretCommand(),
getAppConfigCommand(),
getCarveCommand(),
getCarvesCommand(),
getUserRolesCommand(),
getTeamsCommand(),
},
}
}
func getQueriesCommand() *cli.Command {
return &cli.Command{
Name: "queries",
Aliases: []string{"query", "q"},
Usage: "List information about one or more queries",
Flags: []cli.Flag{
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
name := c.Args().First()
// if name wasn't provided, list all queries
if name == "" {
queries, err := client.GetQueries()
if err != nil {
return errors.Wrap(err, "could not list queries")
}
if len(queries) == 0 {
fmt.Println("No queries found")
return nil
}
if c.Bool(yamlFlagName) || c.Bool(jsonFlagName) {
for _, query := range queries {
if err := printQuery(c, query); err != nil {
return errors.Wrap(err, "unable to print query")
}
}
} else {
// Default to printing as a table
data := [][]string{}
for _, query := range queries {
data = append(data, []string{
query.Name,
query.Description,
query.Query,
})
}
columns := []string{"name", "description", "query"}
printTable(c, columns, data)
}
return nil
}
query, err := client.GetQuery(name)
if err != nil {
return err
}
if err := printQuery(c, query); err != nil {
return errors.Wrap(err, "unable to print query")
}
return nil
},
}
}
func getPacksCommand() *cli.Command {
return &cli.Command{
Name: "packs",
Aliases: []string{"pack", "p"},
Usage: "List information about one or more packs",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: withQueriesFlagName,
Usage: "Output queries included in pack(s) too",
},
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
name := c.Args().First()
shouldPrintQueries := c.Bool(withQueriesFlagName)
queriesToPrint := make(map[string]bool)
addQueries := func(pack *fleet.PackSpec) {
if shouldPrintQueries {
for _, q := range pack.Queries {
queriesToPrint[q.QueryName] = true
}
}
}
printQueries := func() error {
if !shouldPrintQueries {
return nil
}
queries, err := client.GetQueries()
if err != nil {
return errors.Wrap(err, "could not list queries")
}
// Getting all queries then filtering is usually faster than getting
// one query at a time.
for _, query := range queries {
if !queriesToPrint[query.Name] {
continue
}
if err := printQuery(c, query); err != nil {
return errors.Wrap(err, "unable to print query")
}
}
return nil
}
// if name wasn't provided, list all packs
if name == "" {
packs, err := client.GetPacks()
if err != nil {
return errors.Wrap(err, "could not list packs")
}
if c.Bool(yamlFlagName) {
for _, pack := range packs {
if err := printPack(c, pack); err != nil {
return errors.Wrap(err, "unable to print pack")
}
addQueries(pack)
}
return printQueries()
}
if len(packs) == 0 {
fmt.Println("No packs found")
return nil
}
data := [][]string{}
for _, pack := range packs {
data = append(data, []string{
pack.Name,
pack.Platform,
pack.Description,
strconv.FormatBool(pack.Disabled),
})
}
columns := []string{"name", "platform", "description", "disabled"}
printTable(c, columns, data)
return nil
}
// Name was specified
pack, err := client.GetPack(name)
if err != nil {
return err
}
addQueries(pack)
if err := printPack(c, pack); err != nil {
return errors.Wrap(err, "unable to print pack")
}
return printQueries()
},
}
}
func getLabelsCommand() *cli.Command {
return &cli.Command{
Name: "labels",
Aliases: []string{"label", "l"},
Usage: "List information about one or more labels",
Flags: []cli.Flag{
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
name := c.Args().First()
// if name wasn't provided, list all labels
if name == "" {
labels, err := client.GetLabels()
if err != nil {
return errors.Wrap(err, "could not list labels")
}
if c.Bool(yamlFlagName) || c.Bool(jsonFlagName) {
for _, label := range labels {
printLabel(c, label)
}
return nil
}
if len(labels) == 0 {
fmt.Println("No labels found")
return nil
}
// Default to printing as a table
data := [][]string{}
for _, label := range labels {
data = append(data, []string{
label.Name,
label.Platform,
label.Description,
label.Query,
})
}
columns := []string{"name", "platform", "description", "query"}
printTable(c, columns, data)
return nil
}
// Label name was specified
label, err := client.GetLabel(name)
if err != nil {
return err
}
printLabel(c, label)
return nil
},
}
}
func getEnrollSecretCommand() *cli.Command {
return &cli.Command{
Name: "enroll_secret",
Aliases: []string{"enroll_secrets", "enroll-secret", "enroll-secrets"},
Usage: "Retrieve the osquery enroll secrets",
Flags: []cli.Flag{
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
secrets, err := client.GetEnrollSecretSpec()
if err != nil {
return err
}
err = printSecret(c, secrets)
if err != nil {
return err
}
return nil
},
}
}
func getAppConfigCommand() *cli.Command {
return &cli.Command{
Name: "config",
Usage: "Retrieve the Fleet configuration",
Flags: []cli.Flag{
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
config, err := client.GetAppConfig()
if err != nil {
return err
}
err = printConfig(c, config)
if err != nil {
return err
}
return nil
},
}
}
func getHostsCommand() *cli.Command {
return &cli.Command{
Name: "hosts",
Aliases: []string{"host", "h"},
Usage: "List information about one or more hosts",
Flags: []cli.Flag{
&cli.UintFlag{
Name: "team",
Usage: "filter hosts by team_id",
Required: false,
},
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
identifier := c.Args().First()
if identifier == "" {
query := ""
if c.Uint("team") > 0 {
query = fmt.Sprintf("team_id=%d", c.Uint("team"))
}
hosts, err := client.GetHosts(query)
if err != nil {
return errors.Wrap(err, "could not list hosts")
}
if len(hosts) == 0 {
fmt.Println("No hosts found")
return nil
}
if c.Bool(jsonFlagName) || c.Bool(yamlFlagName) {
for _, host := range hosts {
err = printHost(c, &host)
if err != nil {
return err
}
}
return nil
}
// Default to printing as table
data := [][]string{}
for _, host := range hosts {
data = append(data, []string{
host.Host.UUID,
host.DisplayText,
host.Host.Platform,
host.OsqueryVersion,
string(host.Status),
})
}
columns := []string{"uuid", "hostname", "platform", "osquery_version", "status"}
printTable(c, columns, data)
} else {
host, err := client.HostByIdentifier(identifier)
if err != nil {
return errors.Wrap(err, "could not get host")
}
b, err := yaml.Marshal(host)
if err != nil {
return err
}
fmt.Print(string(b))
}
return nil
},
}
}
func getCarvesCommand() *cli.Command {
return &cli.Command{
Name: "carves",
Usage: "Retrieve the file carving sessions",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: expiredFlagName,
Usage: "Include expired carves",
},
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
expired := c.Bool(expiredFlagName)
carves, err := client.ListCarves(fleet.CarveListOptions{Expired: expired})
if err != nil {
return err
}
if len(carves) == 0 {
fmt.Println("No carves found")
return nil
}
data := [][]string{}
for _, c := range carves {
completion := fmt.Sprintf(
"%d%%",
int64((float64(c.MaxBlock+1)/float64(c.BlockCount))*100),
)
if c.Expired {
completion = "Expired"
}
data = append(data, []string{
strconv.FormatInt(c.ID, 10),
c.CreatedAt.Local().String(),
c.RequestId,
strconv.FormatInt(c.CarveSize, 10),
completion,
})
}
columns := []string{"id", "created_at", "request_id", "carve_size", "completion"}
printTable(c, columns, data)
return nil
},
}
}
func getCarveCommand() *cli.Command {
return &cli.Command{
Name: "carve",
Usage: "Retrieve details for a carve by ID",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: stdoutFlagName,
Usage: "Print carve contents to stdout",
},
configFlag(),
contextFlag(),
outfileFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
idString := c.Args().First()
if idString == "" {
return errors.Errorf("must provide carve ID as first argument")
}
id, err := strconv.ParseInt(idString, 10, 64)
if err != nil {
return errors.Wrap(err, "unable to parse carve ID as int")
}
outFile := getOutfile(c)
stdout := c.Bool(stdoutFlagName)
if stdout && outFile != "" {
return errors.Errorf("-stdout and -outfile must not be specified together")
}
if stdout || outFile != "" {
out := os.Stdout
if outFile != "" {
f, err := secure.OpenFile(outFile, os.O_CREATE|os.O_WRONLY, defaultFileMode)
if err != nil {
return errors.Wrap(err, "open out file")
}
defer f.Close()
out = f
}
reader, err := client.DownloadCarve(id)
if err != nil {
return err
}
if _, err := io.Copy(out, reader); err != nil {
return errors.Wrap(err, "download carve contents")
}
return nil
}
carve, err := client.GetCarve(id)
if err != nil {
return err
}
if err := printYaml(carve, c.App.Writer); err != nil {
return errors.Wrap(err, "print carve yaml")
}
return nil
},
}
}
func log(c *cli.Context, msg ...interface{}) {
fmt.Fprint(c.App.Writer, msg...)
}
func logf(c *cli.Context, format string, a ...interface{}) {
fmt.Fprintf(c.App.Writer, format, a...)
}
func getUserRolesCommand() *cli.Command {
return &cli.Command{
Name: "user_roles",
Aliases: []string{"user_role", "ur"},
Usage: "List global and team roles for users",
Flags: []cli.Flag{
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
users, err := client.ListUsers()
if err != nil {
return errors.Wrap(err, "could not list users")
}
if len(users) == 0 {
log(c, "No users found")
return nil
}
if c.Bool(jsonFlagName) || c.Bool(yamlFlagName) {
err = printUserRoles(c, users)
if err != nil {
return err
}
return nil
}
// Default to printing as table
data := [][]string{}
for _, u := range users {
data = append(data, []string{
u.Name,
null.StringFromPtr(u.GlobalRole).ValueOrZero(),
})
}
columns := []string{"User", "Global Role"}
printTable(c, columns, data)
return nil
},
}
}
func printTable(c *cli.Context, columns []string, data [][]string) {
table := defaultTable(c.App.Writer)
table.SetHeader(columns)
table.AppendBulk(data)
table.Render()
}
func getTeamsCommand() *cli.Command {
return &cli.Command{
Name: "teams",
Aliases: []string{"t"},
Usage: "List teams",
Flags: []cli.Flag{
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
client, err := clientFromCLI(c)
if err != nil {
return err
}
teams, err := client.ListTeams()
if err != nil {
return errors.Wrap(err, "could not list teams")
}
if len(teams) == 0 {
log(c, "No teams found")
return nil
}
if c.Bool(jsonFlagName) || c.Bool(yamlFlagName) {
err = printTeams(c, teams)
if err != nil {
return err
}
return nil
}
// Default to printing as table
data := [][]string{}
for _, team := range teams {
data = append(data, []string{
team.Name,
team.Description,
fmt.Sprintf("%d", team.UserCount),
})
}
columns := []string{"Team Name", "Description", "User count"}
printTable(c, columns, data)
return nil
},
}
}