diff --git a/changes/19447-ios-ipados-software b/changes/19447-ios-ipados-software new file mode 100644 index 0000000000..755f37b0c6 --- /dev/null +++ b/changes/19447-ios-ipados-software @@ -0,0 +1 @@ +- iOS and iPadOS device details refetch can now be triggered with the existing `POST /api/latest/fleet/hosts/:id/refetch` endpoint. diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 4e8d547fe4..03d3621b19 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -1305,28 +1305,7 @@ func newIPhoneIPadRefetcher( } logger.Log("msg", "sending commands to refetch", "count", len(uuids), "lookup-duration", time.Since(start)) commandUUID := fleet.RefetchCommandUUIDPrefix + uuid.NewString() - if err := commander.EnqueueCommand(ctx, uuids, fmt.Sprintf(` - - - - Command - - Queries - - DeviceName - DeviceCapacity - AvailableDeviceCapacity - OSVersion - WiFiMAC - ProductName - - RequestType - DeviceInformation - - CommandUUID - %s - -`, commandUUID)); err != nil { + if err := commander.DeviceInformation(ctx, uuids, commandUUID); err != nil { return ctxerr.Wrap(ctx, err, "send DeviceInformation commands to ios and ipados devices") } return nil diff --git a/server/mdm/apple/commander.go b/server/mdm/apple/commander.go index 43ea5d7143..7c246e0743 100644 --- a/server/mdm/apple/commander.go +++ b/server/mdm/apple/commander.go @@ -273,6 +273,33 @@ func (svc *MDMAppleCommander) DeviceConfigured(ctx context.Context, hostUUID, cm return svc.EnqueueCommand(ctx, []string{hostUUID}, raw) } +func (svc *MDMAppleCommander) DeviceInformation(ctx context.Context, hostUUIDs []string, cmdUUID string) error { + raw := fmt.Sprintf(` + + + + Command + + Queries + + DeviceName + DeviceCapacity + AvailableDeviceCapacity + OSVersion + WiFiMAC + ProductName + + RequestType + DeviceInformation + + CommandUUID + %s + +`, cmdUUID) + + return svc.EnqueueCommand(ctx, hostUUIDs, raw) +} + // EnqueueCommand takes care of enqueuing the commands and sending push // notifications to the devices. // diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 5e0d149698..fa32591168 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2752,6 +2752,7 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ host.PrimaryMac = wifiMac host.HardwareModel = productName host.DetailUpdatedAt = time.Now() + host.RefetchRequested = false if err := svc.ds.UpdateHost(r.Context, host); err != nil { return nil, ctxerr.Wrap(r.Context, err, "failed to update host") } diff --git a/server/service/hosts.go b/server/service/hosts.go index 4de8dd091a..18f6831d82 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "encoding/csv" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -30,6 +31,7 @@ import ( "github.com/fleetdm/fleet/v4/server/worker" "github.com/go-kit/log/level" "github.com/gocarina/gocsv" + "github.com/google/uuid" ) // HostDetailResponse is the response struct that contains the full host information @@ -1008,12 +1010,15 @@ func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Ser } func (svc *Service) RefetchHost(ctx context.Context, id uint) error { + var host *fleet.Host + // iOS and iPadOS refetch are not authenticated with device token because these devices do not have Fleet Desktop if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) { - if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { + var err error + if err = svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { return err } - host, err := svc.ds.HostLite(ctx, id) + host, err = svc.ds.HostLite(ctx, id) if err != nil { return ctxerr.Wrap(ctx, err, "find host for refetch") } @@ -1025,6 +1030,17 @@ func (svc *Service) RefetchHost(ctx context.Context, id uint) error { } } + if host != nil && (host.Platform == "ios" || host.Platform == "ipados") { + err := svc.verifyMDMConfiguredAndConnected(ctx, host) + if err != nil { + return err + } + err = svc.mdmAppleCommander.DeviceInformation(ctx, []string{host.UUID}, fleet.RefetchCommandUUIDPrefix+uuid.NewString()) + if err != nil { + return ctxerr.Wrap(ctx, err, "refetch host with MDM") + } + } + if err := svc.ds.UpdateHostRefetchRequested(ctx, id, true); err != nil { return ctxerr.Wrap(ctx, err, "save host") } @@ -1032,6 +1048,24 @@ func (svc *Service) RefetchHost(ctx context.Context, id uint) error { return nil } +func (svc *Service) verifyMDMConfiguredAndConnected(ctx context.Context, host *fleet.Host) error { + if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { + if errors.Is(err, fleet.ErrMDMNotConfigured) { + err = fleet.NewInvalidArgumentError("id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest) + } + return ctxerr.Wrap(ctx, err, "check macOS MDM enabled") + } + connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host) + if err != nil { + return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet") + } + if !connected { + return ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError("id", "Host does not have MDM turned on.")) + } + return nil +} + func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) { if !opts.ExcludeSoftware { if err := svc.ds.LoadHostSoftware(ctx, host, opts.IncludeCVEScores); err != nil { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 3ada175919..eb6bf02ed1 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -771,6 +771,43 @@ func createHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testi return fleetHost, mdmDevice } +func (s *integrationMDMTestSuite) createAppleMobileHostThenEnrollMDM(platform string) (*fleet.Host, *mdmtest.TestAppleMDMClient) { + ctx := context.Background() + t := s.T() + + // create a host with minimal information and the serial, no uuid/osquery id + // (as when created via DEP sync). + dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + serialNumber := mdmtest.RandSerialNumber() + fleetHost, err := s.ds.NewHost(ctx, &fleet.Host{ + HardwareSerial: serialNumber, + Platform: platform, + LastEnrolledAt: dbZeroTime, + DetailUpdatedAt: dbZeroTime, + RefetchRequested: true, + }) + require.NoError(t, err) + require.Equal(t, dbZeroTime, fleetHost.LastEnrolledAt) + + // Perform the MDM enrollment. + mdmEnrollInfo := mdmtest.AppleEnrollInfo{ + SCEPChallenge: s.scepChallenge, + SCEPURL: s.server.URL + apple_mdm.SCEPPath, + MDMURL: s.server.URL + apple_mdm.MDMPath, + } + model := "iPhone14,6" + if platform == "ipados" { + model = "iPad13,18" + } + mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model) + mdmDevice.SerialNumber = serialNumber + err = mdmDevice.Enroll() + require.NoError(t, err) + + return fleetHost, mdmDevice + +} + func createWindowsHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestWindowsMDMClient) { host := createOrbitEnrolledHost(t, "windows", "h1", ds) mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(fleetServerURL, *host.OrbitNodeKey) @@ -9311,37 +9348,56 @@ func (s *integrationMDMTestSuite) TestInvalidCommandUUID() { func (s *integrationMDMTestSuite) TestEnrollAfterDEPSyncIOSIPadOS() { t := s.T() - ctx := context.Background() - // create a host with minimal information and the serial, no uuid/osquery id - // (as when created via DEP sync). - dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) - serialNumber := mdmtest.RandSerialNumber() - h, err := s.ds.NewHost(ctx, &fleet.Host{ - HardwareSerial: serialNumber, - Platform: "ios", - LastEnrolledAt: dbZeroTime, - DetailUpdatedAt: dbZeroTime, - RefetchRequested: true, - }) - require.NoError(t, err) - require.Equal(t, dbZeroTime, h.LastEnrolledAt) - - // Perform the MDM enrollment. - mdmEnrollInfo := mdmtest.AppleEnrollInfo{ - SCEPChallenge: s.scepChallenge, - SCEPURL: s.server.URL + apple_mdm.SCEPPath, - MDMURL: s.server.URL + apple_mdm.MDMPath, - } - mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "iPhone14,6") - mdmDevice.SerialNumber = serialNumber - err = mdmDevice.Enroll() - require.NoError(t, err) + h, _ := s.createAppleMobileHostThenEnrollMDM("ios") // fetch the host, it will match the one created above // (NOTE: cannot check the returned OrbitNodeKey, this field is not part of the response) var hostResp getHostResponse s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResp) require.Equal(t, h.ID, hostResp.Host.ID) - require.NotEqual(t, dbZeroTime, hostResp.Host.LastEnrolledAt) + require.NotEqual(t, h.LastEnrolledAt, hostResp.Host.LastEnrolledAt) + + h, _ = s.createAppleMobileHostThenEnrollMDM("ipados") + + // fetch the host, it will match the one created above + // (NOTE: cannot check the returned OrbitNodeKey, this field is not part of the response) + hostResp = getHostResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResp) + require.Equal(t, h.ID, hostResp.Host.ID) + require.NotEqual(t, h.LastEnrolledAt, hostResp.Host.LastEnrolledAt) + +} + +func (s *integrationMDMTestSuite) TestRefetchIOSIPadOS() { + t := s.T() + + // Try to refetch host that is not MDM enrolled + serialNumber := mdmtest.RandSerialNumber() + fleetHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + HardwareSerial: serialNumber, + Platform: "ipados", + LastEnrolledAt: time.Now(), + DetailUpdatedAt: time.Now(), + RefetchRequested: true, + }) + require.NoError(t, err) + r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", fleetHost.ID), nil, http.StatusUnprocessableEntity, "error", + "host is not enrolled in MDM") + assert.Contains(t, extractServerErrorText(r.Body), "Host does not have MDM turned on") + + // Try to refetch an MDM enrolled host + host, mdmClient := s.createAppleMobileHostThenEnrollMDM("ios") + _ = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", host.ID), nil, http.StatusOK) + + // Check the MDM command + cmd, err := mdmClient.Idle() + require.NoError(t, err) + require.NotNil(t, cmd) + assert.Equal(t, "DeviceInformation", cmd.Command.RequestType) + + var hostResp getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp) + assert.Equal(t, host.ID, hostResp.Host.ID) + assert.True(t, host.RefetchRequested) }