diff --git a/cmd/fleetctl/fleetctl/flags.go b/cmd/fleetctl/fleetctl/flags.go index 0050d95ced..847cce2952 100644 --- a/cmd/fleetctl/fleetctl/flags.go +++ b/cmd/fleetctl/fleetctl/flags.go @@ -71,3 +71,10 @@ func byMDMCommandRequestType() cli.Flag { Usage: "Filter MDM commands by type.", } } + +func withMDMCommandStatusFilter() cli.Flag { + return &cli.StringFlag{ + Name: "command_status", + Usage: "Filter MDM commands by command status in a comma-separated list. Valid values are 'pending', 'ran', and 'failed'. ", + } +} diff --git a/cmd/fleetctl/fleetctl/get.go b/cmd/fleetctl/fleetctl/get.go index 0a7dca4637..771a21cfe6 100644 --- a/cmd/fleetctl/fleetctl/get.go +++ b/cmd/fleetctl/fleetctl/get.go @@ -1473,6 +1473,7 @@ func getMDMCommandResultsCommand() *cli.Command { Usage: "Filter MDM commands by ID.", Required: true, }, + byHostIdentifier(), }, Action: func(c *cli.Context) error { client, err := clientFromCLI(c) @@ -1485,10 +1486,10 @@ func getMDMCommandResultsCommand() *cli.Command { return err } - res, err := client.MDMGetCommandResults(c.String("id")) + res, err := client.MDMGetCommandResults(c.String("id"), c.String("host")) if err != nil { var nfe service.NotFoundErr - if errors.As(err, &nfe) { + if errors.As(err, &nfe) && c.String("host") == "" { return errors.New("The command doesn't exist. Please provide a valid command ID. To see a list of commands that were run, run `fleetctl get mdm-commands`.") } @@ -1569,6 +1570,7 @@ func getMDMCommandsCommand() *cli.Command { debugFlag(), byHostIdentifier(), byMDMCommandRequestType(), + withMDMCommandStatusFilter(), }, Action: func(c *cli.Context) error { client, err := clientFromCLI(c) @@ -1581,10 +1583,18 @@ func getMDMCommandsCommand() *cli.Command { return err } + commandStatuses := []fleet.MDMCommandStatusFilter{} + if c.IsSet("command_status") { + for val := range strings.SplitSeq(c.String("command_status"), ",") { + commandStatuses = append(commandStatuses, fleet.MDMCommandStatusFilter(val)) + } + } + opts := fleet.MDMCommandListOptions{ Filters: fleet.MDMCommandFilters{ - HostIdentifier: c.String("host"), - RequestType: c.String("type"), + HostIdentifier: c.String("host"), + RequestType: c.String("type"), + CommandStatuses: commandStatuses, }, } @@ -1595,7 +1605,13 @@ func getMDMCommandsCommand() *cli.Command { } return err } - if len(results) == 0 && opts.Filters.HostIdentifier == "" && opts.Filters.RequestType == "" { + + if len(results) == 0 { + if opts.Filters.HostIdentifier != "" { + log(c, "No MDM commands have been run on this host.\n") + return nil + } + log(c, "You haven't run any MDM commands. Run MDM commands with the `fleetctl mdm run-command` command.\n") return nil } diff --git a/cmd/fleetctl/fleetctl/get_test.go b/cmd/fleetctl/fleetctl/get_test.go index 115ad0465a..4bc67d1cad 100644 --- a/cmd/fleetctl/fleetctl/get_test.go +++ b/cmd/fleetctl/fleetctl/get_test.go @@ -2593,6 +2593,18 @@ func TestGetMDMCommandResults(t *testing.T) { {ID: 2, UUID: uuids[1], Hostname: "host2"}, }, nil } + ds.GetHostMDMIdentifiersFunc = func(ctx context.Context, identifer string, teamFilter fleet.TeamFilter) ([]*fleet.HostMDMIdentifiers, error) { + return []*fleet.HostMDMIdentifiers{ + { + UUID: "device1", + HardwareSerial: "C02XXXXXXX1", + Hostname: "host1", + ID: 1, + TeamID: ptr.Uint(1), + Platform: "darwin", + }, + }, nil + } ds.GetMDMAppleCommandResultsFunc = func(ctx context.Context, commandUUID string, hostUUID string) ([]*fleet.MDMCommandResult, error) { switch commandUUID { case "empty-cmd": @@ -3000,6 +3012,66 @@ RESULTS: ds.GetMDMWindowsCommandResultsFuncInvoked = false require.False(t, ds.GetMDMAppleCommandResultsFuncInvoked) }) + + t.Run("host specific results", func(t *testing.T) { + expectedOutput := strings.TrimSpace(` +ID: +valid-cmd + +TIME: +2023-04-04T15:29:00Z + +TYPE: +test + +STATUS: +Acknowledged + +HOSTNAME: +host1 + +PAYLOAD: + + + + + Command + + ManagedOnly + + RequestType + ProfileList + + CommandUUID + 0001_ProfileList + + + + +RESULTS: + + + + + CommandUUID + 6d7cb698-8d93-45a3-b544-71aef37d42e8 + Status + Acknowledged + UDID + 419D46EC-06E6-557C-AD52-601BA0667730 + +`) + + platform = "darwin" + buf, err := RunAppNoChecks([]string{"get", "mdm-command-results", "--id", "valid-cmd", "--host", "device1"}) + require.NoError(t, err) + require.Contains(t, buf.String(), expectedOutput) + require.True(t, ds.GetMDMCommandPlatformFuncInvoked) + ds.GetMDMCommandPlatformFuncInvoked = false + require.False(t, ds.GetMDMWindowsCommandResultsFuncInvoked) + require.True(t, ds.GetMDMAppleCommandResultsFuncInvoked) + ds.GetMDMAppleCommandResultsFuncInvoked = false + }) } func TestGetMDMCommands(t *testing.T) { @@ -3008,6 +3080,14 @@ func TestGetMDMCommands(t *testing.T) { ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil } + ds.HostLiteByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.HostLite, error) { + fmt.Println("Called", identifier) + if identifier == "foo" || identifier == "h1" { + return &fleet.HostLite{ID: 1, UUID: "h1", Hostname: "host1"}, nil + } + return nil, errors.New(fleet.HostIdentiferNotFound) + } + var empty bool var listErr error var noHostErr error @@ -3029,7 +3109,7 @@ func TestGetMDMCommands(t *testing.T) { if expectRequestType { require.NotEmpty(t, listOpts.Filters.RequestType) } - + fmt.Println("Returning commands") return []*fleet.MDMCommand{ { HostUUID: "h1", @@ -3123,6 +3203,20 @@ The list of 3 most recent commands: _, err = RunAppNoChecks([]string{"get", "mdm-commands", "--host", "foo"}) require.Error(t, err) require.ErrorContains(t, err, fleet.HostIdentiferNotFound) + + // Empty results when using host identifier + listErr = nil + empty = true + expectIdentifier = true + noHostErr = nil + buf, err = RunAppNoChecks([]string{"get", "mdm-commands", "--host", "foo"}) + require.NoError(t, err) + require.Contains(t, buf.String(), "No MDM commands have been run on this host.") + + // Command status no host + _, err = RunAppNoChecks([]string{"get", "mdm-commands", "--command_status", "ran"}) + require.Error(t, err) + require.ErrorContains(t, err, `"host_identifier" must be specified when filtering by "command_status"`) } func TestUserIsObserver(t *testing.T) { diff --git a/server/service/client_mdm.go b/server/service/client_mdm.go index 4f41f1b57d..3429d93c1b 100644 --- a/server/service/client_mdm.go +++ b/server/service/client_mdm.go @@ -283,7 +283,7 @@ func (c *Client) deleteMacOSSetupAssistant(teamID *uint) error { func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDMCommand, error) { const defaultCommandsPerPage = 20 - verb, path := http.MethodGet, "/api/latest/fleet/mdm/commands" + verb, path := http.MethodGet, "/api/latest/fleet/commands" query := url.Values{} query.Set("per_page", fmt.Sprint(defaultCommandsPerPage)) @@ -292,6 +292,15 @@ func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDM query.Set("host_identifier", opts.Filters.HostIdentifier) query.Set("request_type", opts.Filters.RequestType) + var statuses []string + if len(opts.Filters.CommandStatuses) > 0 { + statuses = make([]string, 0, len(opts.Filters.CommandStatuses)) + for _, s := range opts.Filters.CommandStatuses { + statuses = append(statuses, string(s)) + } + } + query.Set("command_status", strings.Join(statuses, ",")) + var responseBody listMDMCommandsResponse err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query.Encode()) if err != nil { @@ -301,11 +310,12 @@ func (c *Client) MDMListCommands(opts fleet.MDMCommandListOptions) ([]*fleet.MDM return responseBody.Results, nil } -func (c *Client) MDMGetCommandResults(commandUUID string) ([]*fleet.MDMCommandResult, error) { - verb, path := http.MethodGet, "/api/latest/fleet/mdm/commandresults" +func (c *Client) MDMGetCommandResults(commandUUID, hostIdentifier string) ([]*fleet.MDMCommandResult, error) { + verb, path := http.MethodGet, "/api/latest/fleet/commands/results" query := url.Values{} query.Set("command_uuid", commandUUID) + query.Set("host_identifier", hostIdentifier) var responseBody getMDMCommandResultsResponse err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query.Encode()) diff --git a/server/service/mdm.go b/server/service/mdm.go index a7c7c3aa36..7452b92c8b 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1060,7 +1060,6 @@ func (svc *Service) ListMDMCommands(ctx context.Context, opts *fleet.MDMCommandL return nil, nil, fleet.NewInvalidArgumentError("Invalid Host", fleet.HostIdentiferNotFound).WithStatus(http.StatusNotFound) } } - return results, total, nil }