From 7f5fc14f59961a0dc9f2f8e18748e2444e4610bb Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 14 May 2025 09:38:53 -0400 Subject: [PATCH] Immediately ask for a host refetch when a host re-enrolls and reuses an existing host row (#29081) --- ...x-chrome-profiles-not-reset-after-reenroll | 1 + server/datastore/mysql/hosts.go | 16 ++-- server/fleet/hosts.go | 7 +- server/service/integration_core_test.go | 81 +++++++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 changes/27867-fix-chrome-profiles-not-reset-after-reenroll diff --git a/changes/27867-fix-chrome-profiles-not-reset-after-reenroll b/changes/27867-fix-chrome-profiles-not-reset-after-reenroll new file mode 100644 index 0000000000..2ff69b20c5 --- /dev/null +++ b/changes/27867-fix-chrome-profiles-not-reset-after-reenroll @@ -0,0 +1 @@ +* Fixed a bug where a host that was wiped and re-enrolled without deleting the corresponding host row in Fleet had its old Google Chrome profiles (and other osquery-based data) showing for about an hour. diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 7622ec7949..1534e8a906 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1987,6 +1987,7 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf "host_id", enrolledHostInfo.ID, ) } + refetchRequested := fleet.PlatformSupportsOsquery(enrolledHostInfo.Platform) sqlUpdate := ` UPDATE @@ -1996,9 +1997,10 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf uuid = COALESCE(NULLIF(uuid, ''), ?), osquery_host_id = COALESCE(NULLIF(osquery_host_id, ''), ?), hardware_serial = COALESCE(NULLIF(hardware_serial, ''), ?), - computer_name = COALESCE(NULLIF(computer_name, ''), ?), - hardware_model = COALESCE(NULLIF(hardware_model, ''), ?), - team_id = ? + computer_name = COALESCE(NULLIF(computer_name, ''), ?), + hardware_model = COALESCE(NULLIF(hardware_model, ''), ?), + team_id = ?, + refetch_requested = ? WHERE id = ?` _, err := tx.ExecContext(ctx, sqlUpdate, orbitNodeKey, @@ -2008,6 +2010,7 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf hostInfo.ComputerName, hostInfo.HardwareModel, teamID, + refetchRequested, enrolledHostInfo.ID, ) if err != nil { @@ -2171,6 +2174,8 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH return ctxerr.Wrap(ctx, err, "error clearing host_mdm_actions") } + refetchRequested := fleet.PlatformSupportsOsquery(enrolledHostInfo.Platform) + // Update existing host record sqlUpdate := ` UPDATE hosts @@ -2179,10 +2184,11 @@ func (ds *Datastore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryH last_enrolled_at = NOW(), osquery_host_id = ?, uuid = COALESCE(NULLIF(uuid, ''), ?), - hardware_serial = COALESCE(NULLIF(hardware_serial, ''), ?) + hardware_serial = COALESCE(NULLIF(hardware_serial, ''), ?), + refetch_requested = ? WHERE id = ? ` - _, err := tx.ExecContext(ctx, sqlUpdate, nodeKey, teamID, osqueryHostID, hardwareUUID, hardwareSerial, enrolledHostInfo.ID) + _, err := tx.ExecContext(ctx, sqlUpdate, nodeKey, teamID, osqueryHostID, hardwareUUID, hardwareSerial, refetchRequested, enrolledHostInfo.ID) if err != nil { return ctxerr.Wrap(ctx, err, "update host") } diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 0c30ddee4d..930b3bd327 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -862,7 +862,12 @@ func (h *Host) FleetPlatform() string { // SupportsOsquery returns whether the device runs osquery. func (h *Host) SupportsOsquery() bool { - return h.Platform != "ios" && h.Platform != "ipados" && h.Platform != "android" + return PlatformSupportsOsquery(h.Platform) +} + +// PlatformSupportsOsquery returns whether osquery is supported on this platform. +func PlatformSupportsOsquery(platform string) bool { + return platform != "ios" && platform != "ipados" && platform != "android" } // HostLinuxOSs are the possible linux values for Host.Platform. diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index f1b1f7d1d9..6d4251df6b 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -13260,3 +13260,84 @@ func (s *integrationTestSuite) TestHostCertificates() { }) } } + +func (s *integrationTestSuite) TestHostReenrollWithSameHostRowRefetchOsquery() { + t := s.T() + + // create a mac, linux and windows host + host1 := createOrbitEnrolledHost(t, "darwin", "host1", s.ds) + host2 := createOrbitEnrolledHost(t, "linux", "host2", s.ds) + host3 := createOrbitEnrolledHost(t, "windows", "host3", s.ds) + + // set a chrome profile for each host + for i, h := range []*fleet.Host{host1, host2, host3} { + distributedReq := submitDistributedQueryResultsRequestShim{ + NodeKey: *h.NodeKey, + Results: map[string]json.RawMessage{ + hostDetailQueryPrefix + "google_chrome_profiles": json.RawMessage(fmt.Sprintf( + `[{"email": "%s"}]`, fmt.Sprintf("user%d@example.com", i))), + }, + Statuses: map[string]interface{}{ + hostDistributedQueryPrefix + "google_chrome_profiles": 0, + }, + Messages: map[string]string{}, + Stats: map[string]*fleet.Stats{}, + } + distributedResp := submitDistributedQueryResultsResponse{} + s.DoJSON("POST", "/api/osquery/distributed/write", distributedReq, http.StatusOK, &distributedResp) + } + + oldHosts := make([]fleet.Host, 3) + for i, h := range []*fleet.Host{host1, host2, host3} { + var hostResponse getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResponse) + require.False(t, hostResponse.Host.RefetchRequested) + require.Len(t, hostResponse.Host.EndUsers, 1) + require.Len(t, hostResponse.Host.EndUsers[0].OtherEmails, 1) + require.Equal(t, hostResponse.Host.EndUsers[0].OtherEmails[0].Source, "google_chrome_profiles") + oldHosts[i] = hostResponse.Host.Host + } + + // do an orbit re-enrollment of the hosts, should set refetch requested + orbitKey := setOrbitEnrollment(t, host1, s.ds) + host1.OrbitNodeKey = &orbitKey + orbitKey = setOrbitEnrollment(t, host2, s.ds) + host2.OrbitNodeKey = &orbitKey + orbitKey = setOrbitEnrollment(t, host3, s.ds) + host3.OrbitNodeKey = &orbitKey + + for i, h := range []*fleet.Host{host1, host2, host3} { + var hostResponse getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResponse) + require.True(t, hostResponse.Host.RefetchRequested) + require.Len(t, hostResponse.Host.EndUsers, 1) + require.Len(t, hostResponse.Host.EndUsers[0].OtherEmails, 1) + require.Equal(t, hostResponse.Host.EndUsers[0].OtherEmails[0].Source, "google_chrome_profiles") + require.Equal(t, oldHosts[i].ID, h.ID) + } + + // send a response for the refetch request + for _, h := range []*fleet.Host{host1, host2, host3} { + distributedReq := submitDistributedQueryResultsRequestShim{ + NodeKey: *h.NodeKey, + Results: map[string]json.RawMessage{ + hostDetailQueryPrefix + "google_chrome_profiles": json.RawMessage(`[]`), + }, + Statuses: map[string]interface{}{ + hostDistributedQueryPrefix + "google_chrome_profiles": 0, + }, + Messages: map[string]string{}, + Stats: map[string]*fleet.Stats{}, + } + distributedResp := submitDistributedQueryResultsResponse{} + s.DoJSON("POST", "/api/osquery/distributed/write", distributedReq, http.StatusOK, &distributedResp) + } + + for i, h := range []*fleet.Host{host1, host2, host3} { + var hostResponse getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResponse) + require.False(t, hostResponse.Host.RefetchRequested) + require.Len(t, hostResponse.Host.EndUsers, 0) + require.Equal(t, oldHosts[i].ID, h.ID) + } +}