Skip to content

Commit

Permalink
Fix last_enrolled_at for macOS devices when re-enrolling to MDM (#2…
Browse files Browse the repository at this point in the history
…0173)

#20059

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#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] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [X] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
  • Loading branch information
lucasmrod committed Jul 11, 2024
1 parent 90ca7ba commit b8479fa
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 21 deletions.
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ go.mod @fleetdm/go
/ee/fleetd-chrome @lucasmrod @getvictor
/ee/vulnerability-dashboard @eashaw
/ee/cis @sharon-fdm @lucasmrod
/ee/server/calender @lucasmrod @getvictor
/ee/server/calendar @lucasmrod @getvictor
/ee/server/service @roperzh @gillespi314 @lucasmrod @getvictor
/scripts/mdm @roperzh @gillespi314

Expand Down
1 change: 1 addition & 0 deletions changes/20059-fix-last_enrolled_at
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Fixed bug that set `Added to Fleet` to `Never` after macOS hosts re-enrolled to Fleet via MDM.
39 changes: 23 additions & 16 deletions server/datastore/mysql/apple_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -784,30 +784,37 @@ func updateMDMAppleHostDB(
) error {
refetchRequested, lastEnrolledAt := mdmHostEnrollFields(mdmHost)

updateStmt := `
UPDATE hosts SET
hardware_serial = ?,
uuid = ?,
hardware_model = ?,
platform = ?,
refetch_requested = ?,
last_enrolled_at = ?,
osquery_host_id = COALESCE(NULLIF(osquery_host_id, ''), ?)
WHERE id = ?`

if _, err := tx.ExecContext(
ctx,
updateStmt,
args := []interface{}{
mdmHost.HardwareSerial,
mdmHost.UUID,
mdmHost.HardwareModel,
mdmHost.Platform,
refetchRequested,
lastEnrolledAt,
// Set osquery_host_id to the device UUID only if it is not already set.
mdmHost.UUID,
hostID,
); err != nil {
}

// Only update last_enrolled_at if this is a iOS/iPadOS device.
// macOS should not update last_enrolled_at as it is set when osquery enrolls.
lastEnrolledAtColumn := ""
if mdmHost.Platform == "ios" || mdmHost.Platform == "ipados" {
lastEnrolledAtColumn = "last_enrolled_at = ?,"
args = append([]interface{}{lastEnrolledAt}, args...)
}

updateStmt := fmt.Sprintf(`
UPDATE hosts SET
%s
hardware_serial = ?,
uuid = ?,
hardware_model = ?,
platform = ?,
refetch_requested = ?,
osquery_host_id = COALESCE(NULLIF(osquery_host_id, ''), ?)
WHERE id = ?`, lastEnrolledAtColumn)

if _, err := tx.ExecContext(ctx, updateStmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "update mdm apple host")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20240710155623, Down_20240710155623)
}

func Up_20240710155623(tx *sql.Tx) error {
// `hosts.last_enrolled_at` contains the date the osquery agent enrolled.
//
// A bug in v4.51.0 caused the `last_enrolled_at` column to be set to
// '2000-01-01 00:00:00' (aka the "Never" date) when macOS hosts perform a
// MDM re-enrollment (see https://github.com/fleetdm/fleet/issues/20059
// for more details).
//
// We cannot restore the exact date of the osquery enrollment but
// `host_disks.created_at` is a good approximation.
if _, err := tx.Exec(`
UPDATE hosts h
JOIN host_disks hd ON h.id=hd.host_id
SET h.last_enrolled_at = hd.created_at, h.updated_at = h.updated_at
WHERE h.platform = 'darwin' AND h.last_enrolled_at = '2000-01-01 00:00:00';`,
); err != nil {
return fmt.Errorf("failed to update hosts.last_enrolled_at: %w", err)
}

return nil
}

