diff --git a/changes/19127-update-logic-and-copy-around-host-identifiers b/changes/19127-update-logic-and-copy-around-host-identifiers new file mode 100644 index 0000000000..f482ac0fd2 --- /dev/null +++ b/changes/19127-update-logic-and-copy-around-host-identifiers @@ -0,0 +1,2 @@ +- Update `fleetctl query --hosts` to work with hostnames, host UUIDs, and/or hardware serial numbers. +- Clarify various help and error texts around host identifiers. diff --git a/cmd/fleetctl/hosts.go b/cmd/fleetctl/hosts.go index 5fae97315c..3fc511ad07 100644 --- a/cmd/fleetctl/hosts.go +++ b/cmd/fleetctl/hosts.go @@ -36,7 +36,7 @@ func transferCommand() *cli.Command { }, &cli.StringSliceFlag{ Name: hostsFlagName, - Usage: "Comma separated hostnames to transfer", + Usage: "Comma-separated hostnames to transfer", }, &cli.StringFlag{ Name: labelFlagName, diff --git a/cmd/fleetctl/mdm.go b/cmd/fleetctl/mdm.go index 80d6fe42e5..53ef15b3a5 100644 --- a/cmd/fleetctl/mdm.go +++ b/cmd/fleetctl/mdm.go @@ -41,7 +41,7 @@ func mdmRunCommand() *cli.Command { debugFlag(), &cli.StringSliceFlag{ Name: "hosts", - Usage: "Hosts specified by hostname, serial number, uuid, osquery_host_id or node_key that you want to target.", + Usage: "Comma-separated hosts to target. Hosts can be specified by hostname, UUID, or serial number.", Required: true, }, &cli.StringFlag{ @@ -122,13 +122,9 @@ func mdmRunCommand() *cli.Command { hostUUIDs = append(hostUUIDs, host.UUID) } - if len(hostUUIDs) == 0 { - // all hosts were not found - return errors.New("No hosts targeted. Make sure you provide a valid hostname, UUID, osquery host ID, or node key.") - } - if notFoundCount > 0 { - // at least one was not found - return errors.New("One or more targeted hosts don't exist. Make sure you provide a valid hostname, UUID, osquery host ID, or node key.") + if len(hostUUIDs) == 0 || notFoundCount > 0 { + // Either no hosts were targeted, or at least one targeted host was not found + return errors.New(fleet.TargetedHostsDontExistErrMsg) } result, err := client.RunMDMCommand(hostUUIDs, payload, mdmPlatform) @@ -171,7 +167,7 @@ func mdmLockCommand() *cli.Command { Usage: "Lock a host when it needs to be returned to your organization.", Flags: []cli.Flag{contextFlag(), debugFlag(), &cli.StringFlag{ Name: "host", - Usage: "The host, specified by identifier, that you want to lock.", + Usage: "The host, specified by hostname, UUID, or serial number.", Required: true, }}, Action: func(c *cli.Context) error { @@ -210,7 +206,7 @@ func mdmUnlockCommand() *cli.Command { Usage: "Unlock a host when it needs to be returned to your organization.", Flags: []cli.Flag{contextFlag(), debugFlag(), &cli.StringFlag{ Name: "host", - Usage: "The host, specified by identifier, that you want to unlock.", + Usage: "The host, specified by hostname, UUID, or serial number.", Required: true, }}, Action: func(c *cli.Context) error { @@ -312,7 +308,7 @@ func hostMdmActionSetup(c *cli.Context, hostIdent string, actionType string) (cl if err != nil { var nfe service.NotFoundErr if errors.As(err, &nfe) { - return nil, nil, errors.New("The host doesn't exist. Please provide a valid host identifier.") + return nil, nil, errors.New(fleet.HostNotFoundErrMsg) } var sce kithttp.StatusCoder diff --git a/cmd/fleetctl/mdm_test.go b/cmd/fleetctl/mdm_test.go index 5251a8dda1..9d66c2126a 100644 --- a/cmd/fleetctl/mdm_test.go +++ b/cmd/fleetctl/mdm_test.go @@ -291,7 +291,7 @@ func TestMDMRunCommand(t *testing.T) { {"macOS yaml payload", []string{"--hosts", "mac-enrolled", "--payload", yamlFilePath}, appCfgAllMDM, `The payload isn't valid XML`}, {"win yaml payload", []string{"--hosts", "win-enrolled", "--payload", yamlFilePath}, appCfgAllMDM, `The payload isn't valid XML`}, {"non-mdm-command plist payload", []string{"--hosts", "mac-enrolled", "--payload", mobileConfigFilePath}, appCfgAllMDM, `The payload isn't valid. Please provide a valid MDM command in the form of a plist-encoded XML file:`}, - {"single host not found", []string{"--hosts", "no-such-host", "--payload", appleCmdFilePath}, appCfgAllMDM, `No hosts targeted.`}, + {"single host not found", []string{"--hosts", "no-such-host", "--payload", appleCmdFilePath}, appCfgAllMDM, fleet.TargetedHostsDontExistErrMsg}, {"unenrolled macOS host", []string{"--hosts", "mac-unenrolled", "--payload", appleCmdFilePath}, appCfgAllMDM, `Can't run the MDM command because one or more hosts have MDM turned off.`}, {"unenrolled windows host", []string{"--hosts", "win-unenrolled", "--payload", winCmdFilePath}, appCfgAllMDM, `Can't run the MDM command because one or more hosts have MDM turned off.`}, {"macOS non-fleet host", []string{"--hosts", "mac-non-fleet-enrolled", "--payload", appleCmdFilePath}, appCfgAllMDM, `Can't run the MDM command because one or more hosts have MDM turned off.`}, @@ -319,7 +319,7 @@ func TestMDMRunCommand(t *testing.T) { {"non-Exec win file", []string{"--hosts", "win-enrolled", "--payload", nonExecWinCmdFilePath.Name()}, appCfgAllMDM, `You can run only command type.`}, {"empty win file", []string{"--hosts", "win-enrolled", "--payload", emptyWinCmdFilePath.Name()}, appCfgAllMDM, `You can run only a single command.`}, {"hosts with different platforms", []string{"--hosts", "win-enrolled,mac-enrolled", "--payload", winCmdFilePath}, appCfgAllMDM, `Command can't run on hosts with different platforms.`}, - {"all hosts not found", []string{"--hosts", "no-such-1,no-such-2,no-such-3", "--payload", winCmdFilePath}, appCfgAllMDM, `No hosts targeted.`}, + {"all hosts not found", []string{"--hosts", "no-such-1,no-such-2,no-such-3", "--payload", winCmdFilePath}, appCfgAllMDM, fleet.TargetedHostsDontExistErrMsg}, {"one host not found", []string{"--hosts", "win-enrolled,no-such-2,win-enrolled-2", "--payload", winCmdFilePath}, appCfgAllMDM, `One or more targeted hosts don't exist.`}, {"one windows host not enrolled", []string{"--hosts", "win-enrolled,win-unenrolled,win-enrolled-2", "--payload", winCmdFilePath}, appCfgAllMDM, `Can't run the MDM command because one or more hosts have MDM turned off.`}, {"one macOS host not enrolled", []string{"--hosts", "mac-enrolled,mac-unenrolled,mac-enrolled-2", "--payload", appleCmdFilePath}, appCfgAllMDM, `Can't run the MDM command because one or more hosts have MDM turned off.`}, @@ -350,7 +350,6 @@ func TestMDMRunCommand(t *testing.T) { } func TestMDMLockCommand(t *testing.T) { - macEnrolled := testhost{ host: &fleet.Host{ ID: 1, @@ -590,7 +589,7 @@ fleetctl mdm unlock --host=%s }{ {appCfgAllMDM, "no flags", nil, `Required flag "host" not set`}, {appCfgAllMDM, "host flag empty", []string{"--host", ""}, `No host targeted. Please provide --host.`}, - {appCfgAllMDM, "lock non-existent host", []string{"--host", "notfound"}, `The host doesn't exist. Please provide a valid host identifier.`}, + {appCfgAllMDM, "lock non-existent host", []string{"--host", "notfound"}, fleet.HostNotFoundErrMsg}, {appCfgMacMDM, "valid windows but only macos mdm", []string{"--host", winEnrolled.host.UUID}, `Windows MDM isn't turned on.`}, {appCfgWinMDM, "valid macos but only windows mdm", []string{"--host", macEnrolled.host.UUID}, `macOS MDM isn't turned on.`}, {appCfgAllMDM, "valid windows", []string{"--host", winEnrolled.host.UUID}, ""}, @@ -866,7 +865,7 @@ fleetctl get host %s }{ {appCfgAllMDM, "no flags", nil, `Required flag "host" not set`}, {appCfgAllMDM, "host flag empty", []string{"--host", ""}, `No host targeted. Please provide --host.`}, - {appCfgAllMDM, "unlock non-existent host", []string{"--host", "notfound"}, `The host doesn't exist. Please provide a valid host identifier.`}, + {appCfgAllMDM, "unlock non-existent host", []string{"--host", "notfound"}, fleet.HostNotFoundErrMsg}, {appCfgMacMDM, "valid windows but only macos mdm", []string{"--host", winEnrolled.host.UUID}, `Windows MDM isn't turned on.`}, {appCfgAllMDM, "valid windows", []string{"--host", winEnrolled.host.UUID}, ""}, {appCfgAllMDM, "valid macos", []string{"--host", macEnrolled.host.UUID}, ""}, @@ -1225,7 +1224,7 @@ func TestMDMWipeCommand(t *testing.T) { }{ {appCfgAllMDM, "no flags", nil, `Required flag "host" not set`}, {appCfgAllMDM, "host flag empty", []string{"--host", ""}, `No host targeted. Please provide --host.`}, - {appCfgAllMDM, "wipe non-existent host", []string{"--host", "notfound"}, `The host doesn't exist. Please provide a valid host identifier.`}, + {appCfgAllMDM, "wipe non-existent host", []string{"--host", "notfound"}, fleet.HostNotFoundErrMsg}, {appCfgMacMDM, "valid windows but only macos mdm", []string{"--host", winEnrolled.host.UUID}, `Windows MDM isn't turned on.`}, {appCfgAllMDM, "valid windows", []string{"--host", winEnrolled.host.UUID}, ""}, {appCfgAllMDM, "valid macos", []string{"--host", macEnrolled.host.UUID}, ""}, diff --git a/cmd/fleetctl/query.go b/cmd/fleetctl/query.go index 5ea0b31baa..145698e287 100644 --- a/cmd/fleetctl/query.go +++ b/cmd/fleetctl/query.go @@ -9,6 +9,7 @@ import ( "time" "github.com/briandowns/spinner" + "github.com/fleetdm/fleet/v4/server/fleet" "github.com/urfave/cli/v2" ) @@ -28,14 +29,14 @@ func queryCommand() *cli.Command { EnvVars: []string{"HOSTS"}, Value: "", Destination: &flHosts, - Usage: "Comma separated hostnames to target", + Usage: "Comma-separated hosts to target. Hosts can be specified by hostname, UUID, or serial number.", }, &cli.StringFlag{ Name: "labels", EnvVars: []string{"LABELS"}, Value: "", Destination: &flLabels, - Usage: "Comma separated label names to target", + Usage: "Comma-separated label names to target", }, &cli.BoolFlag{ Name: "quiet", @@ -84,7 +85,7 @@ func queryCommand() *cli.Command { debugFlag(), }, Action: func(c *cli.Context) error { - fleet, err := clientFromCLI(c) + client, err := clientFromCLI(c) if err != nil { return err } @@ -103,7 +104,7 @@ func queryCommand() *cli.Command { if tid := c.Uint(teamFlagName); tid != 0 { teamID = &tid } - queries, err := fleet.GetQueries(teamID, &flQueryName) + queries, err := client.GetQueries(teamID, &flQueryName) if err != nil || len(queries) == 0 { return fmt.Errorf("Query '%s' not found", flQueryName) } @@ -131,11 +132,14 @@ func queryCommand() *cli.Command { output = newJsonWriter(c.App.Writer) } - hosts := strings.Split(flHosts, ",") + hostIdentifiers := strings.Split(flHosts, ",") labels := strings.Split(flLabels, ",") - res, err := fleet.LiveQuery(flQuery, queryID, labels, hosts) + res, err := client.LiveQuery(flQuery, queryID, labels, hostIdentifiers) if err != nil { + if strings.Contains(err.Error(), "no hosts targeted") { + return errors.New(fleet.NoHostsTargetedErrMsg) + } return err } diff --git a/cmd/fleetctl/query_test.go b/cmd/fleetctl/query_test.go index 60d569c19f..5a9ca51b9f 100644 --- a/cmd/fleetctl/query_test.go +++ b/cmd/fleetctl/query_test.go @@ -48,7 +48,7 @@ func TestSavedLiveQuery(t *testing.T) { Saved: true, } - ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) { + ds.HostIDsByIdentifierFunc = func(ctx context.Context, filter fleet.TeamFilter, hostIdentifiers []string) ([]uint, error) { return []uint{1234}, nil } ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { @@ -149,6 +149,11 @@ func TestSavedLiveQuery(t *testing.T) { )) }() + // errors before requesting live query + _, err = runAppNoChecks([]string{"query", "--hosts", "", "--query-name", queryName}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "No hosts or labels targeted") + expected := `{"host":"somehostname","rows":[{"bing":"fds","host_display_name":"somehostname","host_hostname":"somehostname"}]} ` // Note: runAppForTest never closes the WebSocket connection and does not exit, @@ -197,7 +202,7 @@ func TestAdHocLiveQuery(t *testing.T) { } } - ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) { + ds.HostIDsByIdentifierFunc = func(ctx context.Context, filter fleet.TeamFilter, hostIdentifiers []string) ([]uint, error) { return []uint{1234}, nil } ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { diff --git a/cmd/fleetctl/scripts.go b/cmd/fleetctl/scripts.go index b38ca9e563..e1b65f3f26 100644 --- a/cmd/fleetctl/scripts.go +++ b/cmd/fleetctl/scripts.go @@ -37,7 +37,7 @@ func runScriptCommand() *cli.Command { }, &cli.StringFlag{ Name: "host", - Usage: "A host, specified by hostname, serial number, UUID, osquery host ID, or node key.", + Usage: "The host, specified by hostname, UUID, or serial number.", Required: true, }, &cli.StringFlag{ @@ -108,7 +108,7 @@ func runScriptCommand() *cli.Command { if err != nil { var nfe service.NotFoundErr if errors.As(err, &nfe) { - return errors.New(fleet.RunScriptHostNotFoundErrMsg) + return errors.New(fleet.HostNotFoundErrMsg) } var sce fleet.ErrWithStatusCode if errors.As(err, &sce) { diff --git a/cmd/fleetctl/scripts_test.go b/cmd/fleetctl/scripts_test.go index 13c30f4818..abb5758584 100644 --- a/cmd/fleetctl/scripts_test.go +++ b/cmd/fleetctl/scripts_test.go @@ -102,7 +102,7 @@ hello world { name: "host not found", scriptPath: generateValidPath, - expectErrMsg: fleet.RunScriptHostNotFoundErrMsg, + expectErrMsg: fleet.HostNotFoundErrMsg, expectNotFound: true, }, { diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index b919a4c000..46f6e69183 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -2299,7 +2299,7 @@ func main() { munkiIssueCount = flag.Int("munki_issue_count", 10, "Number of munki issues reported by hosts identified to have munki issues") // E.g. when running with `-host_count=10`, you can set host count for each template the following way: // `-os_templates=windows_11.tmpl:3,macos_14.1.2.tmpl:4,ubuntu_22.04.tmpl:3` - osTemplates = flag.String("os_templates", "macos_14.1.2", fmt.Sprintf("Comma separated list of host OS templates to use and optionally their host count separated by ':' (any of %v, with or without the .tmpl extension)", allowedTemplateNames)) + osTemplates = flag.String("os_templates", "macos_14.1.2", fmt.Sprintf("Comma-separated list of host OS templates to use and optionally their host count separated by ':' (any of %v, with or without the .tmpl extension)", allowedTemplateNames)) emptySerialProb = flag.Float64("empty_serial_prob", 0.1, "Probability of a host having no serial number [0, 1]") mdmProb = flag.Float64("mdm_prob", 0.0, "Probability of a host enrolling via Fleet MDM (applies for macOS and Windows hosts, implies orbit enrollment on Windows) [0, 1]") diff --git a/frontend/utilities/convert_to_csv/index.ts b/frontend/utilities/convert_to_csv/index.ts index 5a4b7b4769..33232564d9 100644 --- a/frontend/utilities/convert_to_csv/index.ts +++ b/frontend/utilities/convert_to_csv/index.ts @@ -17,7 +17,7 @@ const formatFieldForCSV = (value: any): string => { } // Wrap the value in double quotes to enclose any value that may - // have a, or a " in it to distinguish them from a comma separated delimiter + // have a, or a " in it to distinguish them from a comma-separated delimiter return `"${value}"`; }; diff --git a/orbit/pkg/augeas/lenses/crypttab.aug b/orbit/pkg/augeas/lenses/crypttab.aug index 8c9409b3db..3df72268b7 100644 --- a/orbit/pkg/augeas/lenses/crypttab.aug +++ b/orbit/pkg/augeas/lenses/crypttab.aug @@ -75,7 +75,7 @@ module Crypttab = (************************************************************************ * View: comma_sep_list - * A comma separated list of options (opt=value or opt) + * A comma-separated list of options (opt=value or opt) *************************************************************************) let comma_sep_list (l:string) = let value = [ label "value" . Util.del_str "=" . store optval ] in diff --git a/orbit/pkg/augeas/lenses/gshadow.aug b/orbit/pkg/augeas/lenses/gshadow.aug index c5e8947cc5..d344dee0e4 100644 --- a/orbit/pkg/augeas/lenses/gshadow.aug +++ b/orbit/pkg/augeas/lenses/gshadow.aug @@ -47,7 +47,7 @@ let integer = Rx.integer (* View: member *) let member = [ label "member" . store word ] (* View: member_list - the member list is a comma separated list of + the member list is a comma-separated list of users allowed to chgrp to the group without being prompted for the group's password *) let member_list = Build.opt_list member comma @@ -55,7 +55,7 @@ let member_list = Build.opt_list member comma (* View: admin *) let admin = [ label "admin" . store word ] (* View: admin_list - the admin_list is a comma separated list of + the admin_list is a comma-separated list of users allowed to change the group's password and the member_list *) let admin_list = Build.opt_list admin comma diff --git a/orbit/pkg/augeas/lenses/opendkim.aug b/orbit/pkg/augeas/lenses/opendkim.aug index 10fb44592d..4db5df772e 100644 --- a/orbit/pkg/augeas/lenses/opendkim.aug +++ b/orbit/pkg/augeas/lenses/opendkim.aug @@ -8,7 +8,7 @@ module Opendkim = (* The Dataset spec is so broad as to encompass any string (particularly the - degenerate 'single literal string' case of a comma separated list with + degenerate 'single literal string' case of a comma-separated list with only one item). So treat them as 'String' types, and it's up to the user to format them correctly. Given that many of the variants include file paths etc, it's impossible to validate for 'correctness' anyway diff --git a/orbit/pkg/table/crowdstrike/falconctl/parser.go b/orbit/pkg/table/crowdstrike/falconctl/parser.go index 63ac277689..486af70f65 100644 --- a/orbit/pkg/table/crowdstrike/falconctl/parser.go +++ b/orbit/pkg/table/crowdstrike/falconctl/parser.go @@ -9,7 +9,7 @@ import ( ) // parseOptions parses the stdout returned from falconctl's displayed options. As far as we know, output is a single -// line, comma separated. We parse multiple lines, but assume data does not space that. Eg: linebreaks and commas +// line, comma-separated. We parse multiple lines, but assume data does not space that. Eg: linebreaks and commas // treated as seperators. func parseOptions(reader io.Reader) (any, error) { results := make(map[string]interface{}) diff --git a/orbit/pkg/table/mdm/mdm_windows.go b/orbit/pkg/table/mdm/mdm_windows.go index e3ed4f4c42..e360aa8412 100644 --- a/orbit/pkg/table/mdm/mdm_windows.go +++ b/orbit/pkg/table/mdm/mdm_windows.go @@ -207,7 +207,7 @@ func getCmdResponseData(outputCmd string) (string, error) { continue } - // results will be appended in a comma separated list + // results will be appended in a comma-separated list if len(element.Item) > 0 { // extracting the data from the result diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 55340fd82b..8ff25625b8 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -2547,25 +2547,30 @@ func (ds *Datastore) SearchHosts(ctx context.Context, filter fleet.TeamFilter, m return hosts, nil } -func (ds *Datastore) HostIDsByName(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) { - if len(hostnames) == 0 { +func (ds *Datastore) HostIDsByIdentifier(ctx context.Context, filter fleet.TeamFilter, hostIdentifiers []string) ([]uint, error) { + if len(hostIdentifiers) == 0 { return []uint{}, nil } sqlStatement := fmt.Sprintf(` - SELECT id FROM hosts - WHERE hostname IN (?) AND %s + SELECT + DISTINCT id FROM hosts + WHERE + (hostname IN (?) + OR uuid IN (?) + OR hardware_serial IN (?)) + AND %s `, ds.whereFilterHostsByTeams(filter, "hosts"), ) - sql, args, err := sqlx.In(sqlStatement, hostnames) + sql, args, err := sqlx.In(sqlStatement, hostIdentifiers, hostIdentifiers, hostIdentifiers) if err != nil { - return nil, ctxerr.Wrap(ctx, err, "building query to get host IDs") + return nil, ctxerr.Wrap(ctx, err, "building query to get host IDs by identifier") } var hostIDs []uint if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hostIDs, sql, args...); err != nil { - return nil, ctxerr.Wrap(ctx, err, "get host IDs") + return nil, ctxerr.Wrap(ctx, err, "get host IDs by identifier") } return hostIDs, nil diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 52805bdb0a..9e36aba767 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -97,7 +97,7 @@ func TestHosts(t *testing.T) { {"MarkSeen", testHostsMarkSeen}, {"MarkSeenMany", testHostsMarkSeenMany}, {"CleanupIncoming", testHostsCleanupIncoming}, - {"IDsByName", testHostsIDsByName}, + {"IDsByIdentifier", testHostIDsByIdentifier}, {"Additional", testHostsAdditional}, {"ByIdentifier", testHostsByIdentifier}, {"HostLiteByIdentifierAndID", testHostLiteByIdentifierAndID}, @@ -2273,7 +2273,7 @@ func testHostsCleanupIncoming(t *testing.T, ds *Datastore) { require.NoError(t, err) } -func testHostsIDsByName(t *testing.T, ds *Datastore) { +func testHostIDsByIdentifier(t *testing.T, ds *Datastore) { hosts := make([]*fleet.Host, 10) for i := range hosts { h, err := ds.NewHost(context.Background(), &fleet.Host{ @@ -2281,9 +2281,10 @@ func testHostsIDsByName(t *testing.T, ds *Datastore) { LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), - OsqueryHostID: ptr.String(fmt.Sprintf("host%d", i)), - NodeKey: ptr.String(fmt.Sprintf("%d", i)), - UUID: fmt.Sprintf("%d", i), + OsqueryHostID: ptr.String(fmt.Sprintf("osq.host%d", i)), + NodeKey: ptr.String(fmt.Sprintf("nk.%d", i)), + UUID: fmt.Sprintf("uuid.%d", i), + HardwareSerial: fmt.Sprintf("hws.%d", i), Hostname: fmt.Sprintf("foo.%d.local", i), }) require.NoError(t, err) @@ -2295,34 +2296,47 @@ func testHostsIDsByName(t *testing.T, ds *Datastore) { require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[0].ID})) filter := fleet.TeamFilter{User: test.UserAdmin} - hostsByName, err := ds.HostIDsByName(context.Background(), filter, []string{"foo.2.local", "foo.1.local", "foo.5.local"}) + hostsByIdentifier, err := ds.HostIDsByIdentifier(context.Background(), filter, []string{"foo.2.local", "foo.1.local", "foo.5.local"}) require.NoError(t, err) - sort.Slice(hostsByName, func(i, j int) bool { return hostsByName[i] < hostsByName[j] }) - assert.Equal(t, hostsByName, []uint{2, 3, 6}) + sort.Slice(hostsByIdentifier, func(i, j int) bool { return hostsByIdentifier[i] < hostsByIdentifier[j] }) + assert.Equal(t, hostsByIdentifier, []uint{2, 3, 6}) + + // by UUID + hostsByIdentifier, err = ds.HostIDsByIdentifier(context.Background(), filter, []string{"uuid.0", "uuid.4"}) + require.NoError(t, err) + require.Len(t, hostsByIdentifier, 2) + assert.Equal(t, hostsByIdentifier[0], hosts[0].ID) + assert.Equal(t, hostsByIdentifier[1], hosts[4].ID) + + // by HardwareSerial + hostsByIdentifier, err = ds.HostIDsByIdentifier(context.Background(), filter, []string{"hws.2"}) + require.NoError(t, err) + require.Len(t, hostsByIdentifier, 1) + assert.Equal(t, hostsByIdentifier[0], hosts[2].ID) userObs := &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)} filter = fleet.TeamFilter{User: userObs} - hostsByName, err = ds.HostIDsByName(context.Background(), filter, []string{"foo.2.local", "foo.1.local", "foo.5.local"}) + hostsByIdentifier, err = ds.HostIDsByIdentifier(context.Background(), filter, []string{"foo.2.local", "foo.1.local", "foo.5.local"}) require.NoError(t, err) - assert.Len(t, hostsByName, 0) + assert.Len(t, hostsByIdentifier, 0) filter.IncludeObserver = true - hostsByName, err = ds.HostIDsByName(context.Background(), filter, []string{"foo.2.local", "foo.1.local", "foo.5.local"}) + hostsByIdentifier, err = ds.HostIDsByIdentifier(context.Background(), filter, []string{"foo.2.local", "foo.1.local", "foo.5.local"}) require.NoError(t, err) - assert.Len(t, hostsByName, 3) + assert.Len(t, hostsByIdentifier, 3) userTeam1 := &fleet.User{Teams: []fleet.UserTeam{{Team: *team1, Role: fleet.RoleAdmin}}} filter = fleet.TeamFilter{User: userTeam1} - hostsByName, err = ds.HostIDsByName(context.Background(), filter, []string{"foo.2.local", "foo.1.local", "foo.5.local"}) + hostsByIdentifier, err = ds.HostIDsByIdentifier(context.Background(), filter, []string{"foo.2.local", "foo.1.local", "foo.5.local"}) require.NoError(t, err) - assert.Len(t, hostsByName, 0) + assert.Len(t, hostsByIdentifier, 0) - hostsByName, err = ds.HostIDsByName(context.Background(), filter, []string{"foo.0.local", "foo.1.local", "foo.5.local"}) + hostsByIdentifier, err = ds.HostIDsByIdentifier(context.Background(), filter, []string{"foo.0.local", "foo.1.local", "foo.5.local"}) require.NoError(t, err) - require.Len(t, hostsByName, 1) - assert.Equal(t, hostsByName[0], hosts[0].ID) + require.Len(t, hostsByIdentifier, 1) + assert.Equal(t, hostsByIdentifier[0], hosts[0].ID) } func testLoadHostByNodeKeyLoadsDisk(t *testing.T, ds *Datastore) { diff --git a/server/fleet/app.go b/server/fleet/app.go index 4f61323f28..f0755add59 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -995,7 +995,7 @@ type ListOptions struct { // How many results per page (must be positive integer, 0 indicates // unlimited) PerPage uint `query:"per_page,optional"` - // Key to use for ordering. Can be a comma separated set of items, eg: host_count,id + // Key to use for ordering. Can be a comma-separated set of items, eg: host_count,id OrderKey string `query:"order_key,optional"` // Direction of ordering OrderDirection OrderDirection `query:"order_direction,optional"` diff --git a/server/fleet/capabilities.go b/server/fleet/capabilities.go index 36bcd32dab..b1bfff1139 100644 --- a/server/fleet/capabilities.go +++ b/server/fleet/capabilities.go @@ -15,7 +15,7 @@ type CapabilityMap map[Capability]struct{} // mu is used to allow for safe access to the capability map. var mu sync.Mutex -// PopulateFromString populates the CapabilityMap from a comma separated string. +// PopulateFromString populates the CapabilityMap from a comma-separated string. // Example: "foo,bar,baz" => {"foo": struct{}, "bar": struct{}, "baz": struct{}} func (c *CapabilityMap) PopulateFromString(s string) { mu.Lock() @@ -31,7 +31,7 @@ func (c *CapabilityMap) PopulateFromString(s string) { } } -// String returns a comma separated string with the capabilities in the map. +// String returns a comma-separated string with the capabilities in the map. // Example: {"foo": struct{}, "bar": struct{}, "baz": struct{}} => "foo,bar,baz" func (c *CapabilityMap) String() string { mu.Lock() diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index da5d4df785..5ae659341e 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -261,8 +261,8 @@ type Datastore interface { CleanupIncomingHosts(ctx context.Context, now time.Time) ([]uint, error) // GenerateHostStatusStatistics retrieves the count of online, offline, MIA and new hosts. GenerateHostStatusStatistics(ctx context.Context, filter TeamFilter, now time.Time, platform *string, lowDiskSpace *int) (*HostSummary, error) - // HostIDsByName Retrieve the IDs associated with the given hostnames - HostIDsByName(ctx context.Context, filter TeamFilter, hostnames []string) ([]uint, error) + // HostIDsByIdentifier retrieves the IDs associated with the given hostnames, UUIDs, or hardware serials. + HostIDsByIdentifier(ctx context.Context, filter TeamFilter, hostnames []string) ([]uint, error) // HostIDsByOSID retrieves the IDs of all host for the given OS ID HostIDsByOSID(ctx context.Context, osID uint, offset int, limit int) ([]uint, error) diff --git a/server/fleet/errors.go b/server/fleet/errors.go index c14da53e11..484ca36832 100644 --- a/server/fleet/errors.go +++ b/server/fleet/errors.go @@ -536,10 +536,14 @@ func (e OrbitError) Error() string { // Message that may surfaced by the server or the fleetctl client. const ( + // Hosts, general + HostNotFoundErrMsg = "Host doesn't exist. Make sure you provide a valid hostname, UUID, or serial number. Learn more about host identifiers: https://fleetdm.com/learn-more-about/host-identifiers" + NoHostsTargetedErrMsg = "No hosts targeted. Make sure you provide a valid hostname, UUID, or serial number. Learn more about host identifiers: https://fleetdm.com/learn-more-about/host-identifiers" + TargetedHostsDontExistErrMsg = "One or more targeted hosts don't exist. Make sure you provide a valid hostname, UUID, or serial number. Learn more about host identifiers: https://fleetdm.com/learn-more-about/host-identifiers" + // Scripts RunScriptInvalidTypeErrMsg = "File type not supported. Only .sh (Bash) and .ps1 (PowerShell) file types are allowed." RunScriptHostOfflineErrMsg = "Script can't run on offline host." - RunScriptHostNotFoundErrMsg = "Host doesn't exist. Make sure you provide a valid hostname, UUID, osquery host ID, or node key." RunScriptForbiddenErrMsg = "You don't have the right permissions in Fleet to run the script." RunScriptAlreadyRunningErrMsg = "A script is already running on this host. Please wait about 5 minutes to let it finish." RunScriptHostTimeoutErrMsg = "Fleet didn't hear back from the host in under 5 minutes (timeout for live scripts). Fleet doesn't know if the script ran because it didn't receive the result. Please try again." @@ -554,6 +558,7 @@ const ( // End user authentication EndUserAuthDEPWebURLConfiguredErrMsg = `End user authentication can't be configured when the configured automatic enrollment (DEP) profile specifies a configuration_web_url.` // #nosec G101 + ) // ConflictError is used to indicate a conflict, such as a UUID conflict in the DB. diff --git a/server/fleet/service.go b/server/fleet/service.go index ac1bbffb9c..27bfa5b187 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -294,9 +294,9 @@ type Service interface { // ///////////////////////////////////////////////////////////////////////////// // CampaignService defines the distributed query campaign related service methods - // NewDistributedQueryCampaignByNames creates a new distributed query campaign with the provided query (or the query - // referenced by ID) and host/label targets (specified by name). - NewDistributedQueryCampaignByNames( + // NewDistributedQueryCampaignByIdentifiers creates a new distributed query campaign with the provided query (or the query + // referenced by ID) and host/label targets (specified by hostname, UUID, or hardware serial). + NewDistributedQueryCampaignByIdentifiers( ctx context.Context, queryString string, queryID *uint, hosts []string, labels []string, ) (*DistributedQueryCampaign, error) diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index d38323714a..db2275186b 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -191,7 +191,7 @@ type CleanupIncomingHostsFunc func(ctx context.Context, now time.Time) ([]uint, type GenerateHostStatusStatisticsFunc func(ctx context.Context, filter fleet.TeamFilter, now time.Time, platform *string, lowDiskSpace *int) (*fleet.HostSummary, error) -type HostIDsByNameFunc func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) +type HostIDsByIdentifierFunc func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) type HostIDsByOSIDFunc func(ctx context.Context, osID uint, offset int, limit int) ([]uint, error) @@ -1244,8 +1244,8 @@ type DataStore struct { GenerateHostStatusStatisticsFunc GenerateHostStatusStatisticsFunc GenerateHostStatusStatisticsFuncInvoked bool - HostIDsByNameFunc HostIDsByNameFunc - HostIDsByNameFuncInvoked bool + HostIDsByIdentifierFunc HostIDsByIdentifierFunc + HostIDsByIdentifierFuncInvoked bool HostIDsByOSIDFunc HostIDsByOSIDFunc HostIDsByOSIDFuncInvoked bool @@ -3040,11 +3040,11 @@ func (s *DataStore) GenerateHostStatusStatistics(ctx context.Context, filter fle return s.GenerateHostStatusStatisticsFunc(ctx, filter, now, platform, lowDiskSpace) } -func (s *DataStore) HostIDsByName(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) { +func (s *DataStore) HostIDsByIdentifier(ctx context.Context, filter fleet.TeamFilter, hostIdentifiers []string) ([]uint, error) { s.mu.Lock() - s.HostIDsByNameFuncInvoked = true + s.HostIDsByIdentifierFuncInvoked = true s.mu.Unlock() - return s.HostIDsByNameFunc(ctx, filter, hostnames) + return s.HostIDsByIdentifierFunc(ctx, filter, hostIdentifiers) } func (s *DataStore) HostIDsByOSID(ctx context.Context, osID uint, offset int, limit int) ([]uint, error) { diff --git a/server/service/campaigns.go b/server/service/campaigns.go index cba57bd04e..81e0520c0f 100644 --- a/server/service/campaigns.go +++ b/server/service/campaigns.go @@ -174,34 +174,35 @@ func (svc *Service) NewDistributedQueryCampaign(ctx context.Context, queryString // Create Distributed Query Campaign By Names //////////////////////////////////////////////////////////////////////////////// -type createDistributedQueryCampaignByNamesRequest struct { - QuerySQL string `json:"query"` - QueryID *uint `json:"query_id"` - Selected distributedQueryCampaignTargetsByNames `json:"selected"` +type createDistributedQueryCampaignByIdentifierRequest struct { + QuerySQL string `json:"query"` + QueryID *uint `json:"query_id"` + Selected distributedQueryCampaignTargetsByIdentifiers `json:"selected"` } -type distributedQueryCampaignTargetsByNames struct { +type distributedQueryCampaignTargetsByIdentifiers struct { Labels []string `json:"labels"` - Hosts []string `json:"hosts"` + // list of hostnames, UUIDs, and/or hardware serials + Hosts []string `json:"hosts"` } -func createDistributedQueryCampaignByNamesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { - req := request.(*createDistributedQueryCampaignByNamesRequest) - campaign, err := svc.NewDistributedQueryCampaignByNames(ctx, req.QuerySQL, req.QueryID, req.Selected.Hosts, req.Selected.Labels) +func createDistributedQueryCampaignByIdentifierEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*createDistributedQueryCampaignByIdentifierRequest) + campaign, err := svc.NewDistributedQueryCampaignByIdentifiers(ctx, req.QuerySQL, req.QueryID, req.Selected.Hosts, req.Selected.Labels) if err != nil { return createDistributedQueryCampaignResponse{Err: err}, nil } return createDistributedQueryCampaignResponse{Campaign: campaign}, nil } -func (svc *Service) NewDistributedQueryCampaignByNames(ctx context.Context, queryString string, queryID *uint, hosts []string, labels []string) (*fleet.DistributedQueryCampaign, error) { +func (svc *Service) NewDistributedQueryCampaignByIdentifiers(ctx context.Context, queryString string, queryID *uint, hostIdentifiers []string, labels []string) (*fleet.DistributedQueryCampaign, error) { vc, ok := viewer.FromContext(ctx) if !ok { return nil, fleet.ErrNoContext } filter := fleet.TeamFilter{User: vc.User, IncludeObserver: true} - hostIDs, err := svc.ds.HostIDsByName(ctx, filter, hosts) + hostIDs, err := svc.ds.HostIDsByIdentifier(ctx, filter, hostIdentifiers) if err != nil { return nil, ctxerr.Wrap(ctx, err, "finding host IDs") } diff --git a/server/service/campaigns_test.go b/server/service/campaigns_test.go index 0ba50729cb..28c51ad863 100644 --- a/server/service/campaigns_test.go +++ b/server/service/campaigns_test.go @@ -82,7 +82,7 @@ func TestLiveQueryAuth(t *testing.T) { ds.HostIDsInTargetsFunc = func(ctx context.Context, filters fleet.TeamFilter, targets fleet.HostTargets) ([]uint, error) { return []uint{1}, nil } - ds.HostIDsByNameFunc = func(ctx context.Context, filter fleet.TeamFilter, names []string) ([]uint, error) { + ds.HostIDsByIdentifierFunc = func(ctx context.Context, filter fleet.TeamFilter, identifiers []string) ([]uint, error) { return nil, nil } ds.LabelIDsByNameFunc = func(ctx context.Context, names []string) (map[string]uint, error) { @@ -225,13 +225,13 @@ func TestLiveQueryAuth(t *testing.T) { // tests with a team target cannot run the "ByNames" calls, as there's no way // to pass a team target with this call. if tt.teamID == nil { - _, err = svc.NewDistributedQueryCampaignByNames(ctx, query1ObsCanRun.Query, nil, nil, nil) + _, err = svc.NewDistributedQueryCampaignByIdentifiers(ctx, query1ObsCanRun.Query, nil, nil, nil) checkAuthErr(t, tt.shouldFailRunNew, err) - _, err = svc.NewDistributedQueryCampaignByNames(ctx, query1ObsCanRun.Query, ptr.Uint(query1ObsCanRun.ID), nil, nil) + _, err = svc.NewDistributedQueryCampaignByIdentifiers(ctx, query1ObsCanRun.Query, ptr.Uint(query1ObsCanRun.ID), nil, nil) checkAuthErr(t, tt.shouldFailRunObsCan, err) - _, err = svc.NewDistributedQueryCampaignByNames(ctx, query2ObsCannotRun.Query, ptr.Uint(query2ObsCannotRun.ID), nil, nil) + _, err = svc.NewDistributedQueryCampaignByIdentifiers(ctx, query2ObsCannotRun.Query, ptr.Uint(query2ObsCannotRun.ID), nil, nil) checkAuthErr(t, tt.shouldFailRunObsCannot, err) } }) diff --git a/server/service/client_live_query.go b/server/service/client_live_query.go index eb0a69197e..6b7fe54605 100644 --- a/server/service/client_live_query.go +++ b/server/service/client_live_query.go @@ -62,19 +62,19 @@ func (h *LiveQueryResultsHandler) Status() *campaignStatus { } // LiveQuery creates a new live query and begins streaming results. -func (c *Client) LiveQuery(query string, queryID *uint, labels []string, hosts []string) (*LiveQueryResultsHandler, error) { - return c.LiveQueryWithContext(context.Background(), query, queryID, labels, hosts) +func (c *Client) LiveQuery(query string, queryID *uint, labels []string, hostIdentifiers []string) (*LiveQueryResultsHandler, error) { + return c.LiveQueryWithContext(context.Background(), query, queryID, labels, hostIdentifiers) } func (c *Client) LiveQueryWithContext( - ctx context.Context, query string, queryID *uint, labels []string, hosts []string, + ctx context.Context, query string, queryID *uint, labels []string, hostIdentifiers []string, ) (*LiveQueryResultsHandler, error) { - req := createDistributedQueryCampaignByNamesRequest{ + req := createDistributedQueryCampaignByIdentifierRequest{ QueryID: queryID, QuerySQL: query, - Selected: distributedQueryCampaignTargetsByNames{Labels: labels, Hosts: hosts}, + Selected: distributedQueryCampaignTargetsByIdentifiers{Labels: labels, Hosts: hostIdentifiers}, } - verb, path := "POST", "/api/latest/fleet/queries/run_by_names" + verb, path := "POST", "/api/latest/fleet/queries/run_by_identifiers" var responseBody createDistributedQueryCampaignResponse err := c.authenticatedRequest(req, verb, path, &responseBody) if err != nil { diff --git a/server/service/client_live_query_test.go b/server/service/client_live_query_test.go index f7933fe1cd..f797c025b6 100644 --- a/server/service/client_live_query_test.go +++ b/server/service/client_live_query_test.go @@ -20,7 +20,7 @@ func TestLiveQueryWithContext(t *testing.T) { upgrader := websocket.Upgrader{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/api/latest/fleet/queries/run_by_names": + case "/api/latest/fleet/queries/run_by_identifiers": resp := createDistributedQueryCampaignResponse{ Campaign: &fleet.DistributedQueryCampaign{ UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ diff --git a/server/service/handler.go b/server/service/handler.go index c7892d7306..66709d72f5 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -428,7 +428,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // The live queries are created with these two endpoints and their results can be queried via // websockets via the `GET /api/_version_/fleet/results/` endpoint. ue.POST("/api/_version_/fleet/queries/run", createDistributedQueryCampaignEndpoint, createDistributedQueryCampaignRequest{}) - ue.POST("/api/_version_/fleet/queries/run_by_names", createDistributedQueryCampaignByNamesEndpoint, createDistributedQueryCampaignByNamesRequest{}) + ue.POST("/api/_version_/fleet/queries/run_by_identifiers", createDistributedQueryCampaignByIdentifierEndpoint, createDistributedQueryCampaignByIdentifierRequest{}) + // This endpoint is deprecated and maintained for backwards compatibility. This and above endpoint are functionally equivalent + ue.POST("/api/_version_/fleet/queries/run_by_names", createDistributedQueryCampaignByIdentifierEndpoint, createDistributedQueryCampaignByIdentifierRequest{}) ue.GET("/api/_version_/fleet/activities", listActivitiesEndpoint, listActivitiesRequest{}) diff --git a/server/service/integration_live_queries_test.go b/server/service/integration_live_queries_test.go index 944ea650bf..b5ba57ae68 100644 --- a/server/service/integration_live_queries_test.go +++ b/server/service/integration_live_queries_test.go @@ -999,14 +999,14 @@ func (s *liveQueriesTestSuite) TestCreateDistributedQueryCampaign() { // wait to prevent duplicate name for new query time.Sleep(200 * time.Millisecond) - // create by host name - req2 := createDistributedQueryCampaignByNamesRequest{ + req2 := createDistributedQueryCampaignByIdentifierRequest{ QuerySQL: "SELECT 3", - Selected: distributedQueryCampaignTargetsByNames{ + Selected: distributedQueryCampaignTargetsByIdentifiers{ Hosts: []string{h1.Hostname}, }, } - s.DoJSON("POST", "/api/latest/fleet/queries/run_by_names", req2, http.StatusOK, &createResp) + + s.DoJSON("POST", "/api/latest/fleet/queries/run_by_identifiers", req2, http.StatusOK, &createResp) assert.NotEqual(t, camp1.ID, createResp.Campaign.ID) assert.Equal(t, uint(1), createResp.Campaign.Metrics.TotalHosts) @@ -1014,13 +1014,13 @@ func (s *liveQueriesTestSuite) TestCreateDistributedQueryCampaign() { time.Sleep(200 * time.Millisecond) // create by unknown host name - it ignores the unknown names. Must have at least 1 valid host - req2 = createDistributedQueryCampaignByNamesRequest{ + req2 = createDistributedQueryCampaignByIdentifierRequest{ QuerySQL: "SELECT 3", - Selected: distributedQueryCampaignTargetsByNames{ + Selected: distributedQueryCampaignTargetsByIdentifiers{ Hosts: []string{h1.Hostname, h2.Hostname + "ZZZZZ"}, }, } - s.DoJSON("POST", "/api/latest/fleet/queries/run_by_names", req2, http.StatusOK, &createResp) + s.DoJSON("POST", "/api/latest/fleet/queries/run_by_identifiers", req2, http.StatusOK, &createResp) } func (s *liveQueriesTestSuite) TestOsqueryDistributedRead() {