diff --git a/ee/server/service/hosts.go b/ee/server/service/hosts.go index ae752a673d..573ba3da78 100644 --- a/ee/server/service/hosts.go +++ b/ee/server/service/hosts.go @@ -114,6 +114,8 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete.")) case lockWipe.IsPendingWipe(): return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped.")) + case lockWipe.IsWiped(): + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped.")) case lockWipe.IsLocked(): return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict)) } @@ -186,6 +188,8 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error) return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. The host will unlock when it comes online.")) case lockWipe.IsPendingWipe(): return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process unlock requests once host is wiped.")) + case lockWipe.IsWiped(): + return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process unlock requests once host is wiped.")) case lockWipe.IsUnlocked(): return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already unlocked.").WithStatus(http.StatusConflict)) } @@ -275,6 +279,8 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. Host cannot be wiped until unlock is complete.")) case lockWipe.IsPendingWipe(): return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. The host will be wiped when it comes online.")) + case lockWipe.IsLocked(): + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is locked. Host cannot be wiped when it is locked.")) case lockWipe.IsWiped(): return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already wiped.").WithStatus(http.StatusConflict)) } diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 8f81f61e03..d4564b05c5 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -653,6 +653,11 @@ func updateMDMAppleHostDB( return ctxerr.Wrap(ctx, err, "update mdm apple host") } + // clear any host_mdm_actions following re-enrollment here + if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, hostID); err != nil { + return ctxerr.Wrap(ctx, err, "error clearing mdm apple host_mdm_actions") + } + if err := upsertMDMAppleHostMDMInfoDB(ctx, tx, appCfg.ServerSettings, false, hostID); err != nil { return ctxerr.Wrap(ctx, err, "ingest mdm apple host upsert MDM info") } diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 0012f941f4..d2b118bb9c 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1506,9 +1506,9 @@ func filterHostsByVulnerability(sqlstmt string, opt fleet.HostListOptions, param SELECT hs.host_id FROM host_software hs JOIN software_cve sc ON sc.software_id = hs.software_id WHERE sc.cve = ? - + UNION - + SELECT hos.host_id FROM host_operating_system hos JOIN operating_system_vulnerabilities osv ON osv.operating_system_id = hos.os_id WHERE osv.cve = ?)` @@ -1778,6 +1778,11 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf } host.ID = hostID + // clear any host_mdm_actions following re-enrollment here + if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, hostID); err != nil { + return ctxerr.Wrap(ctx, err, "orbit enroll error clearing host_mdm_actions") + } + case errors.Is(err, sql.ErrNoRows): zeroTime := time.Unix(0, 0).Add(24 * time.Hour) // Create new host record. We always create newly enrolled hosts with refetch_requested = true @@ -1900,6 +1905,11 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH return ctxerr.Wrap(ctx, err, "cleanup policy membership on re-enroll") } + // clear any host_mdm_actions following re-enrollment here + if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm_actions WHERE host_id = ?`, matchedID); err != nil { + return ctxerr.Wrap(ctx, err, "error clearing host_mdm_actions") + } + // Update existing host record sqlUpdate := ` UPDATE hosts diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 5bc00a5fc8..c1c0875186 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11067,6 +11067,10 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeWindowsLinux() { // try to lock the host again s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusConflict) + // try to wipe a locked host + res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Host cannot be wiped when it is locked.") // unlock the host s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusNoContent) @@ -11176,10 +11180,35 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeWindowsLinux() { require.NotNil(t, getHostResp.Host.MDM.PendingAction) require.Equal(t, "", *getHostResp.Host.MDM.PendingAction) + // try to lock/unlock the host fails + res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Cannot process lock requests once host is wiped.") + res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Cannot process unlock requests once host is wiped.") + // try to wipe the host again, conflict (already wiped) s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusConflict) // no activity created s.lastActivityOfTypeMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, host.ID, host.DisplayName()), wipeActID) + + // re-enroll the host, simulating that another user received the wiped host + newOrbitKey := uuid.New().String() + newHost, err := s.ds.EnrollOrbit(ctx, true, fleet.OrbitHostInfo{ + HardwareUUID: *host.OsqueryHostID, + HardwareSerial: host.HardwareSerial, + }, newOrbitKey, nil) + require.NoError(t, err) + // it re-enrolled using the same host record + require.Equal(t, host.ID, newHost.ID) + + // refresh the host's status, it is back to unlocked + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp) + require.NotNil(t, getHostResp.Host.MDM.DeviceStatus) + require.Equal(t, "unlocked", *getHostResp.Host.MDM.DeviceStatus) + require.NotNil(t, getHostResp.Host.MDM.PendingAction) + require.Equal(t, "", *getHostResp.Host.MDM.PendingAction) }) } } @@ -11232,6 +11261,10 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() { // try to lock the host again s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusConflict) + // try to wipe a locked host + res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Host cannot be wiped when it is locked.") // unlock the host unlockResp = unlockHostResponse{} @@ -11304,10 +11337,29 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() { require.NotNil(t, getHostResp.Host.MDM.PendingAction) require.Equal(t, "", *getHostResp.Host.MDM.PendingAction) + // try to lock/unlock the host fails + res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Cannot process lock requests once host is wiped.") + res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Cannot process unlock requests once host is wiped.") + // try to wipe the host again, conflict (already wiped) s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusConflict) // no activity created s.lastActivityOfTypeMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, host.ID, host.DisplayName()), wipeActID) + + // re-enroll the host, simulating that another user received the wiped host + err = mdmClient.Enroll() + require.NoError(t, err) + + // refresh the host's status, it is back to unlocked + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp) + require.NotNil(t, getHostResp.Host.MDM.DeviceStatus) + require.Equal(t, "unlocked", *getHostResp.Host.MDM.DeviceStatus) + require.NotNil(t, getHostResp.Host.MDM.PendingAction) + require.Equal(t, "", *getHostResp.Host.MDM.PendingAction) } func (s *integrationMDMTestSuite) TestZCustomConfigurationWebURL() {