Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/45380-windows-mdm-enrollment-row-linkage
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed a race condition after Windows BYOD MDM enrollment (Settings > Access work or school > Connect) where `mdm_windows_enrollments.host_uuid` stayed empty for several seconds, causing server-side enrollment lookups to miss. The enrollment is now linked to the Fleet host record at the first management session via OMA-DM DevDetail/SMBIOSSerialNumber instead of waiting for osquery's distributed-read backfill.
29 changes: 16 additions & 13 deletions server/datastore/mysql/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -6338,6 +6338,21 @@ func (ds *Datastore) HostLiteByID(ctx context.Context, id uint) (*fleet.HostLite
return ds.loadHostLite(ctx, &id, nil)
}

// hostLiteColumns is the shared SELECT column list
const hostLiteColumns = `
h.id,
h.team_id,
h.osquery_host_id,
COALESCE(h.node_key, '') AS node_key,
h.computer_name,
h.hostname,
h.uuid,
h.hardware_model,
h.hardware_serial,
h.distributed_interval,
h.config_tls_refresh,
COALESCE(hst.seen_time, h.created_at) AS seen_time`

func (ds *Datastore) loadHostLite(ctx context.Context, id *uint, identifier *string) (*fleet.HostLite, error) {
if id == nil && identifier == nil {
return nil, errors.New("must set one of id or identifier")
Expand All @@ -6346,19 +6361,7 @@ func (ds *Datastore) loadHostLite(ctx context.Context, id *uint, identifier *str
return nil, errors.New("cannot set both id and identifier")
}
stmt := `
SELECT
h.id,
h.team_id,
h.osquery_host_id,
COALESCE(h.node_key, '') AS node_key,
h.computer_name,
h.hostname,
h.uuid,
h.hardware_model,
h.hardware_serial,
h.distributed_interval,
h.config_tls_refresh,
COALESCE(hst.seen_time, h.created_at) AS seen_time
SELECT ` + hostLiteColumns + `
FROM hosts h
LEFT JOIN host_seen_times hst ON (h.id = hst.host_id)
%s
Expand Down
39 changes: 37 additions & 2 deletions server/datastore/mysql/microsoft_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,31 @@ func (ds *Datastore) MDMWindowsGetUnlinkedEnrolledDeviceWithDeviceName(ctx conte
return &winMDMDevice, nil
}

// WindowsHostLiteByHardwareSerial looks up a Windows host by its hardware_serial. If multiple Windows hosts share the
// same serial (e.g. VM gold images that did not regenerate SMBIOS), the caller cannot pick safely so we return NotFound.
//
// The read honors ctxdb.RequirePrimary: a caller linking a just-enrolled host (whose hosts row may have been inserted
// seconds ago) must pass a primary-required context, otherwise replica lag can return a false NotFound.
func (ds *Datastore) WindowsHostLiteByHardwareSerial(ctx context.Context, hardwareSerial string) (*fleet.HostLite, error) {
if hardwareSerial == "" {
return nil, ctxerr.Wrap(ctx, notFound("Host").WithMessage("empty hardware serial"))
}
const stmt = `
SELECT ` + hostLiteColumns + `
FROM hosts h
LEFT JOIN host_seen_times hst ON h.id = hst.host_id
WHERE h.hardware_serial = ? AND h.platform = 'windows'
LIMIT 2`
var hosts []*fleet.HostLite
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, stmt, hardwareSerial); err != nil {
return nil, ctxerr.Wrap(ctx, err, "select windows host by hardware serial")
}
if len(hosts) != 1 {
return nil, ctxerr.Wrap(ctx, notFound("Host").WithMessage(hardwareSerial))
}
return hosts[0], nil
}

// HasWindowsSetupExperienceItemsForTeam returns true if any active Windows setup-experience software
// installers with install_during_setup=TRUE are configured for the given team. teamID=0 means "no team /
// global", matching the value EnqueueSetupExperienceItems passes in for hosts on no team.
Expand Down Expand Up @@ -805,9 +830,19 @@ func (ds *Datastore) MDMWindowsSaveResponse(ctx context.Context, enrolledDevice
}

if len(matchingCmds) == 0 {
if len(commandIDsBeingResent) == 0 {
// Suppress the warning when the only CmdRefs in the message are Fleet-internal commands that are
// intentionally absent from windows_mdm_commands (e.g. the DevDetail/SMBIOSSerialNumber Get used for
// unlinked-enrollment linkage). Without this filter, an unlinked Windows enrollment with no other pending
// commands warns on every session until linkage completes.
externalCmdRefs := make([]string, 0, len(enrichedSyncML.CmdRefUUIDs))
for _, u := range enrichedSyncML.CmdRefUUIDs {
if !fleet.IsFleetInternalCmdID(u) {
externalCmdRefs = append(externalCmdRefs, u)
}
}
if len(commandIDsBeingResent) == 0 && len(externalCmdRefs) > 0 {
// Only log if not resending commands as we then can expect no matching commands
ds.logger.WarnContext(ctx, "unmatched Windows MDM commands", "uuids", strings.Join(enrichedSyncML.CmdRefUUIDs, ","), "mdm_device_id",
ds.logger.WarnContext(ctx, "unmatched Windows MDM commands", "uuids", strings.Join(externalCmdRefs, ","), "mdm_device_id",
enrolledDevice.MDMDeviceID)
}
return nil
Expand Down
46 changes: 46 additions & 0 deletions server/datastore/mysql/microsoft_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func TestMDMWindows(t *testing.T) {
{"TestMDMWindowsInsertCommandSkipsUnenrolledHosts", testMDMWindowsInsertCommandSkipsUnenrolledHosts},
{"TestCleanupWindowsMDMCommandQueue", testCleanupWindowsMDMCommandQueue},
{"TestMDMWindowsGetUnlinkedEnrolledDeviceWithDeviceName", testMDMWindowsGetUnlinkedEnrolledDeviceWithDeviceName},
{"TestWindowsHostLiteByHardwareSerial", testWindowsHostLiteByHardwareSerial},
}

for _, c := range cases {
Expand Down Expand Up @@ -6351,3 +6352,48 @@ func testMDMWindowsGetUnlinkedEnrolledDeviceWithDeviceName(t *testing.T, ds *Dat
require.True(t, fleet.IsNotFound(err),
"all matching rows now linked; method must return NotFound")
}

func testWindowsHostLiteByHardwareSerial(t *testing.T, ds *Datastore) {
ctx := t.Context()

// 1. Empty serial → NotFound.
_, err := ds.WindowsHostLiteByHardwareSerial(ctx, "")
require.Error(t, err)
require.True(t, fleet.IsNotFound(err))

// 2. No matching host → NotFound.
_, err = ds.WindowsHostLiteByHardwareSerial(ctx, "SERIAL-DOES-NOT-EXIST")
require.Error(t, err)
require.True(t, fleet.IsNotFound(err))

// 3. Single matching Windows host → returns it.
winHost := test.NewHost(t, ds, "win-by-serial", "10.0.0.50", "win-by-serial-key", "win-by-serial-uuid", time.Now())
winHost.Platform = "windows"
winHost.HardwareSerial = "SERIAL-WIN-1"
require.NoError(t, ds.UpdateHost(ctx, winHost))

got, err := ds.WindowsHostLiteByHardwareSerial(ctx, "SERIAL-WIN-1")
require.NoError(t, err)
require.Equal(t, winHost.ID, got.ID)
require.Equal(t, winHost.UUID, got.UUID)

// 4. Non-Windows host with the same serial must not match (platform filter prevents cross-platform mis-linkage).
macHost := test.NewHost(t, ds, "mac-same-serial", "10.0.0.51", "mac-same-serial-key", "mac-same-serial-uuid", time.Now())
macHost.Platform = "darwin"
macHost.HardwareSerial = "SERIAL-WIN-1"
require.NoError(t, ds.UpdateHost(ctx, macHost))

got, err = ds.WindowsHostLiteByHardwareSerial(ctx, "SERIAL-WIN-1")
require.NoError(t, err)
require.Equal(t, winHost.ID, got.ID, "must return the Windows host even when a darwin host shares the serial")

// 5. Two Windows hosts sharing the same serial → NotFound (linkage would be ambiguous).
winHostDup := test.NewHost(t, ds, "win-by-serial-dup", "10.0.0.52", "win-by-serial-dup-key", "win-by-serial-dup-uuid", time.Now())
winHostDup.Platform = "windows"
winHostDup.HardwareSerial = "SERIAL-WIN-1"
require.NoError(t, ds.UpdateHost(ctx, winHostDup))

_, err = ds.WindowsHostLiteByHardwareSerial(ctx, "SERIAL-WIN-1")
require.Error(t, err)
require.True(t, fleet.IsNotFound(err), "two Windows hosts share the serial; method must refuse to pick")
}
3 changes: 3 additions & 0 deletions server/fleet/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,9 @@ type Datastore interface {
// find a row because the host->enrollment link has not been resolved yet.
MDMWindowsGetUnlinkedEnrolledDeviceWithDeviceName(ctx context.Context, deviceName string) (*MDMWindowsEnrolledDevice, error)

// WindowsHostLiteByHardwareSerial returns a HostLite for the Windows host whose hardware_serial matches the given serial.
WindowsHostLiteByHardwareSerial(ctx context.Context, hardwareSerial string) (*HostLite, error)
Comment thread
getvictor marked this conversation as resolved.

// MDMWindowsDeleteEnrolledDeviceWithDeviceID deletes a give MDMWindowsEnrolledDevice entry from the database using the device id
MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) error

Expand Down
51 changes: 51 additions & 0 deletions server/fleet/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,57 @@ func (s HostStatus) IsValid() bool {
}
}

// placeholderHardwareSerials is the set of well-known junk SMBIOS serial numbers that OEM/BIOS firmware and VM
// templates emit in place of a real, unique serial. Keys are already trimmed and lower-cased; compare against a
// normalized serial. The list is best-effort and will never be exhaustive, so IsPlaceholderHardwareSerial also applies
// a repeated-character heuristic and callers fall back to a unique identifier when a serial is a placeholder.
var placeholderHardwareSerials = map[string]struct{}{
"to be filled by o.e.m.": {},
"default string": {},
"system serial number": {},
"not specified": {},
"not applicable": {},
"none": {},
"oem": {},
"o.e.m.": {},
"default": {},
"unknown": {},
"chassis serial number": {},
"base board serial number": {},
"baseboard serial number": {},
"123456789": {},
"0123456789": {},
"1234567890": {},
"1234567": {},
"n/a": {},
"na": {},
"invalid": {},
}

// IsPlaceholderHardwareSerial reports whether serial is empty or a well-known placeholder/junk value that does not
// uniquely identify a device (common on whitebox/consumer hardware and un-sysprepped VM templates). Callers must not
// use such a serial to match or link a host, since multiple unrelated devices report the same value; they should fall
// back to an unambiguous identifier instead.
//
// Matching is case-insensitive and trimmed. In addition to the known-value set, a serial made up of a single repeated
// character (e.g. "0", "00000000", "xxxxxxx", "-------") is treated as a placeholder, since those cannot be enumerated.
func IsPlaceholderHardwareSerial(serial string) bool {
s := strings.TrimSpace(serial)
if s == "" {
return true
}
if _, ok := placeholderHardwareSerials[strings.ToLower(s)]; ok {
return true
}
// A serial that is the same character repeated (all zeros, all dots, all dashes, etc.) is never a real identity.
for i := 1; i < len(s); i++ {
if s[i] != s[0] {
return false
}
}
return true
}

// MDMEnrollStatus defines the possible MDM enrollment statuses.
type MDMEnrollStatus string

Expand Down
59 changes: 59 additions & 0 deletions server/fleet/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,62 @@ func TestMDMNameFromServerURL(t *testing.T) {
})
}
}

func TestIsPlaceholderHardwareSerial(t *testing.T) {
for _, tc := range []struct {
name string
serial string
want bool
}{
// Empty / whitespace.
{"empty", "", true},
{"whitespace only", " ", true},
{"tabs and newline", "\t \n", true},

// Known OEM/BIOS placeholders, matched case-insensitively and trimmed.
{"to be filled exact", "To Be Filled By O.E.M.", true},
{"to be filled lower", "to be filled by o.e.m.", true},
{"to be filled upper", "TO BE FILLED BY O.E.M.", true},
{"to be filled padded", " To Be Filled By O.E.M. ", true},
{"default string", "Default string", true},
{"system serial number", "System Serial Number", true},
{"not specified", "Not Specified", true},
{"not applicable", "Not Applicable", true},
{"none", "None", true},
{"oem", "OEM", true},
{"o.e.m.", "O.E.M.", true},
{"default", "Default", true},
{"unknown", "Unknown", true},
{"chassis serial number", "Chassis Serial Number", true},
{"base board serial number", "Base Board Serial Number", true},
{"baseboard serial number", "Baseboard Serial Number", true},
{"sequential 123456789", "123456789", true},
{"sequential 0123456789", "0123456789", true},
{"sequential 1234567890", "1234567890", true},
{"sequential 1234567", "1234567", true},
{"n/a", "N/A", true},
{"na", "na", true},
{"invalid", "INVALID", true},

// Repeated-character heuristic (cannot be enumerated).
{"single zero", "0", true},
{"all zeros", "00000000", true},
{"all x", "xxxxxxx", true},
{"all dashes", "-------", true},
{"all dots", "........", true},

// Real, unique serials must NOT be flagged.
{"dell service tag", "7XQ2W13", false},
{"lenovo serial", "PF0ABCDE", false},
{"apple-style serial", "C02ABCDEFGHJ", false},
{"vmware unique", "VMware-56 4d 1a 2b 3c", false},
{"none as substring", "NONE123", false},
{"default as substring", "DEFAULT-7H2K", false},
{"leading zeros but real", "00000001", false},
{"long alphanumeric", "ABC123XYZ789", false},
} {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, IsPlaceholderHardwareSerial(tc.serial))
})
}
}
13 changes: 13 additions & 0 deletions server/fleet/microsoft_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,19 @@ const (
CmdStatus = "Status" // Protocol Command verb Status
)

// FleetInternalCmdIDPrefix marks Fleet-injected SyncML commands that are sent inline in a management response but never
// persisted to windows_mdm_commands. The device's <Status>/<Results> for these commands will reference a CmdID that
// won't match any row in windows_mdm_commands. MDMWindowsSaveResponse uses this prefix to skip them before warning
// about "unmatched Windows MDM commands". Example: the DevDetail/SMBIOSSerialNumber Get used to link unlinked Windows
// MDM enrollments (see server/service/microsoft_mdm.go).
const FleetInternalCmdIDPrefix = "fleet-internal-"

// IsFleetInternalCmdID reports whether a SyncML CmdID was injected by Fleet itself and is intentionally absent from
// windows_mdm_commands.
func IsFleetInternalCmdID(cmdID string) bool {
return strings.HasPrefix(cmdID, FleetInternalCmdIDPrefix)
}

// ProtoCmdOperation is the abstraction to represent a SyncML Protocol Command
type ProtoCmdOperation struct {
Verb string `db:"verb"`
Expand Down
18 changes: 18 additions & 0 deletions server/fleet/microsoft_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -812,3 +812,21 @@ func TestExtractLocURIsFromProfileBytes(t *testing.T) {
require.Equal(t, []string{"./Device/A"}, uris)
})
}

func TestIsFleetInternalCmdID(t *testing.T) {
for _, tc := range []struct {
name string
in string
want bool
}{
{"empty string", "", false},
{"random UUID", "550e8400-e29b-41d4-a716-446655440000", false},
{"unrelated prefix", "foo-internal-bar", false},
{"prefix only", FleetInternalCmdIDPrefix, true},
{"devdetail link probe", FleetInternalCmdIDPrefix + "devdetail-smbios-serial", true},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, IsFleetInternalCmdID(tc.in))
})
}
}
12 changes: 12 additions & 0 deletions server/mock/datastore_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,8 @@ type MDMWindowsGetEnrolledDeviceWithHostUUIDFunc func(ctx context.Context, hostU

type MDMWindowsGetUnlinkedEnrolledDeviceWithDeviceNameFunc func(ctx context.Context, deviceName string) (*fleet.MDMWindowsEnrolledDevice, error)

type WindowsHostLiteByHardwareSerialFunc func(ctx context.Context, hardwareSerial string) (*fleet.HostLite, error)

type MDMWindowsDeleteEnrolledDeviceWithDeviceIDFunc func(ctx context.Context, mdmDeviceID string) error

type MDMWindowsInsertCommandForHostsFunc func(ctx context.Context, hostUUIDs []string, cmd *fleet.MDMWindowsCommand) error
Expand Down Expand Up @@ -3947,6 +3949,9 @@ type DataStore struct {
MDMWindowsGetUnlinkedEnrolledDeviceWithDeviceNameFunc MDMWindowsGetUnlinkedEnrolledDeviceWithDeviceNameFunc
MDMWindowsGetUnlinkedEnrolledDeviceWithDeviceNameFuncInvoked bool

WindowsHostLiteByHardwareSerialFunc WindowsHostLiteByHardwareSerialFunc
WindowsHostLiteByHardwareSerialFuncInvoked bool

MDMWindowsDeleteEnrolledDeviceWithDeviceIDFunc MDMWindowsDeleteEnrolledDeviceWithDeviceIDFunc
MDMWindowsDeleteEnrolledDeviceWithDeviceIDFuncInvoked bool

Expand Down Expand Up @@ -9511,6 +9516,13 @@ func (s *DataStore) MDMWindowsGetUnlinkedEnrolledDeviceWithDeviceName(ctx contex
return s.MDMWindowsGetUnlinkedEnrolledDeviceWithDeviceNameFunc(ctx, deviceName)
}

func (s *DataStore) WindowsHostLiteByHardwareSerial(ctx context.Context, hardwareSerial string) (*fleet.HostLite, error) {
s.mu.Lock()
s.WindowsHostLiteByHardwareSerialFuncInvoked = true
s.mu.Unlock()
return s.WindowsHostLiteByHardwareSerialFunc(ctx, hardwareSerial)
}

func (s *DataStore) MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) error {
s.mu.Lock()
s.MDMWindowsDeleteEnrolledDeviceWithDeviceIDFuncInvoked = true
Expand Down
Loading
Loading