diff --git a/pkg/mdm/mdmtest/apple.go b/pkg/mdm/mdmtest/apple.go
index beda8cd9f6..57e20f7c49 100644
--- a/pkg/mdm/mdmtest/apple.go
+++ b/pkg/mdm/mdmtest/apple.go
@@ -930,6 +930,7 @@ func (c *TestAppleMDMClient) AcknowledgeDeviceInformation(udid, cmdUUID, deviceN
"OSVersion": "17.5.1",
"ProductName": productName,
"WiFiMAC": "ff:ff:ff:ff:ff:ff",
+ "IsMDMLostModeEnabled": false,
},
}
return c.sendAndDecodeCommandResponse(payload)
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index 604d6fa1db..916a9efefe 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -7147,3 +7147,30 @@ WHERE
return &idp, nil
}
+
+func (ds *Datastore) GetLatestAppleMDMCommandOfType(ctx context.Context, hostUUID string, commandType string) (*fleet.MDMCommand, error) {
+ const stmt = `
+ SELECT id as host_uuid, command_uuid, request_type FROM nano_view_queue WHERE id = ? AND request_type = ? ORDER BY created_at DESC LIMIT 1
+ `
+
+ var cmd fleet.MDMCommand
+ if err := sqlx.GetContext(ctx, ds.reader(ctx), &cmd, stmt, hostUUID, commandType); err != nil {
+ if err == sql.ErrNoRows {
+ return nil, notFound("MDMCommand")
+ }
+ return nil, ctxerr.Wrap(ctx, err, "get latest apple mdm command of type")
+ }
+
+ return &cmd, nil
+}
+
+func (ds *Datastore) SetLockCommandForLostModeCheckin(ctx context.Context, hostID uint, commandUUID string) error {
+ // We know we can insert here, as this is only called when processing a
+ // a new iphone/ipad checkin with lost mode enabled.
+ const stmt = `
+ INSERT INTO host_mdm_actions (host_id, lock_ref)
+ VALUES (?, ?)
+ `
+ _, err := ds.writer(ctx).ExecContext(ctx, stmt, hostID, commandUUID)
+ return ctxerr.Wrap(ctx, err, "set lock ref for lost mode checkin")
+}
diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go
index a0c64bae2c..f14616f941 100644
--- a/server/datastore/mysql/apple_mdm_test.go
+++ b/server/datastore/mysql/apple_mdm_test.go
@@ -104,6 +104,8 @@ func TestMDMApple(t *testing.T) {
{"TestDeleteMDMAppleDeclarationWithPendingInstalls", testDeleteMDMAppleDeclarationWithPendingInstalls},
{"TestUpdateNanoMDMUserEnrollmentUsername", testUpdateNanoMDMUserEnrollmentUsername},
{"TestLockUnlockWipeIphone", testLockUnlockWipeIphone},
+ {"TestGetLatestAppleMDMCommandOfType", testGetLatestAppleMDMCommandOfType},
+ {"TestSetLockCommandForLostModeCheckin", testSetLockCommandForLostModeCheckin},
}
for _, c := range cases {
@@ -9483,3 +9485,71 @@ func testUpdateNanoMDMUserEnrollmentUsername(t *testing.T, ds *Datastore) {
require.Equal(t, nanoenroll_username, user1)
require.Equal(t, userUUID1, fetchedUserUUID1)
}
+
+func testGetLatestAppleMDMCommandOfType(t *testing.T, ds *Datastore) {
+ ctx := t.Context()
+
+ // Fails if host does not have a record
+ _, err := ds.GetLatestAppleMDMCommandOfType(ctx, "non-existing-uuid", "DeviceLock")
+ require.Error(t, err)
+ require.True(t, errors.Is(err, sql.ErrNoRows))
+
+ // Nano enroll a single device
+ realHostUUID := uuid.NewString()
+ host := &fleet.Host{
+ UUID: realHostUUID,
+ HardwareSerial: "serial",
+ Platform: "darwin",
+ TeamID: nil,
+ }
+ nanoEnroll(t, ds, host, false)
+
+ // Insert one record
+ deviceLockCommandUUID := uuid.NewString()
+ requestType := "DeviceLock"
+ insertIntoNanoViewQueue(t, ds, host.UUID, deviceLockCommandUUID, requestType)
+
+ // Fails if host does exist but not request type
+ _, err = ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, "EnableLostMode")
+ require.Error(t, err)
+ require.True(t, errors.Is(err, sql.ErrNoRows))
+
+ // Succeeds if host and request type exist
+ cmd, err := ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, requestType)
+ require.NoError(t, err)
+ require.Equal(t, deviceLockCommandUUID, cmd.CommandUUID)
+ require.Equal(t, requestType, cmd.RequestType)
+}
+
+// insertIntoNanoViewQueue is a helper function that populates the entries that nano_view_queue is made up of.
+func insertIntoNanoViewQueue(t *testing.T, ds *Datastore, hostUUID, commandUUID, requestType string) {
+ ctx := t.Context()
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ // Insert into nano_commands
+ _, err := q.ExecContext(ctx, `INSERT INTO nano_commands (command_uuid, request_type, command, subtype) VALUES (?, ?, 'OSVersion
WiFiMAC
ProductName
+ IsMDMLostModeEnabled
RequestType
DeviceInformation
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index eacb9ef4f4..5b2c74c4e2 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -1453,6 +1453,10 @@ type ListHostMDMAndroidProfilesPendingInstallWithVersionFunc func(ctx context.Co
type GetAndroidPolicyRequestByUUIDFunc func(ctx context.Context, requestUUID string) (*fleet.MDMAndroidPolicyRequest, error)
+type GetLatestAppleMDMCommandOfTypeFunc func(ctx context.Context, hostUUID string, commandType string) (*fleet.MDMCommand, error)
+
+type SetLockCommandForLostModeCheckinFunc func(ctx context.Context, hostID uint, commandUUID string) error
+
type NewMDMAndroidConfigProfileFunc func(ctx context.Context, cp fleet.MDMAndroidConfigProfile) (*fleet.MDMAndroidConfigProfile, error)
type GetMDMAndroidConfigProfileFunc func(ctx context.Context, profileUUID string) (*fleet.MDMAndroidConfigProfile, error)
@@ -3695,6 +3699,12 @@ type DataStore struct {
GetAndroidPolicyRequestByUUIDFunc GetAndroidPolicyRequestByUUIDFunc
GetAndroidPolicyRequestByUUIDFuncInvoked bool
+ GetLatestAppleMDMCommandOfTypeFunc GetLatestAppleMDMCommandOfTypeFunc
+ GetLatestAppleMDMCommandOfTypeFuncInvoked bool
+
+ SetLockCommandForLostModeCheckinFunc SetLockCommandForLostModeCheckinFunc
+ SetLockCommandForLostModeCheckinFuncInvoked bool
+
NewMDMAndroidConfigProfileFunc NewMDMAndroidConfigProfileFunc
NewMDMAndroidConfigProfileFuncInvoked bool
@@ -8847,6 +8857,20 @@ func (s *DataStore) GetAndroidPolicyRequestByUUID(ctx context.Context, requestUU
return s.GetAndroidPolicyRequestByUUIDFunc(ctx, requestUUID)
}
+func (s *DataStore) GetLatestAppleMDMCommandOfType(ctx context.Context, hostUUID string, commandType string) (*fleet.MDMCommand, error) {
+ s.mu.Lock()
+ s.GetLatestAppleMDMCommandOfTypeFuncInvoked = true
+ s.mu.Unlock()
+ return s.GetLatestAppleMDMCommandOfTypeFunc(ctx, hostUUID, commandType)
+}
+
+func (s *DataStore) SetLockCommandForLostModeCheckin(ctx context.Context, hostID uint, commandUUID string) error {
+ s.mu.Lock()
+ s.SetLockCommandForLostModeCheckinFuncInvoked = true
+ s.mu.Unlock()
+ return s.SetLockCommandForLostModeCheckinFunc(ctx, hostID, commandUUID)
+}
+
func (s *DataStore) NewMDMAndroidConfigProfile(ctx context.Context, cp fleet.MDMAndroidConfigProfile) (*fleet.MDMAndroidConfigProfile, error) {
s.mu.Lock()
s.NewMDMAndroidConfigProfileFuncInvoked = true
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index 96e9261ef3..f75bca354d 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -4079,6 +4079,7 @@ func (svc *MDMAppleCheckinAndCommandService) handleRefetchDeviceResults(ctx cont
wifiMac = wifiMacVal.(string)
}
productName := deviceInformationResponse.QueryResponses["ProductName"].(string)
+ isLostModeEnabled := deviceInformationResponse.QueryResponses["IsMDMLostModeEnabled"].(bool)
host.ComputerName = deviceName
host.Hostname = deviceName
host.GigsDiskSpaceAvailable = availableDeviceCapacity
@@ -4120,7 +4121,28 @@ func (svc *MDMAppleCheckinAndCommandService) handleRefetchDeviceResults(ctx cont
if err := svc.ds.UpdateMDMData(ctx, host.ID, true); err != nil {
return nil, ctxerr.Wrap(ctx, err, "failed to update MDM data")
}
+
+ // We run this check here as we only want to run it on re-check ins for deleted hosts.
+ if (platform == "ios" || platform == "ipados") && isLostModeEnabled {
+ fmt.Println("===lost mode enabled on iPhone/iPad, checking for lock command record", host.UUID)
+ cmd, err := svc.ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, "EnableLostMode")
+ if err != nil && !fleet.IsNotFound(err) {
+ return nil, ctxerr.Wrap(ctx, err, "check for existing EnableLostMode command")
+ }
+ if fleet.IsNotFound(err) {
+ // Device is in lost mode, but we do not have a lock command record for it.
+ // Lost mode was enabled outside of Fleet?
+ return nil, ctxerr.NewWithData(ctx, "device is in lost mode but no EnableLostMode command record found", map[string]interface{}{"host_uuid": host.UUID})
+ }
+
+ level.Debug(svc.logger).Log("msg", "device is in lost mode and EnableLostMode command record found, updating host lock/wipe status", "host_uuid", host.UUID)
+ err = svc.ds.SetLockCommandForLostModeCheckin(ctx, host.ID, cmd.CommandUUID)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "update host lost mode status on refetch")
+ }
+ }
}
+
return nil, nil
}
diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go
index e81ddc9bf6..4caf6a2646 100644
--- a/server/service/apple_mdm_test.go
+++ b/server/service/apple_mdm_test.go
@@ -4479,14 +4479,18 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
hostID := uint(42)
hostUUID := "ABC-DEF-GHI"
commandUUID := fleet.RefetchDeviceCommandUUIDPrefix + "UUID"
+ lostModeCommandUUID := uuid.NewString()
ds := new(mock.Store)
- svc := MDMAppleCheckinAndCommandService{ds: ds}
+ svc := MDMAppleCheckinAndCommandService{ds: ds, logger: kitlog.NewNopLogger()}
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
return &fleet.Host{
ID: hostID,
UUID: hostUUID,
+ MDM: fleet.MDMHostData{
+ EnrollmentStatus: ptr.String("Pending"), // We check it in as a new device, to trigger lost mode flow
+ },
}, nil
}
ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error {
@@ -4498,15 +4502,15 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
require.WithinDuration(t, time.Now(), host.DetailUpdatedAt, 1*time.Minute)
return nil
}
- ds.SetOrUpdateHostDisksSpaceFunc = func(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error {
- require.Equal(t, hostID, hostID)
+ ds.SetOrUpdateHostDisksSpaceFunc = func(ctx context.Context, incomingHostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error {
+ require.Equal(t, hostID, incomingHostID)
require.NotZero(t, 51, int64(gigsAvailable))
require.NotZero(t, 79, int64(percentAvailable))
require.NotZero(t, 64, int64(gigsTotal))
return nil
}
- ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error {
- require.Equal(t, hostID, hostID)
+ ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, incomingHostID uint, hostOS fleet.OperatingSystem) error {
+ require.Equal(t, hostID, incomingHostID)
require.Equal(t, "iPadOS", hostOS.Name)
require.Equal(t, "17.5.1", hostOS.Version)
require.Equal(t, "ipados", hostOS.Platform)
@@ -4517,6 +4521,22 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
assert.Equal(t, fleet.RefetchDeviceCommandUUIDPrefix, command.CommandType)
return nil
}
+ ds.UpdateMDMDataFunc = func(ctx context.Context, incomingHostID uint, enrolled bool) error {
+ require.Equal(t, hostID, incomingHostID)
+ return nil
+ }
+ ds.GetLatestAppleMDMCommandOfTypeFunc = func(ctx context.Context, incomingHostUUID, commandType string) (*fleet.MDMCommand, error) {
+ require.Equal(t, hostUUID, incomingHostUUID)
+ require.Equal(t, "EnableLostMode", commandType)
+ return &fleet.MDMCommand{
+ CommandUUID: lostModeCommandUUID,
+ }, nil
+ }
+ ds.SetLockCommandForLostModeCheckinFunc = func(ctx context.Context, incomingHostUUID uint, commandUUID string) error {
+ require.Equal(t, hostID, incomingHostUUID)
+ require.Equal(t, lostModeCommandUUID, commandUUID)
+ return nil
+ }
_, err := svc.CommandAndReportResults(
&mdm.Request{Context: ctx},
@@ -4543,6 +4563,8 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
iPad13,18
WiFiMAC
ff:ff:ff:ff:ff:ff
+ IsMDMLostModeEnabled
+
Status
Acknowledged
@@ -4559,6 +4581,9 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
require.True(t, ds.SetOrUpdateHostDisksSpaceFuncInvoked)
require.True(t, ds.UpdateHostOperatingSystemFuncInvoked)
assert.True(t, ds.RemoveHostMDMCommandFuncInvoked)
+ require.True(t, ds.UpdateMDMDataFuncInvoked)
+ require.True(t, ds.GetLatestAppleMDMCommandOfTypeFuncInvoked)
+ require.True(t, ds.SetLockCommandForLostModeCheckinFuncInvoked)
}
func TestUnmarshalAppList(t *testing.T) {