diff --git a/changes/27501-clear-windows-mdm-info-on-linux-enroll b/changes/27501-clear-windows-mdm-info-on-linux-enroll new file mode 100644 index 0000000000..70f3173be4 --- /dev/null +++ b/changes/27501-clear-windows-mdm-info-on-linux-enroll @@ -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). diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index bded5cb7ac..47412b355b 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -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 diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 85efd52a35..3bbd473d39 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -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) {