func Down_20240710155623(tx *sql.Tx) error {
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package tables

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestUp_20240710155623(t *testing.T) {
db := applyUpToPrev(t)

i := uint(1)
newHost := func(platform, lastEnrolledAt string, hostDisk bool) uint {
id := fmt.Sprintf("%d", i)
i++
hostID := uint(execNoErrLastID(t, db,
`INSERT INTO hosts (osquery_host_id, node_key, uuid, platform, last_enrolled_at) VALUES (?, ?, ?, ?, ?);`,
id, id, id, platform, lastEnrolledAt,
))
if hostDisk {
execNoErr(t, db,
`INSERT INTO host_disks (host_id) VALUES (?);`,
hostID,
)
}
return hostID
}
neverDate := "2000-01-01 00:00:00"
ubuntuHostID := newHost("ubuntu", neverDate, true) // non-darwin hosts should not be updated.
validMacOSHostID := newHost("darwin", "2024-07-08 18:00:53", true) // host without the issue, should not be updated.
pendingMacOSDEPHostID := newHost("darwin", neverDate, false) // host without the issue (e.g. DEP pending, not enrolled), should not be updated.
invalidMacOSHostID := newHost("darwin", neverDate, true) // host with the issue, should be updated.

const getColumnsQuery = `
SELECT h.last_enrolled_at, h.updated_at, hd.created_at AS host_disks_created_at
FROM hosts h LEFT JOIN host_disks hd ON h.id=hd.host_id WHERE h.id = ?;`
type hostTimestamps struct {
LastEnrolledAt time.Time `db:"last_enrolled_at"`
UpdatedAt time.Time `db:"updated_at"`
HostDisksCreatedAt *time.Time `db:"host_disks_created_at"`
}
var ubuntuTimestampsBefore hostTimestamps
err := db.Get(&ubuntuTimestampsBefore, getColumnsQuery, ubuntuHostID)
require.NoError(t, err)
require.NotZero(t, ubuntuTimestampsBefore.UpdatedAt)
require.Equal(t, ubuntuTimestampsBefore.LastEnrolledAt.Format("2006-01-02 15:04:05"), neverDate)
require.NotNil(t, ubuntuTimestampsBefore.HostDisksCreatedAt)
require.NotZero(t, *ubuntuTimestampsBefore.HostDisksCreatedAt)
var validMacOSTimestampsBefore hostTimestamps
err = db.Get(&validMacOSTimestampsBefore, getColumnsQuery, validMacOSHostID)
require.NoError(t, err)
require.NotZero(t, validMacOSTimestampsBefore.UpdatedAt)
require.Equal(t, validMacOSTimestampsBefore.LastEnrolledAt.Format("2006-01-02 15:04:05"), "2024-07-08 18:00:53")
require.NotNil(t, validMacOSTimestampsBefore.HostDisksCreatedAt)
require.NotZero(t, *validMacOSTimestampsBefore.HostDisksCreatedAt)
var pendingMacOSDEPTimestampsBefore hostTimestamps
err = db.Get(&pendingMacOSDEPTimestampsBefore, getColumnsQuery, pendingMacOSDEPHostID)
require.NoError(t, err)
require.NotZero(t, pendingMacOSDEPTimestampsBefore.UpdatedAt)
require.Equal(t, pendingMacOSDEPTimestampsBefore.LastEnrolledAt.Format("2006-01-02 15:04:05"), neverDate)
require.Nil(t, pendingMacOSDEPTimestampsBefore.HostDisksCreatedAt)
var invalidMacOSTimestampsBefore hostTimestamps
err = db.Get(&invalidMacOSTimestampsBefore, getColumnsQuery, invalidMacOSHostID)
require.NoError(t, err)
require.NotZero(t, invalidMacOSTimestampsBefore.UpdatedAt)
require.Equal(t, invalidMacOSTimestampsBefore.LastEnrolledAt.Format("2006-01-02 15:04:05"), neverDate)
require.NotNil(t, invalidMacOSTimestampsBefore.HostDisksCreatedAt)
require.NotZero(t, *invalidMacOSTimestampsBefore.HostDisksCreatedAt)

// Apply current migration.
applyNext(t, db)

var ubuntuTimestampsAfter hostTimestamps
err = db.Get(&ubuntuTimestampsAfter, getColumnsQuery, ubuntuHostID)
require.NoError(t, err)
require.Equal(t, ubuntuTimestampsBefore, ubuntuTimestampsAfter)
var validMacOSTimestampsAfter hostTimestamps
err = db.Get(&validMacOSTimestampsAfter, getColumnsQuery, validMacOSHostID)
require.NoError(t, err)
require.Equal(t, validMacOSTimestampsBefore, validMacOSTimestampsAfter)
var pendingMacOSDEPTimestampsAfter hostTimestamps
err = db.Get(&pendingMacOSDEPTimestampsAfter, getColumnsQuery, pendingMacOSDEPHostID)
require.NoError(t, err)
require.Equal(t, pendingMacOSDEPTimestampsBefore.UpdatedAt, pendingMacOSDEPTimestampsAfter.UpdatedAt) // updated_at is unmodified
require.Equal(t, neverDate, pendingMacOSDEPTimestampsAfter.LastEnrolledAt.Format("2006-01-02 15:04:05")) // last_enrolled_at was not modified
var invalidMacOSTimestampsAfter hostTimestamps
err = db.Get(&invalidMacOSTimestampsAfter, getColumnsQuery, invalidMacOSHostID)
require.NoError(t, err)
require.Equal(t, invalidMacOSTimestampsBefore.UpdatedAt, invalidMacOSTimestampsAfter.UpdatedAt) // updated_at is unmodified
require.NotNil(t, invalidMacOSTimestampsAfter.HostDisksCreatedAt)
require.Equal(t, *invalidMacOSTimestampsAfter.HostDisksCreatedAt, invalidMacOSTimestampsAfter.LastEnrolledAt) // last_enrolled_at was updated to host_disks date
}
Loading

0 comments on commit b8479fa

Please sign in to comment.