package mysql import ( "context" "crypto/md5" // nolint:gosec // used only to hash for efficient comparisons "database/sql" "encoding/xml" "fmt" "strings" "testing" "time" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMDMWindows(t *testing.T) { ds := CreateMySQLDS(t) cases := []struct { name string fn func(t *testing.T, ds *Datastore) }{ {"TestMDMWindowsEnrolledDevices", testMDMWindowsEnrolledDevice}, {"TestMDMWindowsInsertCommandForHosts", testMDMWindowsInsertCommandForHosts}, {"TestMDMWindowsGetPendingCommands", testMDMWindowsGetPendingCommands}, {"TestMDMWindowsCommandResults", testMDMWindowsCommandResults}, {"TestMDMWindowsCommandResultsWithPendingResult", testMDMWindowsCommandResultsWithPendingResult}, {"TestMDMWindowsProfileManagement", testMDMWindowsProfileManagement}, {"TestBulkOperationsMDMWindowsHostProfiles", testBulkOperationsMDMWindowsHostProfiles}, {"TestBulkOperationsMDMWindowsHostProfilesBatch2", testBulkOperationsMDMWindowsHostProfilesBatch2}, {"TestBulkOperationsMDMWindowsHostProfilesBatch3", testBulkOperationsMDMWindowsHostProfilesBatch3}, {"TestGetMDMWindowsProfilesContents", testGetMDMWindowsProfilesContents}, {"TestMDMWindowsConfigProfiles", testMDMWindowsConfigProfiles}, {"TestMDMWindowsConfigProfilesWithFleetVars", testMDMWindowsConfigProfilesWithFleetVars}, {"TestSetOrReplaceMDMWindowsConfigProfile", testSetOrReplaceMDMWindowsConfigProfile}, {"TestMDMWindowsDiskEncryption", testMDMWindowsDiskEncryption}, {"TestMDMWindowsProfilesSummary", testMDMWindowsProfilesSummary}, {"TestBatchSetMDMWindowsProfiles", testBatchSetMDMWindowsProfiles}, {"TestMDMWindowsProfileLabels", testMDMWindowsProfileLabels}, {"TestMDMWindowsSaveResponse", testSaveResponse}, {"TestSetMDMWindowsProfilesWithVariables", testSetMDMWindowsProfilesWithVariables}, {"TestWindowsMDMManagedSCEPCertificates", testWindowsMDMManagedSCEPCertificates}, {"TestGetWindowsMDMCommandsForResending", testGetWindowsMDMCommandsForResending}, {"TestResendWindowsMDMCommand", testResendWindowsMDMCommand}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { defer TruncateTables(t, ds) c.fn(t, ds) }) } } func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) { ctx := context.Background() enrolledDevice := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: uuid.New().String(), MDMHardwareID: uuid.New().String() + uuid.New().String(), MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "CIMClient_Windows", MDMDeviceName: "DESKTOP-1C3ARC1", MDMEnrollType: "ProgrammaticEnrollment", MDMEnrollUserID: "", MDMEnrollProtoVersion: "5.0", MDMEnrollClientVersion: "10.0.19045.2965", MDMNotInOOBE: false, } err := ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice) require.NoError(t, err) // inserting a device again doesn't trow an error err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice) require.NoError(t, err) gotEnrolledDevice, err := ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID) require.NoError(t, err) require.NotZero(t, gotEnrolledDevice.CreatedAt) require.Equal(t, enrolledDevice.MDMDeviceID, gotEnrolledDevice.MDMDeviceID) require.Equal(t, enrolledDevice.MDMHardwareID, gotEnrolledDevice.MDMHardwareID) err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID) require.NoError(t, err) var nfe fleet.NotFoundError _, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID) require.ErrorAs(t, err, &nfe) err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID) require.ErrorAs(t, err, &nfe) // Test using device ID instead of hardware ID err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice) require.NoError(t, err) // inserting a device again doesn't trow an error err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice) require.NoError(t, err) gotEnrolledDevice, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID) require.NoError(t, err) require.NotZero(t, gotEnrolledDevice.CreatedAt) require.Equal(t, enrolledDevice.MDMDeviceID, gotEnrolledDevice.MDMDeviceID) require.Equal(t, enrolledDevice.MDMHardwareID, gotEnrolledDevice.MDMHardwareID) require.Empty(t, gotEnrolledDevice.HostUUID) err = ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID) require.NoError(t, err) _, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID) require.ErrorAs(t, err, &nfe) err = ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(ctx, enrolledDevice.MDMHardwareID) require.ErrorAs(t, err, &nfe) } func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) { ctx := context.Background() checkBitLockerSummary := func(t *testing.T, teamID *uint, expected fleet.MDMWindowsBitLockerSummary) { bls, err := ds.GetMDMWindowsBitLockerSummary(ctx, teamID) require.NoError(t, err) require.NotNil(t, bls) require.Equal(t, expected, *bls) } checkMDMProfilesSummary := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) { ps, err := ds.GetMDMWindowsProfilesSummary(ctx, teamID) require.NoError(t, err) require.NotNil(t, ps) require.Equal(t, expected, *ps) } checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) { gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status}) require.NoError(t, err) require.Len(t, gotHosts, len(expectedIDs)) for _, h := range gotHosts { require.Contains(t, expectedIDs, h.ID) } } checkListHostsFilterDiskEncryption := func(t *testing.T, teamID *uint, status fleet.DiskEncryptionStatus, expectedIDs []uint) { gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsDiskEncryptionFilter: status}) require.NoError(t, err) require.Len(t, gotHosts, len(expectedIDs), "status: %s", status) for _, h := range gotHosts { require.Contains(t, expectedIDs, h.ID) } count, err := ds.CountHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsDiskEncryptionFilter: status}) require.NoError(t, err) require.Equal(t, len(expectedIDs), count, fmt.Sprintf("status: %s", status)) } checkHostBitLockerStatus := func(t *testing.T, expected fleet.DiskEncryptionStatus, hostIDs []uint) { for _, id := range hostIDs { h, err := ds.Host(ctx, id) require.NoError(t, err) require.NotNil(t, h) mdmInfo, err := ds.GetHostMDM(ctx, id) require.NoError(t, err) require.NotNil(t, mdmInfo) bls, err := ds.GetMDMWindowsBitLockerStatus(ctx, h) require.NoError(t, err) require.NotNil(t, bls) require.NotNil(t, bls.Status) require.Equal(t, expected, *bls.Status) } } type hostIDsByDEStatus map[fleet.DiskEncryptionStatus][]uint type hostIDsByProfileStatus map[fleet.MDMDeliveryStatus][]uint expectedProfilesFromDE := func(expectedDE hostIDsByDEStatus) hostIDsByProfileStatus { expectedProfiles := make(hostIDsByProfileStatus) expectedProfiles[fleet.MDMDeliveryPending] = []uint{} for status, hostIDs := range expectedDE { switch status { case fleet.DiskEncryptionVerified: expectedProfiles[fleet.MDMDeliveryVerified] = hostIDs case fleet.DiskEncryptionVerifying: expectedProfiles[fleet.MDMDeliveryVerifying] = hostIDs case fleet.DiskEncryptionFailed: expectedProfiles[fleet.MDMDeliveryFailed] = hostIDs case fleet.DiskEncryptionEnforcing, fleet.DiskEncryptionRemovingEnforcement, fleet.DiskEncryptionActionRequired: expectedProfiles[fleet.MDMDeliveryPending] = append(expectedProfiles[fleet.MDMDeliveryPending], hostIDs...) } } return expectedProfiles } checkExpected := func(t *testing.T, teamID *uint, expectedDE hostIDsByDEStatus, expectedProfiles ...hostIDsByProfileStatus) { var ep hostIDsByProfileStatus switch len(expectedProfiles) { case 1: ep = expectedProfiles[0] case 0: ep = expectedProfilesFromDE(expectedDE) default: require.FailNow(t, "expectedProfiles must have length 0 or 1") } for _, status := range []fleet.DiskEncryptionStatus{ fleet.DiskEncryptionVerified, fleet.DiskEncryptionVerifying, fleet.DiskEncryptionFailed, fleet.DiskEncryptionEnforcing, fleet.DiskEncryptionRemovingEnforcement, fleet.DiskEncryptionActionRequired, } { hostIDs, ok := expectedDE[status] if !ok { hostIDs = []uint{} } checkListHostsFilterDiskEncryption(t, teamID, status, hostIDs) checkHostBitLockerStatus(t, status, hostIDs) } checkBitLockerSummary(t, teamID, fleet.MDMWindowsBitLockerSummary{ Verified: uint(len(expectedDE[fleet.DiskEncryptionVerified])), Verifying: uint(len(expectedDE[fleet.DiskEncryptionVerifying])), Failed: uint(len(expectedDE[fleet.DiskEncryptionFailed])), Enforcing: uint(len(expectedDE[fleet.DiskEncryptionEnforcing])), RemovingEnforcement: uint(len(expectedDE[fleet.DiskEncryptionRemovingEnforcement])), ActionRequired: uint(len(expectedDE[fleet.DiskEncryptionActionRequired])), }) checkMDMProfilesSummary(t, teamID, fleet.MDMProfilesSummary{ Pending: uint(len(ep[fleet.MDMDeliveryPending])), Failed: uint(len(ep[fleet.MDMDeliveryFailed])), Verifying: uint(len(ep[fleet.MDMDeliveryVerifying])), Verified: uint(len(ep[fleet.MDMDeliveryVerified])), }) checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, ep[fleet.MDMDeliveryVerified]) checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, ep[fleet.MDMDeliveryVerifying]) checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, ep[fleet.MDMDeliveryFailed]) checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, ep[fleet.MDMDeliveryPending]) } updateHostDisks := func(t *testing.T, hostID uint, encrypted bool, updated_at time.Time) { ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { stmt := `UPDATE host_disks SET encrypted = ?, updated_at = ? where host_id = ?` _, err := q.ExecContext(ctx, stmt, encrypted, updated_at, hostID) return err }) } setKeyUpdatedAt := func(t *testing.T, hostID uint, keyUpdatedAt time.Time) { ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { stmt := `UPDATE host_disk_encryption_keys SET updated_at = ? where host_id = ?` _, err := q.ExecContext(ctx, stmt, keyUpdatedAt, hostID) return err }) } upsertHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, status fleet.MDMDeliveryStatus) { ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { // Generate a command UUID for the profile commandUUID := "cmd-" + profUUID stmt := `INSERT INTO host_mdm_windows_profiles (host_uuid, profile_uuid, status, command_uuid) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?` _, err := q.ExecContext(ctx, stmt, hostUUID, profUUID, status, commandUUID, status) return err }) } cleanupHostProfiles := func(t *testing.T) { ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `DELETE FROM host_mdm_windows_profiles`) return err }) } // Create some hosts var hosts []*fleet.Host for i := 0; i < 10; i++ { p := "windows" if i >= 5 { p = "darwin" } u := uuid.New().String() h, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), NodeKey: &u, UUID: u, Hostname: u, Platform: p, }) require.NoError(t, err) require.NotNil(t, h) hosts = append(hosts, h) require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false)) if p == "darwin" { nanoEnroll(t, ds, h, false) } else { windowsEnroll(t, ds, h) } } t.Run("Disk encryption disabled", func(t *testing.T) { ac, err := ds.AppConfig(ctx) require.NoError(t, err) ac.MDM.EnableDiskEncryption = optjson.SetBool(false) require.NoError(t, ds.SaveAppConfig(ctx, ac)) ac, err = ds.AppConfig(ctx) require.NoError(t, err) require.False(t, ac.MDM.EnableDiskEncryption.Value) cleanupHostProfiles(t) checkExpected(t, nil, hostIDsByDEStatus{}) // no hosts are counted because disk encryption is not enabled }) t.Run("Disk encryption enabled", func(t *testing.T) { ac, err := ds.AppConfig(ctx) require.NoError(t, err) ac.MDM.EnableDiskEncryption = optjson.SetBool(true) require.NoError(t, ds.SaveAppConfig(ctx, ac)) ac, err = ds.AppConfig(ctx) require.NoError(t, err) require.True(t, ac.MDM.EnableDiskEncryption.Value) t.Run("Bitlocker enforcing-verifying-verified", func(t *testing.T) { // all windows hosts are counted as enforcing because they have not reported any disk encryption status yet checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) _, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true)) require.NoError(t, err) checkExpected(t, nil, hostIDsByDEStatus{ // status is still pending because hosts_disks hasn't been updated yet fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionEnforcing: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) cases := []struct { name string hostDisksEncrypted bool reportedAfterKey bool expectedWithinGracePeriod fleet.DiskEncryptionStatus expectedOutsideGracePeriod fleet.DiskEncryptionStatus }{ { name: "encrypted reported after key", hostDisksEncrypted: true, reportedAfterKey: true, expectedWithinGracePeriod: fleet.DiskEncryptionVerified, expectedOutsideGracePeriod: fleet.DiskEncryptionVerified, }, { name: "encrypted reported before key", hostDisksEncrypted: true, reportedAfterKey: false, expectedWithinGracePeriod: fleet.DiskEncryptionVerifying, expectedOutsideGracePeriod: fleet.DiskEncryptionVerifying, }, { name: "not encrypted reported before key", hostDisksEncrypted: false, reportedAfterKey: false, expectedWithinGracePeriod: fleet.DiskEncryptionEnforcing, expectedOutsideGracePeriod: fleet.DiskEncryptionEnforcing, }, { name: "not encrypted reported after key", hostDisksEncrypted: false, reportedAfterKey: true, expectedWithinGracePeriod: fleet.DiskEncryptionVerifying, expectedOutsideGracePeriod: fleet.DiskEncryptionEnforcing, }, } testHostID := hosts[0].ID otherWindowsHostIDs := []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} for _, c := range cases { t.Run(c.name, func(t *testing.T) { var keyUpdatedAt, hostDisksUpdatedAt time.Time t.Run("within grace period", func(t *testing.T) { expected := make(hostIDsByDEStatus) if c.expectedWithinGracePeriod == fleet.DiskEncryptionEnforcing { expected[fleet.DiskEncryptionEnforcing] = append([]uint{testHostID}, otherWindowsHostIDs...) } else { expected[c.expectedWithinGracePeriod] = []uint{testHostID} expected[fleet.DiskEncryptionEnforcing] = otherWindowsHostIDs } keyUpdatedAt = time.Now().Add(-10 * time.Minute) setKeyUpdatedAt(t, testHostID, keyUpdatedAt) if c.reportedAfterKey { hostDisksUpdatedAt = keyUpdatedAt.Add(5 * time.Minute) } else { hostDisksUpdatedAt = keyUpdatedAt.Add(-5 * time.Minute) } updateHostDisks(t, testHostID, c.hostDisksEncrypted, hostDisksUpdatedAt) checkExpected(t, nil, expected) }) t.Run("outside grace period", func(t *testing.T) { expected := make(hostIDsByDEStatus) if c.expectedOutsideGracePeriod == fleet.DiskEncryptionEnforcing { expected[fleet.DiskEncryptionEnforcing] = append([]uint{testHostID}, otherWindowsHostIDs...) } else { expected[c.expectedOutsideGracePeriod] = []uint{testHostID} expected[fleet.DiskEncryptionEnforcing] = otherWindowsHostIDs } keyUpdatedAt = time.Now().Add(-2 * time.Hour) setKeyUpdatedAt(t, testHostID, keyUpdatedAt) if c.reportedAfterKey { hostDisksUpdatedAt = keyUpdatedAt.Add(5 * time.Minute) } else { hostDisksUpdatedAt = keyUpdatedAt.Add(-5 * time.Minute) } updateHostDisks(t, testHostID, c.hostDisksEncrypted, hostDisksUpdatedAt) checkExpected(t, nil, expected) }) }) } }) // ensure hosts[0] is set to verified for the rest of the tests _, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true)) require.NoError(t, err) require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionEnforcing: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) t.Run("BitLocker failed status", func(t *testing.T) { // set hosts[1] to failed _, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1], "", "test-error", ptr.Bool(false)) require.NoError(t, err) expected := hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID, hosts[3].ID, hosts[4].ID}, } checkExpected(t, nil, expected) // bitlocker failed status determines MDM aggregate status (profiles status is ignored) upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", fleet.MDMDeliveryFailed) checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", fleet.MDMDeliveryPending) checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", fleet.MDMDeliveryVerifying) checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", fleet.MDMDeliveryVerified) checkExpected(t, nil, expected) // profiles failed status determines MDM aggregate status (bitlocker status is ignored) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", fleet.MDMDeliveryFailed) expectedProfiles := expectedProfilesFromDE(expected) expectedProfiles[fleet.MDMDeliveryFailed] = append(expectedProfiles[fleet.MDMDeliveryFailed], hosts[0].ID) expectedProfiles[fleet.MDMDeliveryVerified] = []uint{} checkExpected(t, nil, expected, expectedProfiles) cleanupHostProfiles(t) }) t.Run("BitLocker profile status with PIN required", func(t *testing.T) { // Turn on Bitlocker requirement ac.MDM.RequireBitLockerPIN = optjson.SetBool(true) require.NoError(t, ds.SaveAppConfig(ctx, ac)) ac, err = ds.AppConfig(ctx) require.NoError(t, err) require.True(t, ac.MDM.RequireBitLockerPIN.Value) // Expect that the host that would be "verified" // is now in "action required" status. // This will also verify that when filtering by profile status, // the "verified" host is now counted as "pending". expected := hostIDsByDEStatus{ fleet.DiskEncryptionActionRequired: []uint{hosts[0].ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID, hosts[3].ID, hosts[4].ID}, } checkExpected(t, nil, expected) // Set the "tpm_pin_set" to true for the host that would be "verified" ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `UPDATE host_disks SET tpm_pin_set = true WHERE host_id = ?`, hosts[0].ID) return err }) expected = hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID, hosts[3].ID, hosts[4].ID}, } checkExpected(t, nil, expected) // Reset the "tpm_pin_set" to false for the host. ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `UPDATE host_disks SET tpm_pin_set = false WHERE host_id = ?`, hosts[0].ID) return err }) // Reset "RequireBitLockerPIN" to false ac.MDM.RequireBitLockerPIN = optjson.SetBool(false) require.NoError(t, ds.SaveAppConfig(ctx, ac)) ac, err = ds.AppConfig(ctx) require.NoError(t, err) require.False(t, ac.MDM.RequireBitLockerPIN.Value) }) t.Run("BitLocker team filtering", func(t *testing.T) { // Test team filtering team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team"}) require.NoError(t, err) tm, err := ds.TeamWithExtras(ctx, team.ID) require.NoError(t, err) require.NotNil(t, tm) require.False(t, tm.Config.MDM.EnableDiskEncryption) // disk encryption is not enabled for team // Transfer hosts[2] to the team require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{hosts[2].ID}))) // Check the summary for the team checkExpected(t, &team.ID, hostIDsByDEStatus{}) // disk encryption is not enabled for team so hosts[2] is not counted // Check the summary for no team checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, fleet.DiskEncryptionEnforcing: []uint{hosts[3].ID, hosts[4].ID}, // hosts[2] is no longer included in the no team summary }) // Enable disk encryption for the team tm.Config.MDM.EnableDiskEncryption = true tm, err = ds.SaveTeam(ctx, tm) require.NoError(t, err) require.NotNil(t, tm) require.True(t, tm.Config.MDM.EnableDiskEncryption) // Check the summary for the team checkExpected(t, &team.ID, hostIDsByDEStatus{ fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID}, // disk encryption is enabled for team so hosts[2] is counted }) // Check the summary for no team (should be unchanged) checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, fleet.DiskEncryptionEnforcing: []uint{hosts[3].ID, hosts[4].ID}, }) }) t.Run("BitLocker Windows server excluded", func(t *testing.T) { require.NoError(t, ds.SetOrUpdateMDMData(ctx, hosts[3].ID, true, // set is_server to true for hosts[3] true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false)) // Check Windows servers not counted checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, fleet.DiskEncryptionEnforcing: []uint{hosts[4].ID}, // hosts[3] is not counted }) }) t.Run("OS settings filters include Windows and macOS hosts", func(t *testing.T) { // Make macOS host fail disk encryption require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { HostUUID: hosts[5].UUID, ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier, ProfileName: "Disk encryption", ProfileUUID: "a" + uuid.NewString(), CommandUUID: uuid.New().String(), OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryFailed, Checksum: []byte("checksum"), Scope: fleet.PayloadScopeSystem, }, })) // Check that BitLocker summary does not include macOS hosts checkBitLockerSummary(t, nil, fleet.MDMWindowsBitLockerSummary{ Verified: 1, Verifying: 0, Failed: 1, Enforcing: 1, RemovingEnforcement: 0, ActionRequired: 0, }) // Check that filtered lists do include macOS hosts checkListHostsFilterDiskEncryption(t, nil, fleet.DiskEncryptionFailed, []uint{hosts[1].ID, hosts[5].ID}) checkListHostsFilterOSSettings(t, nil, fleet.OSSettingsFailed, []uint{hosts[1].ID, hosts[5].ID}) // delete the macOS host profile ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_profiles WHERE host_uuid = ? AND profile_identifier = ?`, hosts[5].UUID, mobileconfig.FleetFileVaultPayloadIdentifier) return err }) }) t.Run("BitLocker host disks must update to transition from Verifying to Verified", func(t *testing.T) { // we'll use hosts[4] as the target for this test targetHost := hosts[4] // confirm our initial state is as expected from previous tests // hosts[2] is was transferred to a team and is not counted // hosts[3] is a Windows server and is not counted checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, fleet.DiskEncryptionEnforcing: []uint{targetHost.ID}, // targetHost is initially enforcing }) // simulate targetHost previously reported encrypted for disk encryption detail query // results require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, targetHost.ID, true)) // manualy update host_disks for targetHost to encrypted and ensure updated_at // timestamp is in the past updateHostDisks(t, targetHost.ID, true, time.Now().Add(-3*time.Hour)) // simulate targetHost reporting disk encryption key _, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, targetHost, "test-key", "", ptr.Bool(true)) require.NoError(t, err) // check that targetHost is now counted as verifying (not verified because host_disks still needs to be updated) checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, fleet.DiskEncryptionVerifying: []uint{targetHost.ID}, }) // simulate targetHost reporting detail query results for disk encryption require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, targetHost.ID, true)) // status for targetHost now verified because SetOrUpdateHostDisksEncryption always sets host_disks.updated_at // to the current timestamp even if the `encrypted` value hasn't changed checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID, targetHost.ID}, fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, }) }) }) } func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) { ctx := context.Background() checkMDMProfilesSummary := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) { ps, err := ds.GetMDMWindowsProfilesSummary(ctx, teamID) require.NoError(t, err) require.NotNil(t, ps) require.Equal(t, expected, *ps) } checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) { gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status}) require.NoError(t, err) if len(expectedIDs) != len(gotHosts) { gotIDs := make([]uint, len(gotHosts)) for i, h := range gotHosts { gotIDs[i] = h.ID } require.Len(t, gotHosts, len(expectedIDs), fmt.Sprintf("status: %s expected: %v got: %v", status, expectedIDs, gotIDs)) } for _, h := range gotHosts { require.Contains(t, expectedIDs, h.ID) } count, err := ds.CountHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status}) require.NoError(t, err) require.Equal(t, len(expectedIDs), count, "status: %s", status) } type hostIDsByProfileStatus map[fleet.MDMDeliveryStatus][]uint checkExpected := func(t *testing.T, teamID *uint, ep hostIDsByProfileStatus) { checkMDMProfilesSummary(t, teamID, fleet.MDMProfilesSummary{ Pending: uint(len(ep[fleet.MDMDeliveryPending])), Failed: uint(len(ep[fleet.MDMDeliveryFailed])), Verifying: uint(len(ep[fleet.MDMDeliveryVerifying])), Verified: uint(len(ep[fleet.MDMDeliveryVerified])), }) checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, ep[fleet.MDMDeliveryVerified]) checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, ep[fleet.MDMDeliveryVerifying]) checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, ep[fleet.MDMDeliveryFailed]) checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, ep[fleet.MDMDeliveryPending]) } upsertHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, status *fleet.MDMDeliveryStatus) { ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { // Generate a command UUID for the profile commandUUID := "cmd-" + profUUID stmt := `INSERT INTO host_mdm_windows_profiles (host_uuid, profile_uuid, status, command_uuid) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?` _, err := q.ExecContext(ctx, stmt, hostUUID, profUUID, status, commandUUID, status) if err != nil { return err } stmt = `UPDATE host_mdm_windows_profiles SET operation_type = ? WHERE host_uuid = ? AND profile_uuid = ?` _, err = q.ExecContext(ctx, stmt, fleet.MDMOperationTypeInstall, hostUUID, profUUID) return err }) } cleanupTables := func(t *testing.T) { ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `DELETE FROM host_mdm_windows_profiles`) return err }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `DELETE FROM host_disk_encryption_keys`) return err }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, `DELETE FROM host_disks`) return err }) } updateHostDisks := func(t *testing.T, hostID uint, encrypted bool, updated_at time.Time) { ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { stmt := `UPDATE host_disks SET encrypted = ?, updated_at = ? where host_id = ?` _, err := q.ExecContext(ctx, stmt, encrypted, updated_at, hostID) return err }) } // Create some hosts var hosts []*fleet.Host uuidToDeviceID := map[string]string{} for i := 0; i < 10; i++ { p := "windows" if i >= 5 { p = "darwin" } u := uuid.New().String() h, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), NodeKey: &u, UUID: u, Hostname: u, Platform: p, }) require.NoError(t, err) require.NotNil(t, h) hosts = append(hosts, h) require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false)) if p == "windows" { uuidToDeviceID[h.UUID] = windowsEnroll(t, ds, h) } } t.Run("profiles summary accounts for bitlocker status", func(t *testing.T) { t.Run("bitlocker disabled", func(t *testing.T) { ac, err := ds.AppConfig(ctx) require.NoError(t, err) ac.MDM.EnableDiskEncryption = optjson.SetBool(false) require.NoError(t, ds.SaveAppConfig(ctx, ac)) ac, err = ds.AppConfig(ctx) require.NoError(t, err) require.False(t, ac.MDM.EnableDiskEncryption.Value) expected := hostIDsByProfileStatus{} // no hosts are counted because no profiles and disk encryption is not enabled checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending) expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed) expected[fleet.MDMDeliveryFailed] = []uint{hosts[1].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[2].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying) expected[fleet.MDMDeliveryVerifying] = []uint{hosts[2].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[3].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified) expected[fleet.MDMDeliveryVerified] = []uint{hosts[3].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[4].UUID, "some-windows-profile", nil) // nil status is treated as pending expected[fleet.MDMDeliveryPending] = append(expected[fleet.MDMDeliveryPending], hosts[4].ID) checkExpected(t, nil, expected) cleanupTables(t) }) t.Run("bitlocker enabled", func(t *testing.T) { ac, err := ds.AppConfig(ctx) require.NoError(t, err) ac.MDM.EnableDiskEncryption = optjson.SetBool(true) require.NoError(t, ds.SaveAppConfig(ctx, ac)) ac, err = ds.AppConfig(ctx) require.NoError(t, err) require.True(t, ac.MDM.EnableDiskEncryption.Value) t.Run("bitlocker pending", func(t *testing.T) { expected := hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, } // all hosts are pending because no profiles and disk encryption is enabled checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending) // hosts[0] status pending because both profiles status and bitlocker status are pending checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[1].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed) // status for hosts[1] now failed because any failed status determines MDM aggregate status expected[fleet.MDMDeliveryFailed] = []uint{hosts[1].ID} expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[2].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying) // status for hosts[2] still pending because bitlocker pending status takes precedence over // profiles verifying status checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[3].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified) // status for hosts[3] still pending because bitlocker pending status takes precedence over // profiles verified status checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[4].UUID, "some-windows-profile", nil) // hosts[0] status pending because bitlocker status is pending and nil profile status is // also treated as pending checkExpected(t, nil, expected) cleanupTables(t) }) t.Run("bitlocker verifying", func(t *testing.T) { expected := hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, } // all hosts are pending because no profiles and disk encryption is enabled checkExpected(t, nil, expected) require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) _, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true)) require.NoError(t, err) // simulate bitlocker verifying status by ensuring host_disks updated at timestamp is before host_disk_encryption_key updateHostDisks(t, hosts[0].ID, true, time.Now().Add(-10*time.Minute)) // status for hosts[0] now verifying because bitlocker status is verifying and host[0] has // no profiles expected[fleet.MDMDeliveryVerifying] = []uint{hosts[0].ID} expected[fleet.MDMDeliveryPending] = []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed) // status for hosts[0] now failed because any failed status takes precedence expected = hostIDsByProfileStatus{ fleet.MDMDeliveryFailed: []uint{hosts[0].ID}, fleet.MDMDeliveryVerifying: []uint{}, fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, } checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending) // status for hosts[0] now pending because profiles status pendiing takes precedence over bitlocker status verifying expected[fleet.MDMDeliveryFailed] = []uint{} expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying) // status for hosts[0] now verifying because both profiles status and bitlocker status are verifying expected[fleet.MDMDeliveryPending] = []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} expected[fleet.MDMDeliveryVerifying] = []uint{hosts[0].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified) // status for hosts[0] still verifying because bitlocker status verifying takes // precedence over profile status verified checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", nil) // status for hosts[0] now pending because nil profile status is treated as pending and // pending status takes precedence over bitlocker status verifying expected[fleet.MDMDeliveryVerifying] = []uint{} expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} checkExpected(t, nil, expected) cleanupTables(t) }) t.Run("bitlocker verified", func(t *testing.T) { expected := hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, } // all hosts are pending because no profiles and disk encryption is enabled checkExpected(t, nil, expected) _, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true)) require.NoError(t, err) // status is still pending because hosts_disks hasn't been updated yet checkExpected(t, nil, expected) require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) // status for hosts[0] now verified because bitlocker status is verified and host[0] has // no profiles checkExpected(t, nil, hostIDsByProfileStatus{ fleet.MDMDeliveryVerified: []uint{hosts[0].ID}, fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed) // status for hosts[0] now failed because any failed status takes precedence expected = hostIDsByProfileStatus{ fleet.MDMDeliveryFailed: []uint{hosts[0].ID}, fleet.MDMDeliveryVerified: []uint{}, fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, } checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending) // status for hosts[0] now pending because profiles status pendiing takes precedence over bitlocker status verified expected[fleet.MDMDeliveryFailed] = []uint{} expected[fleet.MDMDeliveryPending] = []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying) // status for hosts[0] now verifying because profiles status verifying takes precedence over // bitlocker status verified expected[fleet.MDMDeliveryPending] = []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} expected[fleet.MDMDeliveryVerifying] = []uint{hosts[0].ID} checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified) // status for hosts[0] now verified because both profiles status and bitlocker status are verified expected[fleet.MDMDeliveryPending] = []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} expected[fleet.MDMDeliveryVerified] = []uint{hosts[0].ID} expected[fleet.MDMDeliveryVerifying] = []uint{} checkExpected(t, nil, expected) cleanupTables(t) }) t.Run("BitLocker host disks must update to transition from Verifying to Verified", func(t *testing.T) { // all hosts are pending because no profiles and disk encryption is enabled checkExpected(t, nil, hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) // simulate host already has encrypted disks require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) // manualy update host_disks for hosts[0] to encrypted and ensure updated_at // timestamp is in the past updateHostDisks(t, hosts[0].ID, true, time.Now().Add(-2*time.Hour)) _, err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true)) require.NoError(t, err) // status is verifying because hosts_disks hasn't been updated again checkExpected(t, nil, hostIDsByProfileStatus{ fleet.MDMDeliveryVerifying: []uint{hosts[0].ID}, fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) // status for hosts[0] now verified because SetOrUpdateHostDisksEncryption always sets host_disks.updated_at // to the current timestamp even if the `encrypted` value hasn't changed checkExpected(t, nil, hostIDsByProfileStatus{ fleet.MDMDeliveryVerified: []uint{hosts[0].ID}, fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) cleanupTables(t) }) t.Run("bitlocker failed", func(t *testing.T) { expected := hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, } // all hosts are pending because no profiles and disk encryption is enabled checkExpected(t, nil, expected) _, err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "", "some-bitlocker-error", nil) require.NoError(t, err) // status for hosts[0] now failed because any failed status takes precedence expected = hostIDsByProfileStatus{ fleet.MDMDeliveryFailed: []uint{hosts[0].ID}, fleet.MDMDeliveryPending: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, } checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryFailed) // status for hosts[0] still failed because any failed status takes precedence checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryPending) // status for hosts[0] still failed because bitlocker status failed takes precedence // over profiles status pending checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerifying) // status for hosts[0] still failed because bitlocker status failed takes precedence // over profiles status verifying checkExpected(t, nil, expected) upsertHostProfileStatus(t, hosts[0].UUID, "some-windows-profile", &fleet.MDMDeliveryVerified) // status for hosts[0] still failed because bitlocker status failed takes precedence // over profiles status verified checkExpected(t, nil, expected) cleanupTables(t) }) // turn off disk encryption so that the rest of the tests can focus on profiles status ac.MDM.EnableDiskEncryption = optjson.SetBool(false) require.NoError(t, ds.SaveAppConfig(ctx, ac)) }) }) t.Run("profiles summary accounts for host profiles with mixed statuses", func(t *testing.T) { for i := 0; i < 5; i++ { // upsert five profiles for hosts[0] with nil statuses upsertHostProfileStatus(t, hosts[0].UUID, fmt.Sprintf("some-windows-profile-%d", i), nil) // upsert five profiles for hosts[1] with pending statuses upsertHostProfileStatus(t, hosts[1].UUID, fmt.Sprintf("some-windows-profile-%d", i), &fleet.MDMDeliveryPending) // upsert five profiles for hosts[2] with verifying statuses upsertHostProfileStatus(t, hosts[2].UUID, fmt.Sprintf("some-windows-profile-%d", i), &fleet.MDMDeliveryVerifying) // upsert five profiles for hosts[3] with verified statuses upsertHostProfileStatus(t, hosts[3].UUID, fmt.Sprintf("some-windows-profile-%d", i), &fleet.MDMDeliveryVerified) // upsert five profiles for hosts[4] with failed statuses upsertHostProfileStatus(t, hosts[4].UUID, fmt.Sprintf("some-windows-profile-%d", i), &fleet.MDMDeliveryFailed) } expected := hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID}, fleet.MDMDeliveryVerifying: []uint{hosts[2].ID}, fleet.MDMDeliveryVerified: []uint{hosts[3].ID}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // add some other windows hosts that won't be be assigned any profiles otherHosts := make([]*fleet.Host, 0, 5) for i := 0; i < 5; i++ { u := uuid.New().String() h, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), NodeKey: &u, UUID: u, Hostname: u, Platform: "windows", }) require.NoError(t, err) require.NotNil(t, h) otherHosts = append(otherHosts, h) require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false)) windowsEnroll(t, ds, h) } checkExpected(t, nil, expected) // upsert some-profile-0 to failed status for hosts[0:4] for i := 0; i < 5; i++ { upsertHostProfileStatus(t, hosts[i].UUID, "some-windows-profile-0", &fleet.MDMDeliveryFailed) } expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{}, fleet.MDMDeliveryVerifying: []uint{}, fleet.MDMDeliveryVerified: []uint{}, fleet.MDMDeliveryFailed: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, } checkExpected(t, nil, expected) // upsert some-profile-0 to pending status for hosts[0:4] for i := 0; i < 5; i++ { upsertHostProfileStatus(t, hosts[i].UUID, "some-windows-profile-0", &fleet.MDMDeliveryPending) } expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID}, fleet.MDMDeliveryVerifying: []uint{}, fleet.MDMDeliveryVerified: []uint{}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // upsert some-profile-0 to verifying status for hosts[0:4] for i := 0; i < 5; i++ { upsertHostProfileStatus(t, hosts[i].UUID, "some-windows-profile-0", &fleet.MDMDeliveryVerifying) } expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID}, fleet.MDMDeliveryVerifying: []uint{hosts[2].ID, hosts[3].ID}, fleet.MDMDeliveryVerified: []uint{}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // upsert some-profile-0 to verified status for hosts[0:4] for i := 0; i < 5; i++ { upsertHostProfileStatus(t, hosts[i].UUID, "some-windows-profile-0", &fleet.MDMDeliveryVerified) } expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID}, fleet.MDMDeliveryVerifying: []uint{hosts[2].ID}, fleet.MDMDeliveryVerified: []uint{hosts[3].ID}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // turn on disk encryption ac, err := ds.AppConfig(ctx) require.NoError(t, err) ac.MDM.EnableDiskEncryption = optjson.SetBool(true) require.NoError(t, ds.SaveAppConfig(ctx, ac)) // hosts[0:3] are now pending because disk encryption is enabled, hosts[4] is still failed, // and other hosts are now counted as pending expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, otherHosts[0].ID, otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID}, fleet.MDMDeliveryVerifying: []uint{}, fleet.MDMDeliveryVerified: []uint{}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // create a new team t1, err := ds.NewTeam(ctx, &fleet.Team{Name: uuid.NewString()}) require.NoError(t, err) require.NotNil(t, t1) // transfer hosts[1:2] to the team require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&t1.ID, []uint{hosts[1].ID, hosts[2].ID}))) // hosts[1:2] now counted for the team, hosts[2] is counted as verifying again because // disk encryption is not enabled for the team expectedTeam1 := hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[1].ID}, fleet.MDMDeliveryVerifying: []uint{hosts[2].ID}, } checkExpected(t, &t1.ID, expectedTeam1) // hosts[1:2] are not counted for no team expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[3].ID, otherHosts[0].ID, otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // report otherHosts[0] as a server require.NoError(t, ds.SetOrUpdateMDMData(ctx, otherHosts[0].ID, true, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false)) // otherHosts[0] is no longer counted expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[3].ID, otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // report hosts[0] as a server require.NoError(t, ds.SetOrUpdateMDMData(ctx, hosts[0].ID, true, true, "https://example.com", false, fleet.WellKnownMDMFleet, "", false)) // hosts[0] is no longer counted expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{hosts[3].ID, otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // unenroll hosts[3] require.NoError(t, ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, uuidToDeviceID[hosts[3].UUID])) // hosts[3] is no longer counted expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID}, fleet.MDMDeliveryFailed: []uint{hosts[4].ID}, } checkExpected(t, nil, expected) // report hosts[4] as enrolled to a different MDM require.NoError(t, ds.SetOrUpdateMDMData(ctx, hosts[4].ID, false, true, "https://some-other-mdm.example.com", false, "some-other-mdm", "", false)) require.NoError(t, ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, uuidToDeviceID[hosts[4].UUID])) // hosts[4] is no longer counted expected = hostIDsByProfileStatus{ fleet.MDMDeliveryPending: []uint{otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID}, } checkExpected(t, nil, expected) cleanupTables(t) // turn off disk encryption for future tests ac.MDM.EnableDiskEncryption = optjson.SetBool(false) require.NoError(t, ds.SaveAppConfig(ctx, ac)) }) } func testMDMWindowsInsertCommandForHosts(t *testing.T, ds *Datastore) { ctx := context.Background() d1 := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: uuid.New().String(), MDMHardwareID: uuid.New().String() + uuid.New().String(), MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "CIMClient_Windows", MDMDeviceName: "DESKTOP-1C3ARC1", MDMEnrollType: "ProgrammaticEnrollment", MDMEnrollUserID: "", MDMEnrollProtoVersion: "5.0", MDMEnrollClientVersion: "10.0.19045.2965", MDMNotInOOBE: false, HostUUID: uuid.NewString(), } d2 := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: uuid.New().String(), MDMHardwareID: uuid.New().String() + uuid.New().String(), MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "CIMClient_Windows", MDMDeviceName: "DESKTOP-1C3ARC1", MDMEnrollType: "ProgrammaticEnrollment", MDMEnrollUserID: "", MDMEnrollProtoVersion: "5.0", MDMEnrollClientVersion: "10.0.19045.2965", MDMNotInOOBE: false, HostUUID: uuid.NewString(), } err := ds.MDMWindowsInsertEnrolledDevice(ctx, d1) require.NoError(t, err) err = ds.MDMWindowsInsertEnrolledDevice(ctx, d2) require.NoError(t, err) cmd := &fleet.MDMWindowsCommand{ CommandUUID: uuid.NewString(), RawCommand: []byte(""), TargetLocURI: "./test/uri", } err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{}, cmd) require.NoError(t, err) // no commands are enqueued nor created cmds, err := ds.MDMWindowsGetPendingCommands(ctx, d1.MDMDeviceID) require.NoError(t, err) require.Empty(t, cmds) cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d2.MDMDeviceID) require.NoError(t, err) require.Empty(t, cmds) err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{d1.HostUUID, d2.HostUUID}, cmd) require.NoError(t, err) // command enqueued and created cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d1.MDMDeviceID) require.NoError(t, err) require.Len(t, cmds, 1) cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d2.MDMDeviceID) require.NoError(t, err) require.Len(t, cmds, 1) // commands can be added by device id as well cmd.CommandUUID = uuid.NewString() err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{d1.MDMDeviceID, d2.MDMDeviceID}, cmd) require.NoError(t, err) // command enqueued and created cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d1.MDMDeviceID) require.NoError(t, err) require.Len(t, cmds, 2) cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d2.MDMDeviceID) require.NoError(t, err) require.Len(t, cmds, 2) // create a device that enrolls with the same device id and uuid as d1 // but a different hardware id (simulates the issue in #20764). d3 := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: d1.MDMDeviceID, MDMHardwareID: uuid.New().String() + uuid.New().String(), MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "CIMClient_Windows", MDMDeviceName: "DESKTOP-1C3ARC1", MDMEnrollType: "ProgrammaticEnrollment", MDMEnrollUserID: "", MDMEnrollProtoVersion: "5.0", MDMEnrollClientVersion: "10.0.19045.2965", MDMNotInOOBE: false, HostUUID: d1.HostUUID, } time.Sleep(time.Second) // ensure it gets a latest created_at err = ds.MDMWindowsInsertEnrolledDevice(ctx, d3) require.NoError(t, err) // commands can still be enqueued, will be enqueued for the latest enrolled device // when a duplicate host uuid/device id exists (i.e. for d3 even if d2 is passed - // they have the same ids). cmd.CommandUUID = uuid.NewString() err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{d1.MDMDeviceID, d2.MDMDeviceID}, cmd) require.NoError(t, err) // command enqueued and created cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d1.MDMDeviceID) require.NoError(t, err) require.Len(t, cmds, 3) cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d2.MDMDeviceID) require.NoError(t, err) require.Len(t, cmds, 3) // d2 sees the new command as we retrieve by device_id and they share the same cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d3.MDMDeviceID) require.NoError(t, err) require.Len(t, cmds, 3) } func testMDMWindowsGetPendingCommands(t *testing.T, ds *Datastore) { ctx := context.Background() d := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: uuid.New().String(), MDMHardwareID: uuid.New().String() + uuid.New().String(), MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "CIMClient_Windows", MDMDeviceName: "DESKTOP-1C3ARC1", MDMEnrollType: "ProgrammaticEnrollment", MDMEnrollUserID: "", MDMEnrollProtoVersion: "5.0", MDMEnrollClientVersion: "10.0.19045.2965", MDMNotInOOBE: false, HostUUID: uuid.NewString(), } err := ds.MDMWindowsInsertEnrolledDevice(ctx, d) require.NoError(t, err) // device without commands cmds, err := ds.MDMWindowsGetPendingCommands(ctx, d.MDMDeviceID) require.NoError(t, err) require.Empty(t, cmds) // device with commands cmd := &fleet.MDMWindowsCommand{ CommandUUID: uuid.NewString(), RawCommand: []byte(""), TargetLocURI: "./test/uri", } err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{d.HostUUID}, cmd) require.NoError(t, err) cmds, err = ds.MDMWindowsGetPendingCommands(ctx, d.MDMDeviceID) require.NoError(t, err) require.Len(t, cmds, 1) // non-existent device cmds, err = ds.MDMWindowsGetPendingCommands(ctx, "fail") require.NoError(t, err) require.Empty(t, cmds) } func testMDMWindowsCommandResults(t *testing.T, ds *Datastore) { ctx := context.Background() insertDB := func(t *testing.T, query string, args ...interface{}) (int64, error) { t.Helper() res, err := ds.writer(ctx).Exec(query, args...) if err != nil { return 0, err } return res.LastInsertId() } dev := createMDMWindowsEnrollment(ctx, t, ds) var enrollmentID uint require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &enrollmentID, `SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev.MDMDeviceID)) _, err := ds.writer(ctx).ExecContext(ctx, `UPDATE mdm_windows_enrollments SET host_uuid = ? WHERE id = ?`, dev.HostUUID, enrollmentID) require.NoError(t, err) rawCmd := "some-command" cmdUUID := "some-uuid" cmdTarget := "some-target-loc-uri" _, err = insertDB(t, `INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri) VALUES (?, ?, ?)`, cmdUUID, rawCmd, cmdTarget) require.NoError(t, err) rawResponse := []byte("some-response") responseID, err := insertDB(t, `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`, enrollmentID, rawResponse) require.NoError(t, err) rawResult := []byte("some-result") statusCode := "200" _, err = insertDB(t, `INSERT INTO windows_mdm_command_results (enrollment_id, command_uuid, raw_result, response_id, status_code) VALUES (?, ?, ?, ?, ?)`, enrollmentID, cmdUUID, rawResult, responseID, statusCode) require.NoError(t, err) // Create multiple command queue entries to ensure no duplicated rows. dev2 := createEnrolledDevice(t, ds) dev3 := createEnrolledDevice(t, ds) var enrollmentID2 uint var enrollmentID3 uint require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &enrollmentID2, `SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev2.MDMDeviceID)) require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &enrollmentID3, `SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev3.MDMDeviceID)) // Insert queue entry for BOTH enrollments _, err = ds.writer(ctx).ExecContext(ctx, `INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid) VALUES (?, ?), (?, ?)`, enrollmentID2, cmdUUID, enrollmentID3, cmdUUID, ) require.NoError(t, err) p, err := ds.GetMDMCommandPlatform(ctx, cmdUUID) require.NoError(t, err) require.Equal(t, "windows", p) results, err := ds.GetMDMWindowsCommandResults(ctx, cmdUUID, "") require.NoError(t, err) require.Len(t, results, 1) require.Equal(t, dev.HostUUID, results[0].HostUUID) require.Equal(t, cmdUUID, results[0].CommandUUID) require.Equal(t, rawResponse, results[0].Result) require.Equal(t, cmdTarget, results[0].RequestType) require.Equal(t, statusCode, results[0].Status) require.Empty(t, results[0].Hostname) // populated only at the service layer require.Equal(t, rawCmd, string(results[0].Payload)) p, err = ds.GetMDMCommandPlatform(ctx, "unknown-cmd-uuid") require.True(t, fleet.IsNotFound(err)) require.Empty(t, p) results, err = ds.GetMDMWindowsCommandResults(ctx, "unknown-cmd-uuid", "") require.NoError(t, err) // expect no error here, just no results require.Empty(t, results) } func createMDMWindowsEnrollment(ctx context.Context, t *testing.T, ds *Datastore) *fleet.MDMWindowsEnrolledDevice { h, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-win-host-name", OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: "test-win-host-uuid", Platform: "windows", }) require.NoError(t, err) dev := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: "test-device-id", MDMHardwareID: "test-hardware-id", MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "dt", MDMDeviceName: "dn", MDMEnrollType: "et", MDMEnrollUserID: "euid", MDMEnrollProtoVersion: "epv", MDMEnrollClientVersion: "ecv", MDMNotInOOBE: false, HostUUID: h.UUID, } require.NoError(t, ds.MDMWindowsInsertEnrolledDevice(ctx, dev)) return dev } func testMDMWindowsCommandResultsWithPendingResult(t *testing.T, ds *Datastore) { ctx := context.Background() insertDB := func(t *testing.T, query string, args ...interface{}) (int64, error) { t.Helper() res, err := ds.writer(ctx).Exec(query, args...) if err != nil { return 0, err } return res.LastInsertId() } h, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-win-host-name", OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: "test-win-host-uuid", Platform: "windows", }) require.NoError(t, err) dev := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: "test-device-id", MDMHardwareID: "test-hardware-id", MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "dt", MDMDeviceName: "dn", MDMEnrollType: "et", MDMEnrollUserID: "euid", MDMEnrollProtoVersion: "epv", MDMEnrollClientVersion: "ecv", MDMNotInOOBE: false, HostUUID: h.UUID, } require.NoError(t, ds.MDMWindowsInsertEnrolledDevice(ctx, dev)) var enrollmentID uint require.NoError(t, sqlx.GetContext(ctx, ds.writer(ctx), &enrollmentID, `SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev.MDMDeviceID)) _, err = ds.writer(ctx).ExecContext(ctx, `UPDATE mdm_windows_enrollments SET host_uuid = ? WHERE id = ?`, dev.HostUUID, enrollmentID) require.NoError(t, err) rawCmd := "some-command" cmdUUID := "some-uuid" cmdTarget := "some-target-loc-uri" _, err = insertDB(t, `INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri) VALUES (?, ?, ?)`, cmdUUID, rawCmd, cmdTarget) require.NoError(t, err) _, err = insertDB(t, `INSERT INTO windows_mdm_command_queue (enrollment_id, command_uuid) VALUES (?, ? )`, enrollmentID, cmdUUID) require.NoError(t, err) p, err := ds.GetMDMCommandPlatform(ctx, cmdUUID) require.NoError(t, err) require.Equal(t, "windows", p) results, err := ds.GetMDMWindowsCommandResults(ctx, cmdUUID, "") require.NoError(t, err) require.Len(t, results, 1) require.Equal(t, dev.HostUUID, results[0].HostUUID) require.Equal(t, cmdUUID, results[0].CommandUUID) require.Equal(t, []byte{}, results[0].Result) require.Equal(t, cmdTarget, results[0].RequestType) require.Equal(t, "101", results[0].Status) require.Empty(t, results[0].Hostname) // populated only at the service layer require.Equal(t, rawCmd, string(results[0].Payload)) p, err = ds.GetMDMCommandPlatform(ctx, "unknown-cmd-uuid") require.True(t, fleet.IsNotFound(err)) require.Empty(t, p) results, err = ds.GetMDMWindowsCommandResults(ctx, "unknown-cmd-uuid", "") require.NoError(t, err) // expect no error here, just no results require.Empty(t, results) } // enrolls the host in Windows MDM and returns the device's enrollment ID. func windowsEnroll(t *testing.T, ds fleet.Datastore, h *fleet.Host) string { ctx := context.Background() d1 := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: uuid.New().String(), MDMHardwareID: uuid.New().String() + uuid.New().String(), MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "CIMClient_Windows", MDMDeviceName: "DESKTOP-1C3ARC1", MDMEnrollType: "ProgrammaticEnrollment", MDMEnrollUserID: "", MDMEnrollProtoVersion: "5.0", MDMEnrollClientVersion: "10.0.19045.2965", MDMNotInOOBE: false, HostUUID: h.UUID, } err := ds.MDMWindowsInsertEnrolledDevice(ctx, d1) require.NoError(t, err) return d1.MDMDeviceID } func testMDMWindowsProfileManagement(t *testing.T, ds *Datastore) { ctx := context.Background() globalProfiles := []string{ InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), } // if there are no hosts, then no profiles need to be installed profiles, err := ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) require.Empty(t, profiles) host1, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host1-name", OsqueryHostID: ptr.String("1337"), NodeKey: ptr.String("1337"), UUID: "test-uuid-1", TeamID: nil, Platform: "windows", }) require.NoError(t, err) windowsEnroll(t, ds, host1) // non-Windows hosts shouldn't modify any of the results below _, err = ds.NewHost(ctx, &fleet.Host{ Hostname: "test-macos-host", OsqueryHostID: ptr.String("4824"), NodeKey: ptr.String("4824"), UUID: "test-macos-host", TeamID: nil, Platform: "macos", }) require.NoError(t, err) // a windows host that's not MDM enrolled into Fleet shouldn't // modify any of the results below _, err = ds.NewHost(ctx, &fleet.Host{ Hostname: "test-non-mdm-host", OsqueryHostID: ptr.String("4825"), NodeKey: ptr.String("4825"), UUID: "test-non-mdm-host", TeamID: nil, Platform: "windows", }) require.NoError(t, err) profilesMatch := func(t *testing.T, want []string, profs []*fleet.MDMWindowsProfilePayload) { got := []string{} for _, prof := range profs { got = append(got, prof.ProfileUUID) } require.ElementsMatch(t, want, got) } // global profiles to install on the newly added host profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) profilesMatch(t, globalProfiles, profiles) // add another host, it belongs to a team team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"}) require.NoError(t, err) host2, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host2-name", OsqueryHostID: ptr.String("1338"), NodeKey: ptr.String("1338"), UUID: "test-uuid-2", TeamID: &team.ID, Platform: "windows", }) require.NoError(t, err) windowsEnroll(t, ds, host2) // still the same profiles to assign as there are no profiles for team 1 profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) profilesMatch(t, globalProfiles, profiles) // assign profiles to team 1 teamProfiles := []string{ InsertWindowsProfileForTest(t, ds, team.ID), InsertWindowsProfileForTest(t, ds, team.ID), } // new profiles, this time for the new host belonging to team 1 profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) profilesMatch(t, append(globalProfiles, teamProfiles...), profiles) // add another global host host3, err := ds.NewHost(ctx, &fleet.Host{ Hostname: "test-host3-name", OsqueryHostID: ptr.String("1339"), NodeKey: ptr.String("1339"), UUID: "test-uuid-3", TeamID: nil, Platform: "windows", }) require.NoError(t, err) windowsEnroll(t, ds, host3) // more profiles, this time for both global hosts and the team profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) profilesMatch(t, append(globalProfiles, append(globalProfiles, teamProfiles...)...), profiles) profileByUUID := make(map[string]*fleet.MDMWindowsProfilePayload, len(profiles)) for _, prof := range profiles { profileByUUID[prof.ProfileUUID] = prof } // cron runs and updates the status err = ds.BulkUpsertMDMWindowsHostProfiles( ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{ { ProfileUUID: globalProfiles[0], ProfileName: "foo", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: profileByUUID[globalProfiles[0]].Checksum, }, { ProfileUUID: globalProfiles[0], ProfileName: "foo", HostUUID: "test-uuid-3", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: profileByUUID[globalProfiles[0]].Checksum, }, { ProfileUUID: globalProfiles[1], ProfileName: "foo", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: profileByUUID[globalProfiles[1]].Checksum, }, { ProfileUUID: globalProfiles[1], ProfileName: "foo", HostUUID: "test-uuid-3", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: profileByUUID[globalProfiles[1]].Checksum, }, { ProfileUUID: globalProfiles[2], ProfileName: "foo", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: profileByUUID[globalProfiles[2]].Checksum, }, { ProfileUUID: globalProfiles[2], ProfileName: "foo", HostUUID: "test-uuid-3", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: profileByUUID[globalProfiles[2]].Checksum, }, { ProfileUUID: teamProfiles[0], ProfileName: "foo", HostUUID: "test-uuid-2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: profileByUUID[teamProfiles[0]].Checksum, }, { ProfileUUID: teamProfiles[1], ProfileName: "foo", HostUUID: "test-uuid-2", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: profileByUUID[teamProfiles[1]].Checksum, }, }, ) require.NoError(t, err) // no profiles left to install profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) require.Empty(t, profiles) // no profiles to remove yet toRemove, err := ds.ListMDMWindowsProfilesToRemove(ctx) require.NoError(t, err) require.Empty(t, toRemove) // add host1 to team err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID})) require.NoError(t, err) // profiles to be added for host1 are now related to the team profiles, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) profilesMatch(t, teamProfiles, profiles) // profiles to be removed includes host1's old profiles toRemove, err = ds.ListMDMWindowsProfilesToRemove(ctx) require.NoError(t, err) profilesMatch(t, globalProfiles, toRemove) } func testBulkOperationsMDMWindowsHostProfiles(t *testing.T, ds *Datastore) { ctx := context.Background() profiles := []string{ InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), } getAllHostProfiles := func() []*fleet.MDMWindowsProfilePayload { var hostProfiles []*fleet.MDMWindowsProfilePayload ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { stmt := `SELECT profile_uuid, status, operation_type FROM host_mdm_windows_profiles ORDER BY profile_name ASC` return sqlx.SelectContext(ctx, q, &hostProfiles, stmt) }) return hostProfiles } // empty payloads is a noop err := ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{}) require.NoError(t, err) require.Empty(t, getAllHostProfiles()) // valid payload inserts new records err = ds.BulkUpsertMDMWindowsHostProfiles( ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{ { ProfileUUID: profiles[0], ProfileName: "A", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{0}, }, { ProfileUUID: profiles[1], ProfileName: "B", HostUUID: "test-uuid-3", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{1}, }, { ProfileUUID: profiles[2], ProfileName: "C", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{2}, }, { ProfileUUID: profiles[3], ProfileName: "D", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{3}, }, { ProfileUUID: profiles[4], ProfileName: "E", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerifying, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{4}, }, }, ) require.NoError(t, err) hostsProfs := getAllHostProfiles() require.Len(t, hostsProfs, 5) for i, p := range hostsProfs { require.Equal(t, profiles[i], p.ProfileUUID) require.Equal(t, fleet.MDMOperationTypeInstall, p.OperationType) require.Equal(t, &fleet.MDMDeliveryVerifying, p.Status) } // valid payload updates existing records err = ds.BulkUpsertMDMWindowsHostProfiles( ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{ { ProfileUUID: profiles[0], ProfileName: "A", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{0}, }, { ProfileUUID: profiles[1], ProfileName: "B", HostUUID: "test-uuid-3", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{1}, }, { ProfileUUID: profiles[2], ProfileName: "C", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{2}, }, { ProfileUUID: profiles[3], ProfileName: "D", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{3}, }, { ProfileUUID: profiles[4], ProfileName: "E", HostUUID: "test-uuid-1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte{4}, }, }, ) require.NoError(t, err) hostsProfs = getAllHostProfiles() require.Len(t, hostsProfs, 5) for i, p := range hostsProfs { require.Equal(t, profiles[i], p.ProfileUUID) require.Equal(t, fleet.MDMOperationTypeInstall, p.OperationType) require.Equal(t, &fleet.MDMDeliveryVerified, p.Status) } // empty payload err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{}) require.NoError(t, err) hostsProfs = getAllHostProfiles() require.Len(t, hostsProfs, 5) // partial deletes err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{ { ProfileUUID: profiles[0], HostUUID: "test-uuid-1", }, { ProfileUUID: profiles[1], HostUUID: "test-uuid-3", }, { ProfileUUID: profiles[2], HostUUID: "test-uuid-1", }, }) require.NoError(t, err) hostsProfs = getAllHostProfiles() require.Len(t, hostsProfs, 2) // full deletes err = ds.BulkDeleteMDMWindowsHostsConfigProfiles(ctx, []*fleet.MDMWindowsProfilePayload{ { ProfileUUID: profiles[0], HostUUID: "test-uuid-1", }, { ProfileUUID: profiles[1], HostUUID: "test-uuid-3", }, { ProfileUUID: profiles[2], HostUUID: "test-uuid-1", }, { ProfileUUID: profiles[3], HostUUID: "test-uuid-1", }, { ProfileUUID: profiles[4], HostUUID: "test-uuid-1", }, }) require.NoError(t, err) hostsProfs = getAllHostProfiles() require.Len(t, hostsProfs, 0) } func testBulkOperationsMDMWindowsHostProfilesBatch2(t *testing.T, ds *Datastore) { ds.testUpsertMDMDesiredProfilesBatchSize = 2 ds.testDeleteMDMProfilesBatchSize = 2 t.Cleanup(func() { ds.testUpsertMDMDesiredProfilesBatchSize = 0 ds.testDeleteMDMProfilesBatchSize = 0 }) testBulkOperationsMDMWindowsHostProfiles(t, ds) } func testBulkOperationsMDMWindowsHostProfilesBatch3(t *testing.T, ds *Datastore) { ds.testUpsertMDMDesiredProfilesBatchSize = 3 ds.testDeleteMDMProfilesBatchSize = 3 t.Cleanup(func() { ds.testUpsertMDMDesiredProfilesBatchSize = 0 ds.testDeleteMDMProfilesBatchSize = 0 }) testBulkOperationsMDMWindowsHostProfiles(t, ds) } func testGetMDMWindowsProfilesContents(t *testing.T, ds *Datastore) { ctx := context.Background() profileUUIDs := []string{ InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), } cases := []struct { ids []string want map[string]fleet.MDMWindowsProfileContents }{ {[]string{}, nil}, {nil, nil}, { []string{profileUUIDs[0]}, map[string]fleet.MDMWindowsProfileContents{profileUUIDs[0]: generateDummyWindowsProfileContents(profileUUIDs[0])}, }, { []string{profileUUIDs[0], profileUUIDs[1], profileUUIDs[2]}, map[string]fleet.MDMWindowsProfileContents{ profileUUIDs[0]: generateDummyWindowsProfileContents(profileUUIDs[0]), profileUUIDs[1]: generateDummyWindowsProfileContents(profileUUIDs[1]), profileUUIDs[2]: generateDummyWindowsProfileContents(profileUUIDs[2]), }, }, } for _, c := range cases { out, err := ds.GetMDMWindowsProfilesContents(ctx, c.ids) require.NoError(t, err) require.Equal(t, c.want, out) } } func testMDMWindowsConfigProfiles(t *testing.T, ds *Datastore) { ctx := context.Background() // create a couple Windows profiles for no-team (nil and 0 means no team) profA, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "a", TeamID: nil, SyncML: []byte("")}, nil) require.NoError(t, err) require.NotEmpty(t, profA.ProfileUUID) profB, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "b", TeamID: ptr.Uint(0), SyncML: []byte("")}, nil) require.NoError(t, err) require.NotEmpty(t, profB.ProfileUUID) // create an Apple profile for no-team profC, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("c", "c", 0), nil) require.NoError(t, err) require.NotZero(t, profC.ProfileID) require.NotEmpty(t, profC.ProfileUUID) // create the same name for team 1 as Windows profile profATm, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "a", TeamID: ptr.Uint(1), SyncML: []byte("")}, nil) require.NoError(t, err) require.NotEmpty(t, profATm.ProfileUUID) require.NotNil(t, profATm.TeamID) require.Equal(t, uint(1), *profATm.TeamID) // create the same B profile for team 1 as Apple profile profBTm, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("b", "b", 1), nil) require.NoError(t, err) require.NotZero(t, profBTm.ProfileID) require.NotEmpty(t, profBTm.ProfileUUID) var existsErr *existsError // create a duplicate of Windows for no-team _, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "b", TeamID: nil, SyncML: []byte("")}, nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate of Apple for no-team _, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "c", TeamID: nil, SyncML: []byte("")}, nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate of Windows for team _, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "a", TeamID: ptr.Uint(1), SyncML: []byte("")}, nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate of Apple for team _, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "b", TeamID: ptr.Uint(1), SyncML: []byte("")}, nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate name with an Apple profile for no-team _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 0), nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a duplicate name with an Apple profile for team _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 1), nil) require.Error(t, err) require.ErrorAs(t, err, &existsErr) // create a profile with labels that don't exist _, err = ds.NewMDMWindowsConfigProfile( ctx, fleet.MDMWindowsConfigProfile{ Name: "fake-labels", TeamID: nil, SyncML: []byte(""), LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: "foo", LabelID: 1}}, }, nil) require.NotNil(t, err) require.True(t, fleet.IsForeignKey(err)) label := &fleet.Label{ Name: "my label", Description: "a label", Query: "select 1 from processes;", } label, err = ds.NewLabel(ctx, label) require.NoError(t, err) // create a profile with a label that exists profWithLabel, err := ds.NewMDMWindowsConfigProfile( ctx, fleet.MDMWindowsConfigProfile{ Name: "with-labels", TeamID: nil, SyncML: []byte(""), LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: label.Name, LabelID: label.ID}}, }, nil) require.NoError(t, err) require.NotEmpty(t, profWithLabel.ProfileUUID) // get that profile with label prof, err := ds.GetMDMWindowsConfigProfile(ctx, profWithLabel.ProfileUUID) require.NoError(t, err) require.Len(t, prof.LabelsIncludeAll, 1) require.Equal(t, label.Name, prof.LabelsIncludeAll[0].LabelName) require.Equal(t, label.ID, prof.LabelsIncludeAll[0].LabelID) require.False(t, prof.LabelsIncludeAll[0].Broken) // break that profile by deleting the label require.NoError(t, ds.DeleteLabel(ctx, label.Name, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})) prof, err = ds.GetMDMWindowsConfigProfile(ctx, profWithLabel.ProfileUUID) require.NoError(t, err) require.Len(t, prof.LabelsIncludeAll, 1) require.Equal(t, label.Name, prof.LabelsIncludeAll[0].LabelName) require.Zero(t, prof.LabelsIncludeAll[0].LabelID) require.True(t, prof.LabelsIncludeAll[0].Broken) _, err = ds.GetMDMWindowsConfigProfile(ctx, "not-valid") require.Error(t, err) require.True(t, fleet.IsNotFound(err)) prof, err = ds.GetMDMWindowsConfigProfile(ctx, profA.ProfileUUID) require.NoError(t, err) require.Equal(t, profA.ProfileUUID, prof.ProfileUUID) require.NotNil(t, prof.TeamID) require.Zero(t, *prof.TeamID) require.Equal(t, "a", prof.Name) require.Equal(t, "", string(prof.SyncML)) require.NotZero(t, prof.CreatedAt) require.NotZero(t, prof.UploadedAt) require.Nil(t, prof.LabelsIncludeAll) err = ds.DeleteMDMWindowsConfigProfile(ctx, "not-valid") require.Error(t, err) require.True(t, fleet.IsNotFound(err)) err = ds.DeleteMDMWindowsConfigProfile(ctx, profA.ProfileUUID) require.NoError(t, err) } func testMDMWindowsConfigProfilesWithFleetVars(t *testing.T, ds *Datastore) { ctx := t.Context() // Test that usesFleetVars parameter correctly persists variables in the database // Create a profile with Fleet variables profWithVars, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{ Name: "profile_with_vars", TeamID: nil, SyncML: []byte("./Device/Vendor/MSFT/Test/$FLEET_VAR_HOST_UUID"), }, []fleet.FleetVarName{fleet.FleetVarHostUUID}) require.NoError(t, err) require.NotEmpty(t, profWithVars.ProfileUUID) // Query the mdm_configuration_profile_variables table to verify the variables were persisted var varNames []string stmt := ` SELECT fv.name FROM mdm_configuration_profile_variables mcpv JOIN fleet_variables fv ON mcpv.fleet_variable_id = fv.id WHERE mcpv.windows_profile_uuid = ? ORDER BY fv.name ` err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, profWithVars.ProfileUUID) require.NoError(t, err) // Assert that the returned variable names exactly match the provided slice // Note: the database stores the full name with FLEET_VAR_ prefix expectedVarNames := []string{"FLEET_VAR_" + string(fleet.FleetVarHostUUID)} require.Equal(t, expectedVarNames, varNames, "Variable names in database should match the provided usesFleetVars slice") // Test with empty usesFleetVars slice profNoVars, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{ Name: "profile_no_vars", TeamID: nil, SyncML: []byte("./Device/Vendor/MSFT/Test/NoVars"), }, []fleet.FleetVarName{}) require.NoError(t, err) require.NotEmpty(t, profNoVars.ProfileUUID) err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, profNoVars.ProfileUUID) require.NoError(t, err) require.Empty(t, varNames, "No variables should be persisted when usesFleetVars is empty") // Test with nil usesFleetVars profNilVars, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{ Name: "profile_nil_vars", TeamID: nil, SyncML: []byte("./Device/Vendor/MSFT/Test/NilVars"), }, nil) require.NoError(t, err) require.NotEmpty(t, profNilVars.ProfileUUID) err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, profNilVars.ProfileUUID) require.NoError(t, err) require.Empty(t, varNames, "No variables should be persisted when usesFleetVars is nil") // Test that BatchSetMDMProfiles properly clears stale variable associations // Create a team profile with variables teamProf1, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{ Name: "team_profile_1", TeamID: ptr.Uint(1), SyncML: []byte("./Device/Vendor/MSFT/Test/$FLEET_VAR_HOST_UUID"), }, []fleet.FleetVarName{fleet.FleetVarHostUUID}) require.NoError(t, err) // Verify the variable was persisted err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, teamProf1.ProfileUUID) require.NoError(t, err) require.Equal(t, expectedVarNames, varNames, "Team profile should have HOST_UUID variable") // Now update the profile via BatchSetMDMProfiles to remove the variable teamProf1Updated := &fleet.MDMWindowsConfigProfile{ Name: "team_profile_1", TeamID: ptr.Uint(1), SyncML: []byte("./Device/Vendor/MSFT/Test/NoVarsAnymore"), } // BatchSetMDMProfiles should process this profile and clear its variable associations // since the content no longer contains variables _, err = ds.BatchSetMDMProfiles(ctx, ptr.Uint(1), nil, []*fleet.MDMWindowsConfigProfile{teamProf1Updated}, nil, nil, nil) require.NoError(t, err) // Verify the variable associations were cleared err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, teamProf1.ProfileUUID) require.NoError(t, err) require.Empty(t, varNames, "Variables should be cleared when profile is updated without variables") // Create another team profile to test multiple profiles with mixed variables teamProf2, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{ Name: "team_profile_2", TeamID: ptr.Uint(1), SyncML: []byte("./Device/Vendor/MSFT/Test/Profile2"), }, nil) require.NoError(t, err) // Update both profiles - one adds variables, one keeps no variables teamProf1WithVarsAgain := &fleet.MDMWindowsConfigProfile{ Name: "team_profile_1", TeamID: ptr.Uint(1), SyncML: []byte("./Device/Vendor/MSFT/Test/WithVarsAgain/$FLEET_VAR_HOST_UUID"), } teamProf2NoChange := &fleet.MDMWindowsConfigProfile{ Name: "team_profile_2", TeamID: ptr.Uint(1), SyncML: []byte("./Device/Vendor/MSFT/Test/Profile2Updated"), } // Mock the profilesVariablesByIdentifier that would be passed from service layer profilesVars := []fleet.MDMProfileIdentifierFleetVariables{ {Identifier: "team_profile_1", FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostUUID}}, } _, err = ds.BatchSetMDMProfiles(ctx, ptr.Uint(1), nil, []*fleet.MDMWindowsConfigProfile{teamProf1WithVarsAgain, teamProf2NoChange}, nil, nil, profilesVars) require.NoError(t, err) // Verify profile 1 has variables again err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, teamProf1.ProfileUUID) require.NoError(t, err) require.Equal(t, expectedVarNames, varNames, "Profile 1 should have variables again") // Verify profile 2 still has no variables err = ds.writer(ctx).SelectContext(ctx, &varNames, stmt, teamProf2.ProfileUUID) require.NoError(t, err) require.Empty(t, varNames, "Profile 2 should still have no variables") } func testSetOrReplaceMDMWindowsConfigProfile(t *testing.T, ds *Datastore) { ctx := context.Background() getProfileByTeamAndName := func(tmID *uint, name string) *fleet.MDMWindowsConfigProfile { var prof fleet.MDMWindowsConfigProfile var teamID uint if tmID != nil { teamID = *tmID } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &prof, `SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`, teamID, name) }) return &prof } // nothing for no-team, nothing for team 1 expectWindowsProfiles(t, ds, nil, nil) expectWindowsProfiles(t, ds, ptr.Uint(1), nil) // create a profile for no-team cp1 := *windowsConfigProfileForTest(t, "N1", "N1") err := ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp1) require.NoError(t, err) profNoTmN1 := getProfileByTeamAndName(nil, "N1") // creating the same profile for Apple / no-team fails _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("N1", "I1", 0), nil) require.Error(t, err) cp1.UploadedAt = profNoTmN1.UploadedAt profs1 := expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp1}) // wait a second to ensure timestamps in the DB change time.Sleep(time.Second) // update the profile content for no-team cp2 := *windowsConfigProfileForTest(t, "N1", "N1.modified") err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp2) require.NoError(t, err) profNoTmN1b := getProfileByTeamAndName(nil, "N1") profs2 := expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2}) // profile UUIDs are the same require.Equal(t, profs1["N1"], profs2["N1"]) // uploaded_at is not the same require.False(t, profNoTmN1.UploadedAt.Equal(profNoTmN1b.UploadedAt)) // wait a second to ensure timestamps in the DB change time.Sleep(time.Second) // update the profile for no-team without change err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp2) require.NoError(t, err) cp2.UploadedAt = profNoTmN1b.UploadedAt expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2}) // create a profile for Apple and team 1 with that name works _, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("N1", "I1", 1), nil) require.NoError(t, err) // try to create that profile for Windows and team 1 fails cp3 := *windowsConfigProfileForTest(t, "N1", "N1") cp3.TeamID = ptr.Uint(1) err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp3) require.Error(t, err) expectWindowsProfiles(t, ds, ptr.Uint(1), nil) // create a profile with the same name for team 2 works cp4 := *windowsConfigProfileForTest(t, "N1", "N1") cp4.TeamID = ptr.Uint(2) err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp4) require.NoError(t, err) profs3 := expectWindowsProfiles(t, ds, ptr.Uint(2), []*fleet.MDMWindowsConfigProfile{&cp4}) // profile UUIDs are not the same as for no-team require.NotEqual(t, profs3["N1"], profs2["N1"]) // create a different profile for no-team cp5 := *windowsConfigProfileForTest(t, "N2", "N2") err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp5) require.NoError(t, err) expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2, &cp5}) // update that profile for no-team cp6 := *windowsConfigProfileForTest(t, "N2", "N2.modified") err = ds.SetOrUpdateMDMWindowsConfigProfile(ctx, cp6) require.NoError(t, err) expectWindowsProfiles(t, ds, nil, []*fleet.MDMWindowsConfigProfile{&cp2, &cp6}) } func testMDMWindowsProfileLabels(t *testing.T, ds *Datastore) { ctx := context.Background() // Create a windows host u := uuid.New().String() host, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), // Set this slightly in the past to test dynamic label exclusion LabelUpdatedAt: time.Now().Add(-5 * time.Second), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), NodeKey: &u, UUID: u, Hostname: u, Platform: "windows", }) require.NoError(t, err) windowsEnroll(t, ds, host) // "include-any" labels l1, err := ds.NewLabel(ctx, &fleet.Label{ Name: "include-any-label1", Description: "desc", Query: "select 1;", }) require.NoError(t, err) l2, err := ds.NewLabel(ctx, &fleet.Label{ Name: "include-any-label2", Description: "desc", Query: "select 1;", }) require.NoError(t, err) l3, err := ds.NewLabel(ctx, &fleet.Label{ Name: "include-any-label3", Description: "desc", Query: "select 1;", }) require.NoError(t, err) // include-all labels l4, err := ds.NewLabel(ctx, &fleet.Label{ Name: "include-all-label4", Description: "desc", Query: "select 1;", }) require.NoError(t, err) l5, err := ds.NewLabel(ctx, &fleet.Label{ Name: "include-all-label5", Description: "desc", Query: "select 1;", }) require.NoError(t, err) // exclude-any labels l6, err := ds.NewLabel(ctx, &fleet.Label{ Name: "exclude-any-label6", Description: "desc", Query: "select 1;", }) require.NoError(t, err) l7, err := ds.NewLabel(ctx, &fleet.Label{ Name: "exclude-any-label7", Description: "desc", LabelMembershipType: fleet.LabelMembershipTypeManual, }) require.NoError(t, err) // Create a profile with "include-any" with l1 includeAnyProf, err := ds.NewMDMWindowsConfigProfile( ctx, *windowsConfigProfileForTest(t, "prof-include-any", "./Foo/Bar", l1, l2, l3), nil, ) require.NoError(t, err) require.NotEmpty(t, includeAnyProf.ProfileUUID) profileChecksums := make(map[string][]byte) checksum := md5.Sum(includeAnyProf.SyncML) // nolint:gosec // used only to hash for efficient comparisons profileChecksums[includeAnyProf.ProfileUUID] = checksum[:] // Create a profile with "include-all" with l4 and l5 includeAllProf, err := ds.NewMDMWindowsConfigProfile( ctx, *windowsConfigProfileForTest(t, "prof-include-all", "./Foo/Bar", l4, l5), nil, ) require.NoError(t, err) require.NotEmpty(t, includeAllProf.ProfileUUID) checksum = md5.Sum(includeAllProf.SyncML) // nolint:gosec // used only to hash for efficient comparisons profileChecksums[includeAllProf.ProfileUUID] = checksum[:] // Create a profile with "exclude-any" with l6 and l7 excludeAnyProf, err := ds.NewMDMWindowsConfigProfile( ctx, *windowsConfigProfileForTest(t, "prof-exclude-any", "./Foo/Bar", l6, l7), nil, ) require.NoError(t, err) checksum = md5.Sum(excludeAnyProf.SyncML) // nolint:gosec // used only to hash for efficient comparisons profileChecksums[excludeAnyProf.ProfileUUID] = checksum[:] // Create a profile with "exclude-any" with l7 only since it is a manual label excludeAnyManualProf, err := ds.NewMDMWindowsConfigProfile( ctx, *windowsConfigProfileForTest(t, "prof-exclude-any-manual", "./Foo/Bar", l7), nil, ) require.NoError(t, err) checksum = md5.Sum(excludeAnyManualProf.SyncML) // nolint:gosec // used only to hash for efficient comparisons profileChecksums[excludeAnyManualProf.ProfileUUID] = checksum[:] // Connect the host and l1, l4, l5 err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l1.ID, host.ID}, {l4.ID, host.ID}, {l5.ID, host.ID}}) require.NoError(t, err) // We should see 3 profiles in the "to install" list profilesToInstall, err := ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ { ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[includeAllProf.ProfileUUID], }, { ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[includeAnyProf.ProfileUUID], }, { ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID], }, }, profilesToInstall) host.LabelUpdatedAt = time.Now().Add(1 * time.Second) err = ds.UpdateHost(ctx, host) require.NoError(t, err) // We should see all 4 profiles in the "to install" list profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ { ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[includeAllProf.ProfileUUID], }, { ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[includeAnyProf.ProfileUUID], }, { ProfileUUID: excludeAnyProf.ProfileUUID, ProfileName: excludeAnyProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyProf.ProfileUUID], }, { ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID], }, }, profilesToInstall) // Remove the l1<->host relationship, but add l2<->labelHost. The profile should still show // up since it's "include any" err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l1.ID, host.ID}}) require.NoError(t, err) err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l2.ID, host.ID}}) require.NoError(t, err) profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ { ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[includeAllProf.ProfileUUID], }, { ProfileUUID: includeAnyProf.ProfileUUID, ProfileName: includeAnyProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[includeAnyProf.ProfileUUID], }, { ProfileUUID: excludeAnyProf.ProfileUUID, ProfileName: excludeAnyProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyProf.ProfileUUID], }, { ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID], }, }, profilesToInstall) // Remove the l2<->host relationship. Since the profile is "include-any", it should no longer // show up err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l2.ID, host.ID}}) require.NoError(t, err) profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ { ProfileUUID: includeAllProf.ProfileUUID, ProfileName: includeAllProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[includeAllProf.ProfileUUID], }, { ProfileUUID: excludeAnyProf.ProfileUUID, ProfileName: excludeAnyProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyProf.ProfileUUID], }, { ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID], }, }, profilesToInstall) // Remove the l4<->host relationship. Since the profile is "include-all", it should no longer show // up even though the l5<->host connection is still there. err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{{l4.ID, host.ID}}) require.NoError(t, err) profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ { ProfileUUID: excludeAnyProf.ProfileUUID, ProfileName: excludeAnyProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyProf.ProfileUUID], }, { ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID], }, }, profilesToInstall) // Add a l6<->host relationship. The exclude-any profile with l6 and l7 should be gone now with only // the exclude-any-manual profile remaining. err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{{l6.ID, host.ID}}) require.NoError(t, err) profilesToInstall, err = ds.ListMDMWindowsProfilesToInstall(ctx) require.NoError(t, err) require.ElementsMatch(t, []*fleet.MDMWindowsProfilePayload{ { ProfileUUID: excludeAnyManualProf.ProfileUUID, ProfileName: excludeAnyManualProf.Name, HostUUID: host.UUID, Checksum: profileChecksums[excludeAnyManualProf.ProfileUUID], }, }, profilesToInstall) } func expectWindowsProfiles( t *testing.T, ds *Datastore, tmID *uint, want []*fleet.MDMWindowsConfigProfile, ) map[string]string { if tmID == nil { tmID = ptr.Uint(0) } var got []*fleet.MDMWindowsConfigProfile ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { ctx := context.Background() return sqlx.SelectContext(ctx, q, &got, `SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ?`, tmID) }) // create map of expected profiles keyed by name wantMap := make(map[string]*fleet.MDMWindowsConfigProfile, len(want)) for _, cp := range want { wantMap[cp.Name] = cp } // compare only the fields we care about, and build the resulting map of // profile name as key to profile UUID as value m := make(map[string]string) for _, gotp := range got { m[gotp.Name] = gotp.ProfileUUID if gotp.TeamID != nil && *gotp.TeamID == 0 { gotp.TeamID = nil } // ProfileUUID is non-empty and starts with "w", but otherwise we don't // care about it for test assertions. require.NotEmpty(t, gotp.ProfileUUID) require.True(t, strings.HasPrefix(gotp.ProfileUUID, "w")) gotp.ProfileUUID = "" gotp.CreatedAt = time.Time{} // if an expected uploaded_at timestamp is provided for this profile, keep // its value, otherwise clear it as we don't care about asserting its // value. if wantp := wantMap[gotp.Name]; wantp == nil || wantp.UploadedAt.IsZero() { gotp.UploadedAt = time.Time{} } } // order is not guaranteed require.ElementsMatch(t, want, got) return m } func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) { ctx := context.Background() applyAndExpect := func(newSet []*fleet.MDMWindowsConfigProfile, tmID *uint, want []*fleet.MDMWindowsConfigProfile, wantUpdated bool, ) map[string]string { err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { updatedDB, err := ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet, nil) require.NoError(t, err) assert.Equal(t, wantUpdated, updatedDB) return err }) require.NoError(t, err) return expectWindowsProfiles(t, ds, tmID, want) } getProfileByTeamAndName := func(tmID *uint, name string) *fleet.MDMWindowsConfigProfile { var prof fleet.MDMWindowsConfigProfile var teamID uint if tmID != nil { teamID = *tmID } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &prof, `SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`, teamID, name) }) return &prof } withTeamID := func(p *fleet.MDMWindowsConfigProfile, tmID uint) *fleet.MDMWindowsConfigProfile { p.TeamID = &tmID return p } withUploadedAt := func(p *fleet.MDMWindowsConfigProfile, ua time.Time) *fleet.MDMWindowsConfigProfile { p.UploadedAt = ua return p } // apply empty set for no-team applyAndExpect(nil, nil, nil, false) // apply single profile set for tm1 mTm1 := applyAndExpect([]*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N1", "l1"), }, ptr.Uint(1), []*fleet.MDMWindowsConfigProfile{ withTeamID(windowsConfigProfileForTest(t, "N1", "l1"), 1), }, true) profTm1N1 := getProfileByTeamAndName(ptr.Uint(1), "N1") // apply single profile set for no-team applyAndExpect([]*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N1", "l1"), }, nil, []*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N1", "l1"), }, true) // wait a second to ensure timestamps in the DB change time.Sleep(time.Second) // apply new profile set for tm1 mTm1b := applyAndExpect([]*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N1", "l1"), // unchanged windowsConfigProfileForTest(t, "N2", "l2"), }, ptr.Uint(1), []*fleet.MDMWindowsConfigProfile{ withUploadedAt(withTeamID(windowsConfigProfileForTest(t, "N1", "l1"), 1), profTm1N1.UploadedAt), withTeamID(windowsConfigProfileForTest(t, "N2", "l2"), 1), }, true) // uuid for N1-I1 is unchanged require.Equal(t, mTm1["I1"], mTm1b["I1"]) profTm1N2 := getProfileByTeamAndName(ptr.Uint(1), "N2") // wait a second to ensure timestamps in the DB change time.Sleep(time.Second) // apply edited profile (by content only), unchanged profile and new profile // for tm1 mTm1c := applyAndExpect([]*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N1", "l1b"), // content updated windowsConfigProfileForTest(t, "N2", "l2"), // unchanged windowsConfigProfileForTest(t, "N3", "l3"), // new }, ptr.Uint(1), []*fleet.MDMWindowsConfigProfile{ withTeamID(windowsConfigProfileForTest(t, "N1", "l1b"), 1), withUploadedAt(withTeamID(windowsConfigProfileForTest(t, "N2", "l2"), 1), profTm1N2.UploadedAt), withTeamID(windowsConfigProfileForTest(t, "N3", "l3"), 1), }, true) // uuid for N1-I1 is unchanged require.Equal(t, mTm1b["I1"], mTm1c["I1"]) // uuid for N2-I2 is unchanged require.Equal(t, mTm1b["I2"], mTm1c["I2"]) profTm1N1c := getProfileByTeamAndName(ptr.Uint(1), "N1") // uploaded-at was modified because the content changed require.False(t, profTm1N1.UploadedAt.Equal(profTm1N1c.UploadedAt)) // apply only new profiles to no-team applyAndExpect([]*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N4", "l4"), windowsConfigProfileForTest(t, "N5", "l5"), }, nil, []*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N4", "l4"), windowsConfigProfileForTest(t, "N5", "l5"), }, true) // apply the same thing again -- nothing updated applyAndExpect([]*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N4", "l4"), windowsConfigProfileForTest(t, "N5", "l5"), }, nil, []*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N4", "l4"), windowsConfigProfileForTest(t, "N5", "l5"), }, false) // Change the content of one profile -- update expected applyAndExpect([]*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N4", "l4b"), windowsConfigProfileForTest(t, "N5", "l5"), }, nil, []*fleet.MDMWindowsConfigProfile{ windowsConfigProfileForTest(t, "N4", "l4b"), windowsConfigProfileForTest(t, "N5", "l5"), }, true) // clear profiles for tm1 applyAndExpect(nil, ptr.Uint(1), nil, true) } // if the label name starts with "exclude-", the label is considered an "exclude-any", otherwise // it is an "include-all". func windowsConfigProfileForTest(t *testing.T, name, locURI string, labels ...*fleet.Label) *fleet.MDMWindowsConfigProfile { prof := &fleet.MDMWindowsConfigProfile{ Name: name, SyncML: []byte(fmt.Sprintf(` %s `, locURI)), } for _, lbl := range labels { switch { case strings.HasPrefix(lbl.Name, "exclude-"): prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) case strings.HasPrefix(lbl.Name, "include-any-"): prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) default: prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID}) } } return prof } func testSaveResponse(t *testing.T, ds *Datastore) { // Set up: 3 devices, 1 command, 1 response for 1 device enrolledDevice1 := createEnrolledDevice(t, ds) enrolledDevice2 := createEnrolledDevice(t, ds) enrolledDevice3 := createEnrolledDevice(t, ds) atomicCommandUUID := uuid.NewString() replaceCommandUUID := uuid.NewString() cmd := &fleet.MDMWindowsCommand{ CommandUUID: atomicCommandUUID, RawCommand: []byte(fmt.Sprintf(` %s %s ./Device/Vendor/MSFT/Policy/Config/System/DisableOneDriveFileSync int 1 `, atomicCommandUUID, replaceCommandUUID)), TargetLocURI: "", } err := ds.mdmWindowsInsertCommandForHostsDB(context.Background(), ds.primary, []string{enrolledDevice1.MDMDeviceID, enrolledDevice2.MDMDeviceID, enrolledDevice3.MDMDeviceID}, cmd) require.NoError(t, err) // We only found a batch update method, so we are using a single statement here to insert host profile, for simplicity. ExecAdhocSQL(t, ds, func(t sqlx.ExtContext) error { _, err := t.ExecContext(context.Background(), ` INSERT INTO host_mdm_windows_profiles (host_uuid, status, operation_type, command_uuid, profile_name, profile_uuid) VALUES (?, 'pending', 'install', ?, 'disable-onedrive', ?)`, enrolledDevice1.HostUUID, atomicCommandUUID, uuid.NewString()) return err }) enrichedSyncML := createResponseAsEnrichedSyncML(t, enrolledDevice1, atomicCommandUUID, replaceCommandUUID) // Do test err = ds.MDMWindowsSaveResponse(context.Background(), enrolledDevice1.MDMDeviceID, enrichedSyncML, []string{}) require.NoError(t, err) // Verify results results, err := ds.GetMDMWindowsCommandResults(context.Background(), cmd.CommandUUID, "") require.NoError(t, err) require.Len(t, results, 1) assert.Equal(t, enrolledDevice1.HostUUID, results[0].HostUUID) assert.Equal(t, cmd.CommandUUID, results[0].CommandUUID) assert.Equal(t, "200", results[0].Status) var count int ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ?", atomicCommandUUID) }) assert.Equal(t, 2, count, "Only one device has responded, so the command should still be in the queue") // Finish setting up the second device for testing ExecAdhocSQL(t, ds, func(t sqlx.ExtContext) error { _, err := t.ExecContext(context.Background(), ` INSERT INTO host_mdm_windows_profiles (host_uuid, status, operation_type, command_uuid, profile_name, profile_uuid) VALUES (?, 'pending', 'install', ?, 'disable-onedrive', ?)`, enrolledDevice2.HostUUID, atomicCommandUUID, uuid.NewString()) return err }) enrichedSyncML2 := createResponseAsEnrichedSyncML(t, enrolledDevice2, atomicCommandUUID, replaceCommandUUID) // Do test on the second device err = ds.MDMWindowsSaveResponse(context.Background(), enrolledDevice2.MDMDeviceID, enrichedSyncML2, []string{}) require.NoError(t, err) // Verify results for the second device results, err = ds.GetMDMWindowsCommandResults(context.Background(), cmd.CommandUUID, "") require.NoError(t, err) require.Len(t, results, 2) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ?", atomicCommandUUID) }) assert.Equal(t, count, 1, "Two out of three have responded, so the command should still be in the queue for the last host") // Third device, which in our test case failed and will have it's command resent ExecAdhocSQL(t, ds, func(t sqlx.ExtContext) error { _, err := t.ExecContext(context.Background(), ` INSERT INTO host_mdm_windows_profiles (host_uuid, status, operation_type, command_uuid, profile_name, profile_uuid) VALUES (?, 'pending', 'install', ?, 'disable-onedrive', ?)`, enrolledDevice3.HostUUID, atomicCommandUUID, uuid.NewString()) return err }) enrichedSyncML3 := createResponseAsEnrichedSyncML(t, enrolledDevice3, atomicCommandUUID, replaceCommandUUID) // Do test on the third device err = ds.MDMWindowsSaveResponse(context.Background(), enrolledDevice3.MDMDeviceID, enrichedSyncML3, []string{atomicCommandUUID}) require.NoError(t, err) // Verify results does not exist for the third device results, err = ds.GetMDMWindowsCommandResults(context.Background(), cmd.CommandUUID, "") require.NoError(t, err) require.Len(t, results, 2) // still two for _, res := range results { assert.NotEqual(t, enrolledDevice3.HostUUID, res.HostUUID, "Host 3 should not have a result recorded") } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ?", atomicCommandUUID) }) // We still expect one here, as the clearing of the command from the queue will happen in the resend flow. assert.Equal(t, count, 1, "All devices have responded, so the command should be completely removed from the queue") } func createResponseAsEnrichedSyncML(t *testing.T, enrolledDevice *fleet.MDMWindowsEnrolledDevice, atomicCommandUUID string, replaceCommandUUID string, ) fleet.EnrichedSyncML { rawResponse := fmt.Sprintf(` 1.2 DM/1.2 81 2 https://example.com/api/mdm/microsoft/management %s 1 1 0 SyncHdr 200 2 1 %s Atomic 200 3 1 %s Replace 200 `, enrolledDevice.MDMDeviceID, atomicCommandUUID, replaceCommandUUID) syncML := &fleet.SyncML{} err := xml.Unmarshal([]byte(rawResponse), syncML) require.NoError(t, err) syncML.Raw = []byte(rawResponse) enrichedSyncML := fleet.NewEnrichedSyncML(syncML) return enrichedSyncML } func createEnrolledDevice(t *testing.T, ds *Datastore) *fleet.MDMWindowsEnrolledDevice { enrolledDevice := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: uuid.New().String(), MDMHardwareID: uuid.New().String() + uuid.New().String(), MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled, MDMDeviceType: "CIMClient_Windows", MDMDeviceName: "DESKTOP-1C3ARC1", MDMEnrollType: "ProgrammaticEnrollment", MDMEnrollUserID: "", MDMEnrollProtoVersion: "5.0", MDMEnrollClientVersion: "10.0.19045.2965", MDMNotInOOBE: false, HostUUID: uuid.NewString(), } err := ds.MDMWindowsInsertEnrolledDevice(context.Background(), enrolledDevice) require.NoError(t, err) return enrolledDevice } func testSetMDMWindowsProfilesWithVariables(t *testing.T, ds *Datastore) { // NOTE: as of this code being written, Fleet variables are not yet supported // in Windows profiles, but the profile-variable batch-association function // is already implemented as platform-independent (as it was not // harder/longer to do this way). This just sanity-checks that the function // works as expected for Windows. ctx := context.Background() checkProfileVariables := func(profUUID string, teamID uint, wantVars []fleet.FleetVarName) { var gotVars []string ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.SelectContext(ctx, q, &gotVars, ` SELECT fv.name FROM mdm_windows_configuration_profiles mwcp INNER JOIN mdm_configuration_profile_variables mcpv ON mwcp.profile_uuid = mcpv.windows_profile_uuid INNER JOIN fleet_variables fv ON mcpv.fleet_variable_id = fv.id WHERE mwcp.name = ? AND mwcp.team_id = ?`, "name-"+profUUID, teamID) // test profiles are created with a name = "name-" + uuid }) wantVarStrings := make([]string, len(wantVars)) for i := range wantVars { wantVarStrings[i] = "FLEET_VAR_" + string(wantVars[i]) } require.ElementsMatch(t, wantVarStrings, gotVars) } globalProfiles := []string{ InsertWindowsProfileForTest(t, ds, 0), InsertWindowsProfileForTest(t, ds, 0), } // both profiles have no variable _, err := batchSetProfileVariableAssociationsDB(ctx, ds.writer(ctx), []fleet.MDMProfileUUIDFleetVariables{ {ProfileUUID: globalProfiles[0], FleetVariables: nil}, {ProfileUUID: globalProfiles[1], FleetVariables: nil}, }, "windows") require.NoError(t, err) checkProfileVariables(globalProfiles[0], 0, nil) checkProfileVariables(globalProfiles[1], 0, nil) // add some variables _, err = batchSetProfileVariableAssociationsDB(ctx, ds.writer(ctx), []fleet.MDMProfileUUIDFleetVariables{ {ProfileUUID: globalProfiles[0], FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}}, {ProfileUUID: globalProfiles[1], FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}}, }, "windows") require.NoError(t, err) checkProfileVariables(globalProfiles[0], 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarDigiCertDataPrefix}) checkProfileVariables(globalProfiles[1], 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}) } func testWindowsMDMManagedSCEPCertificates(t *testing.T, ds *Datastore) { ctx := context.Background() testCases := []struct { name string caName string caType fleet.CAConfigAssetType challengeRetrievedAt *time.Time }{ /* { name: "NDES", caName: "ndes", caType: fleet.CAConfigNDES, challengeRetrievedAt: ptr.Time(time.Now().Add(-time.Hour).UTC().Round(time.Microsecond)), }, */ { name: "Custom SCEP", caName: "test-ca", caType: fleet.CAConfigCustomSCEPProxy, challengeRetrievedAt: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { caName := tc.caName caType := tc.caType challengeRetrievedAt := tc.challengeRetrievedAt profileUUID := uuid.NewString() dummySyncML := generateDummyWindowsProfile(profileUUID) dummyCP := fleet.MDMWindowsConfigProfile{ Name: tc.caName, SyncML: dummySyncML, } initialCP, err := ds.NewMDMWindowsConfigProfile(ctx, dummyCP, nil) require.NoError(t, err) host, err := ds.NewHost(ctx, &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), PolicyUpdatedAt: time.Now(), SeenTime: time.Now(), OsqueryHostID: ptr.String("host0-osquery-id" + tc.caName), NodeKey: ptr.String("host0-node-key" + tc.caName), UUID: "host0-test-mdm-profiles" + tc.caName, Hostname: "hostname0", }) require.NoError(t, err) // Host and profile are not linked profile, err := ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) assert.Nil(t, profile) err = ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{ { ProfileUUID: initialCP.ProfileUUID, ProfileName: initialCP.Name, HostUUID: host.UUID, Status: &fleet.MDMDeliveryPending, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: "command-uuid", Checksum: []byte("checksum"), }, }, ) require.NoError(t, err) // Host and profile do not have certificate metadata profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) assert.Nil(t, profile) // Initial certificate state where a host has been requested to install but we have no metadata err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, Type: caType, CAName: caName, }, }) require.NoError(t, err) // Check that the managed certificate was inserted correctly profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, caType, profile.Type) assert.Nil(t, profile.Serial) assert.Nil(t, profile.NotValidBefore) assert.Nil(t, profile.NotValidAfter) assert.Equal(t, caName, profile.CAName) // Renew should not do anything yet err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryPending, *profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) serial := "8ABADCAFEF684D6348F5EC95AEFF468F237A9D75" t.Run("Non renewal scenario 1 - validity window > 30 days but not yet time to renew", func(t *testing.T) { // Set not_valid_before to 1 day in the past and not_valid_after to 31 days in the future so // the validity window is 32 days of which there are 31 left which should not trigger renewal notValidAfter := time.Now().Add(31 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidBefore := time.Now().Add(-1 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: caType, CAName: caName, Serial: &serial, }, }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_windows_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, caType, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, caName, profile.CAName) // Renew should not change the MDM delivery status err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) }) t.Run("Non renewal scenario 2 - validity window < 30 days but not yet time to renew", func(t *testing.T) { // Set not_valid_before to 13 days in the past and not_valid_after to 15 days in the future so // the validity window is 28 days of which there are 15 left which should not trigger renewal notValidAfter := time.Now().Add(15 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidBefore := time.Now().Add(-13 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: caType, CAName: caName, Serial: &serial, }, }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_windows_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, caType, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, caName, profile.CAName) // Renew should not change the MDM delivery status err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) }) t.Run("Renew scenario 1 - validity window > 30 days", func(t *testing.T) { // Set not_valid_before to 31 days in the past the validity window becomes 60 days, of which there are // 29 left which should trigger the first renewal scenario(window > 30 days, renew when < 30 // days left) notValidAfter := time.Now().Add(29 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidBefore := time.Now().Add(-31 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: caType, CAName: caName, Serial: &serial, }, }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_windows_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, caType, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, caName, profile.CAName) // Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.Nil(t, profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) }) t.Run("Renew scenario 2 - validity window < 30 days", func(t *testing.T) { // Set not_valid_before to 15 days in the past and not_valid_after to 14 days in the future so the // validity window becomes 29 days, of which there are 14 left which should trigger the second // renewal scenario(window < 30 days, renew when there is half that time left) notValidBefore := time.Now().Add(-15 * 24 * time.Hour).UTC().Round(time.Microsecond) notValidAfter := time.Now().Add(14 * 24 * time.Hour).UTC().Round(time.Microsecond) err = ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{ { HostUUID: host.UUID, ProfileUUID: initialCP.ProfileUUID, ChallengeRetrievedAt: challengeRetrievedAt, NotValidBefore: ¬ValidBefore, NotValidAfter: ¬ValidAfter, Type: caType, CAName: caName, Serial: &serial, }, }) require.NoError(t, err) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { _, err := q.ExecContext(ctx, ` UPDATE host_mdm_windows_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ? `, fleet.MDMDeliveryVerified, host.UUID, initialCP.ProfileUUID) if err != nil { return err } return nil }) require.NoError(t, err) // Verify the policy is not currently marked for resend and that the upsert executed correctly profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile.Status) assert.Equal(t, fleet.MDMDeliveryVerified, *profile.Status) assert.Equal(t, host.UUID, profile.HostUUID) assert.Equal(t, initialCP.ProfileUUID, profile.ProfileUUID) assert.Equal(t, challengeRetrievedAt, profile.ChallengeRetrievedAt) assert.Equal(t, ¬ValidBefore, profile.NotValidBefore) assert.Equal(t, ¬ValidAfter, profile.NotValidAfter) assert.Equal(t, caType, profile.Type) require.NotNil(t, profile.Serial) assert.Equal(t, serial, *profile.Serial) assert.Equal(t, caName, profile.CAName) // Renew should set the MDM delivery status to "null" so the profile gets resent and the certificate renewed err = ds.RenewMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.Nil(t, profile.Status) // Cleanup should do nothing err = ds.CleanUpMDMManagedCertificates(ctx) require.NoError(t, err) profile, err = ds.GetWindowsHostMDMCertificateProfile(ctx, host.UUID, initialCP.ProfileUUID, caName) require.NoError(t, err) require.NotNil(t, profile) }) }) } } func testGetWindowsMDMCommandsForResending(t *testing.T, ds *Datastore) { ctx := context.Background() topLevelCmdUUID := uuid.NewString() cmdUUID := uuid.NewString() // Create entry in mdm_windows_enrollments table for command queue dev := createMDMWindowsEnrollment(ctx, t, ds) // No commands in windows_mdm_commands so doesn't matter what we put in commands, err := ds.GetWindowsMDMCommandsForResending(ctx, []string{cmdUUID}) require.NoError(t, err) require.Empty(t, commands) // Insert a command rawCommand := []byte(cmdUUID) err = ds.mdmWindowsInsertCommandForHostsDB(ctx, ds.writer(ctx), []string{dev.HostUUID}, &fleet.MDMWindowsCommand{ CommandUUID: topLevelCmdUUID, RawCommand: rawCommand, }) require.NoError(t, err) // Fetch command for resending commands, err = ds.GetWindowsMDMCommandsForResending(ctx, []string{cmdUUID}) require.NoError(t, err) require.Len(t, commands, 1) assert.Equal(t, topLevelCmdUUID, commands[0].CommandUUID) assert.Equal(t, rawCommand, commands[0].RawCommand) // Check that we search raw body and not match on command_uuid commands, err = ds.GetWindowsMDMCommandsForResending(ctx, []string{topLevelCmdUUID}) require.NoError(t, err) require.Empty(t, commands) } func testResendWindowsMDMCommand(t *testing.T, ds *Datastore) { ctx := context.Background() dev := createMDMWindowsEnrollment(ctx, t, ds) cmdUUID := uuid.NewString() // Query enrollment id from mdm_windows_enrollments var enrollmentID int64 ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &enrollmentID, "SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?", dev.MDMDeviceID) }) require.Greater(t, enrollmentID, int64(0), "Enrollment ID should be greater than 0") // Insert host profile entry err := ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{ { HostUUID: dev.HostUUID, ProfileUUID: uuid.NewString(), ProfileName: "test-profile", Status: &fleet.MDMDeliveryFailed, OperationType: fleet.MDMOperationTypeInstall, CommandUUID: cmdUUID, Checksum: []byte("checksum"), Detail: "fake detail we expect to be cleared on resend", }, }) require.NoError(t, err) // Insert a command for the original profile cmdBody := []byte(``) cmd := &fleet.MDMWindowsCommand{ CommandUUID: cmdUUID, RawCommand: cmdBody, } err = ds.mdmWindowsInsertCommandForHostsDB(ctx, ds.writer(ctx), []string{dev.HostUUID}, cmd) require.NoError(t, err) // Verify we have a windows_mdm_command_queue entry var count int ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ? AND enrollment_id = ?", cmd.CommandUUID, enrollmentID) }) assert.Equal(t, 1, count, "Command queue entry should exist before resend") // Resend command // We manually do replacement here newCmdUUID := uuid.NewString() newBody := []byte(``) newCmd := &fleet.MDMWindowsCommand{ CommandUUID: newCmdUUID, RawCommand: newBody, } err = ds.ResendWindowsMDMCommand(ctx, dev.MDMDeviceID, newCmd, cmd) require.NoError(t, err) // Verify we have a windows_mdm_command_queue entry for the new command ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ? AND enrollment_id = ?", newCmd.CommandUUID, enrollmentID) }) assert.Equal(t, 1, count, "New command queue entry should exist after resend") // verify we don't have a windows_mdm_command_queue entry for the old command ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &count, "SELECT COUNT(*) FROM windows_mdm_command_queue WHERE command_uuid = ? AND enrollment_id = ?", cmd.CommandUUID, enrollmentID) }) assert.Equal(t, 0, count, "Old command queue entry should not exist after resend") // Verify host profile status is reset and detail cleared var status string var detail sql.NullString ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &status, "SELECT status FROM host_mdm_windows_profiles WHERE command_uuid = ? AND host_uuid = ?", newCmd.CommandUUID, dev.HostUUID) }) ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { return sqlx.GetContext(ctx, q, &detail, "SELECT detail FROM host_mdm_windows_profiles WHERE command_uuid = ? AND host_uuid = ?", newCmd.CommandUUID, dev.HostUUID) }) assert.Equal(t, string(fleet.MDMDeliveryPending), status, "Host profile status should be reset to pending on resend") require.True(t, detail.Valid, "Host profile detail should be cleared on resend") assert.Empty(t, detail.String, "Host profile detail should be cleared on resend") }