Clear host_mdm table row when existing Windows hosts enroll as a different OS (#28463)

For https://github.com/fleetdm/fleet/issues/27501 . We wanted the fix to
be as simple and targeted as possible so I made it only happen when an
existing Windows host enrolls as a different OS.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated automated tests
- [x] A detailed QA plan exists on the associated ticket (if it isn't
there, work with the product group's QA engineer to add it)
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Jordan Montgomery 2025-04-23 08:53:24 -04:00 committed by GitHub
parent 1df3270545
commit 97d261968b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 99 additions and 2 deletions

View file

@ -0,0 +1 @@
Windows hosts which re-enroll as another OS such as Linux will no longer show Windows MDM information on the hosts page unless they later re-enroll as a Windows host again(i.e. when dual booting).

View file

@ -1859,6 +1859,8 @@ type enrolledHostInfo struct {
// NodeKeySet indicates whether `node_key` is set (NOT NULL) for a osquery host
// or if `orbit_node_key` is set (NOT NULL) for a orbit host.
NodeKeySet bool
// Platform is the OS of the host.
Platform string
}
// Attempts to find the matching host ID by osqueryID, host UUID or serial
@ -1887,6 +1889,7 @@ func matchHostDuringEnrollment(
LastEnrolledAt time.Time `db:"last_enrolled_at"`
NodeKeySet bool `db:"node_key_set"`
Priority int
Platform string `db:"platform"`
}
var (
@ -1902,7 +1905,7 @@ func matchHostDuringEnrollment(
}
if osqueryID != "" || uuid != "" {
_, _ = query.WriteString(fmt.Sprintf(`(SELECT id, last_enrolled_at, %s IS NOT NULL AS node_key_set, 1 priority FROM hosts WHERE osquery_host_id = ?)`, nodeKeyColumn))
_, _ = query.WriteString(fmt.Sprintf(`(SELECT id, last_enrolled_at, %s IS NOT NULL AS node_key_set, 1 priority, platform FROM hosts WHERE osquery_host_id = ?)`, nodeKeyColumn))
osqueryHostID := osqueryID
if osqueryID == "" {
// special-case, if there's no osquery identifier, use the uuid
@ -1918,7 +1921,7 @@ func matchHostDuringEnrollment(
if query.Len() > 0 {
_, _ = query.WriteString(" UNION ")
}
_, _ = query.WriteString(fmt.Sprintf(`(SELECT id, last_enrolled_at, %s IS NOT NULL AS node_key_set, 2 priority FROM hosts WHERE hardware_serial = ? AND (platform = 'darwin' OR platform = 'ios' OR platform = 'ipados') ORDER BY id LIMIT 1)`, nodeKeyColumn))
_, _ = query.WriteString(fmt.Sprintf(`(SELECT id, last_enrolled_at, %s IS NOT NULL AS node_key_set, 2 priority, platform FROM hosts WHERE hardware_serial = ? AND (platform = 'darwin' OR platform = 'ios' OR platform = 'ipados') ORDER BY id LIMIT 1)`, nodeKeyColumn))
args = append(args, serial)
}
@ -1936,6 +1939,7 @@ func matchHostDuringEnrollment(
ID: rows[0].ID,
LastEnrolledAt: rows[0].LastEnrolledAt,
NodeKeySet: rows[0].NodeKeySet,
Platform: rows[0].Platform,
}, nil
}
@ -2015,6 +2019,14 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
return ctxerr.Wrap(ctx, err, "orbit enroll error clearing host_mdm_actions")
}
// Clear host_mdm table row for this host if switching from Windows to non-Windows(e.g. Ubuntu) platform
// so that hosts that get re-imaged with other OS don't show erroneous MDM status
if enrolledHostInfo.Platform == "windows" && hostInfo.Platform != "" && hostInfo.Platform != "windows" {
if _, err := tx.ExecContext(ctx, `DELETE FROM host_mdm WHERE host_id = ?`, enrolledHostInfo.ID); err != nil {
return ctxerr.Wrap(ctx, err, "orbit enroll error clearing host_mdm entry on platform change re-enrollment")
}
}
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

View file

@ -8848,6 +8848,90 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) {
scenarioD(platform)
})
}
// Scenario E:
// - Fleet with MDM enabled.
// - two hosts with the same hardware identifiers (e.g. two cloned VMs) but platform may be different
// - fleetd running with host identifier set to uuid (default).
// - orbit enrolls first, then osquery
// - host_mdm entry exists after first host enrolls
// Expected output: The two fleetd instances should be enrolled as one host and if the first host was
// platform="windows" and the second host was not, the host_mdm entry should be removed
scenarioE := func(fromPlatform, toPlatform string) {
dupUUID := uuid.New().String()
dupHWSerial := uuid.New().String()
h1Orbit, err := ds.EnrollOrbit(ctx, false, fleet.OrbitHostInfo{
HardwareUUID: dupUUID,
HardwareSerial: dupHWSerial,
Platform: fromPlatform,
}, uuid.New().String(), nil)
require.NoError(t, err)
h1OrbitFetched, err := ds.Host(ctx, h1Orbit.ID)
require.NoError(t, err)
time.Sleep(1 * time.Second) // to test the update of last_enrolled_at
h1Osquery, err := ds.EnrollHost(ctx, false, dupUUID, dupUUID, dupHWSerial, uuid.New().String(), nil, 0)
require.NoError(t, err)
h1OsqueryFetched, err := ds.Host(ctx, h1Osquery.ID)
require.NoError(t, err)
require.NotEqual(t, h1OrbitFetched.LastEnrolledAt, h1OsqueryFetched.LastEnrolledAt)
require.Equal(t, h1Orbit.ID, h1Osquery.ID)
time.Sleep(1 * time.Second) // to test the update of last_enrolled_at
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `INSERT INTO host_mdm(host_id, enrolled, server_url, installed_from_dep, mdm_id, is_server)
VALUES(?, 1, 'https://example.com/mdm', 0, ?, 0)`, h1Orbit.ID, h1Orbit.ID+100)
return err
})
h1WithMdmFetched, err := ds.Host(ctx, h1Orbit.ID)
require.NoError(t, err)
require.NotNil(t, h1WithMdmFetched.MDM.ServerURL)
require.NotNil(t, h1WithMdmFetched.MDM.EnrollmentStatus)
h2Orbit, err := ds.EnrollOrbit(ctx, false, fleet.OrbitHostInfo{
HardwareUUID: dupUUID,
HardwareSerial: dupHWSerial,
Platform: toPlatform,
}, uuid.New().String(), nil)
require.NoError(t, err)
h2OrbitFetched, err := ds.Host(ctx, h2Orbit.ID)
require.NoError(t, err)
// orbit should not update last_enrolled_at if re-enrolling (because last_enrolled_at
// is to be set by osquery only).
require.Equal(t, h1OsqueryFetched.LastEnrolledAt, h2OrbitFetched.LastEnrolledAt)
time.Sleep(1 * time.Second) // to test the update of last_enrolled_at
h2Osquery, err := ds.EnrollHost(ctx, false, dupUUID, dupUUID, dupHWSerial, uuid.New().String(), nil, 0)
require.NoError(t, err)
require.Equal(t, h2Orbit.ID, h2Osquery.ID)
h2OsqueryFetched, err := ds.Host(ctx, h2Osquery.ID)
require.NoError(t, err)
require.NotEqual(t, h2OrbitFetched.LastEnrolledAt, h2OsqueryFetched.LastEnrolledAt)
// the hosts compete for the host entry (all have same row id)
require.Equal(t, h1Orbit.ID, h2Orbit.ID)
require.Equal(t, h1Orbit.ID, h1Osquery.ID)
require.Equal(t, h2Orbit.ID, h2Osquery.ID)
if fromPlatform == "windows" && toPlatform != "windows" {
assert.Nil(t, h2OrbitFetched.MDM.EnrollmentStatus)
assert.Nil(t, h2OrbitFetched.MDM.ServerURL)
} else {
require.NotNil(t, h2OrbitFetched.MDM.EnrollmentStatus)
assert.Equal(t, *h1WithMdmFetched.MDM.EnrollmentStatus, *h2OrbitFetched.MDM.EnrollmentStatus)
require.NotNil(t, h2OrbitFetched.MDM.ServerURL)
assert.Equal(t, *h1WithMdmFetched.MDM.ServerURL, *h2OrbitFetched.MDM.ServerURL)
}
}
for _, fromPlatform := range []string{"ubuntu", "windows", "darwin"} {
for _, toPlatform := range []string{"ubuntu", "windows", "darwin"} {
fromPlatform := fromPlatform
toPlatform := toPlatform
t.Run("scenarioE_from_"+fromPlatform+"_to_"+toPlatform, func(t *testing.T) {
t.Parallel()
scenarioE(fromPlatform, toPlatform)
})
}
}
}
func testHostsEnrollUpdatesMissingInfo(t *testing.T, ds *Datastore) {