From fa14eaf63a15f91143965eb676e10ee197f5b882 Mon Sep 17 00:00:00 2001 From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Date: Fri, 5 Jan 2024 09:28:54 -0600 Subject: [PATCH] Update categorization of Windows OS updates to exclude from user-defined Windows MDM profiles in API responses (#15924) --- .../15714-windows-os-updates-profile-summary | 3 + server/datastore/mysql/hosts.go | 31 +++- server/datastore/mysql/labels.go | 5 +- server/datastore/mysql/microsoft_mdm.go | 92 +++++++--- server/mdm/mdm.go | 6 + server/service/integration_mdm_test.go | 161 ++++++++++++++++++ 6 files changed, 264 insertions(+), 34 deletions(-) create mode 100644 changes/15714-windows-os-updates-profile-summary diff --git a/changes/15714-windows-os-updates-profile-summary b/changes/15714-windows-os-updates-profile-summary new file mode 100644 index 0000000000..836bab37ad --- /dev/null +++ b/changes/15714-windows-os-updates-profile-summary @@ -0,0 +1,3 @@ +- Modified hosts and labels endpoints so that only user-defined Windows MDM profiles are included in + filtered results, host details, and profiles summaries API responses (more specifically, + "Windows OS updates" is excluded from these responses). diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 42ddc84660..e01ae624c1 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1077,7 +1077,10 @@ func (ds *Datastore) applyHostFilters( } return "", nil, err } else if opt.OSSettingsFilter.IsValid() { - sqlStmt, params = ds.filterHostsByOSSettingsStatus(sqlStmt, opt, params, enableDiskEncryption) + sqlStmt, params, err = ds.filterHostsByOSSettingsStatus(sqlStmt, opt, params, enableDiskEncryption) + if err != nil { + return "", nil, err + } } else if opt.OSSettingsDiskEncryptionFilter.IsValid() { sqlStmt, params = ds.filterHostsByOSSettingsDiskEncryptionStatus(sqlStmt, opt, params, enableDiskEncryption) } @@ -1234,9 +1237,9 @@ func filterHostsByMacOSDiskEncryptionStatus(sql string, opt fleet.HostListOption return sql + fmt.Sprintf(` AND EXISTS (%s)`, subquery), append(params, subqueryParams...) } -func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostListOptions, params []interface{}, isDiskEncryptionEnabled bool) (string, []interface{}) { +func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostListOptions, params []interface{}, isDiskEncryptionEnabled bool) (string, []interface{}, error) { if !opt.OSSettingsFilter.IsValid() { - return sql, params + return sql, params, nil } // TODO: Look into ways we can convert some of the LEFT JOINs in the main list hosts query @@ -1278,13 +1281,25 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis // construct the WHERE for windows whereWindows = `hmdm.name = ? AND hmdm.enrolled = 1 AND hmdm.is_server = 0` paramsWindows := []interface{}{fleet.WellKnownMDMFleet} - subqueryFailed, paramsFailed := subqueryHostsMDMWindowsOSSettingsStatusFailed() + subqueryFailed, paramsFailed, err := subqueryHostsMDMWindowsOSSettingsStatusFailed() + if err != nil { + return "", nil, err + } paramsWindows = append(paramsWindows, paramsFailed...) - subqueryPending, paramsPending := subqueryHostsMDMWindowsOSSettingsStatusPending() + subqueryPending, paramsPending, err := subqueryHostsMDMWindowsOSSettingsStatusPending() + if err != nil { + return "", nil, err + } paramsWindows = append(paramsWindows, paramsPending...) - subqueryVerifying, paramsVerifying := subqueryHostsMDMWindowsOSSettingsStatusVerifying() + subqueryVerifying, paramsVerifying, err := subqueryHostsMDMWindowsOSSettingsStatusVerifying() + if err != nil { + return "", nil, err + } paramsWindows = append(paramsWindows, paramsVerifying...) - subqueryVerified, paramsVerified := subqueryHostsMDMWindowsOSSettingsStatusVerified() + subqueryVerified, paramsVerified, err := subqueryHostsMDMWindowsOSSettingsStatusVerified() + if err != nil { + return "", nil, err + } paramsWindows = append(paramsWindows, paramsVerified...) profilesStatus := fmt.Sprintf(` @@ -1365,7 +1380,7 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis params = append(params, paramsWindows...) params = append(params, paramsMacOS...) - return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), params + return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), params, nil } func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}, enableDiskEncryption bool) (string, []interface{}) { diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go index 5ddb5343bd..480b42b0e9 100644 --- a/server/datastore/mysql/labels.go +++ b/server/datastore/mysql/labels.go @@ -593,7 +593,10 @@ func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.Tea if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { return "", nil, err } else if opt.OSSettingsFilter.IsValid() { - query, params = ds.filterHostsByOSSettingsStatus(query, opt, params, enableDiskEncryption) + query, params, err = ds.filterHostsByOSSettingsStatus(query, opt, params, enableDiskEncryption) + if err != nil { + return "", nil, err + } } else if opt.OSSettingsDiskEncryptionFilter.IsValid() { query, params = ds.filterHostsByOSSettingsDiskEncryptionStatus(query, opt, params, enableDiskEncryption) } diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index ee51a5d13a..28f5f13ce5 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -729,40 +729,46 @@ func (ds *Datastore) DeleteMDMWindowsConfigProfileByTeamAndName(ctx context.Cont return nil } -func subqueryHostsMDMWindowsOSSettingsStatusFailed() (string, []interface{}) { +func subqueryHostsMDMWindowsOSSettingsStatusFailed() (string, []interface{}, error) { sql := ` SELECT 1 FROM host_mdm_windows_profiles hmwp WHERE h.uuid = hmwp.host_uuid - AND hmwp.status = ?` + AND hmwp.status = ? + AND hmwp.profile_name NOT IN(?)` args := []interface{}{ fleet.MDMDeliveryFailed, + mdm.ListFleetReservedWindowsProfileNames(), } - return sql, args + return sqlx.In(sql, args...) } -func subqueryHostsMDMWindowsOSSettingsStatusPending() (string, []interface{}) { +func subqueryHostsMDMWindowsOSSettingsStatusPending() (string, []interface{}, error) { sql := ` SELECT 1 FROM host_mdm_windows_profiles hmwp WHERE h.uuid = hmwp.host_uuid AND (hmwp.status IS NULL OR hmwp.status = ?) + AND hmwp.profile_name NOT IN(?) AND NOT EXISTS ( SELECT 1 FROM host_mdm_windows_profiles hmwp2 WHERE (h.uuid = hmwp2.host_uuid - AND hmwp2.status = ?))` + AND hmwp2.status = ? + AND hmwp2.profile_name NOT IN(?)))` args := []interface{}{ fleet.MDMDeliveryPending, + mdm.ListFleetReservedWindowsProfileNames(), fleet.MDMDeliveryFailed, + mdm.ListFleetReservedWindowsProfileNames(), } - return sql, args + return sqlx.In(sql, args...) } -func subqueryHostsMDMWindowsOSSettingsStatusVerifying() (string, []interface{}) { +func subqueryHostsMDMWindowsOSSettingsStatusVerifying() (string, []interface{}, error) { sql := ` SELECT 1 FROM host_mdm_windows_profiles hmwp @@ -770,25 +776,28 @@ func subqueryHostsMDMWindowsOSSettingsStatusVerifying() (string, []interface{}) h.uuid = hmwp.host_uuid AND hmwp.operation_type = ? AND hmwp.status = ? + AND hmwp.profile_name NOT IN(?) AND NOT EXISTS ( SELECT 1 FROM host_mdm_windows_profiles hmwp2 WHERE (h.uuid = hmwp2.host_uuid AND hmwp2.operation_type = ? + AND hmwp2.profile_name NOT IN(?) AND(hmwp2.status IS NULL - OR hmwp2.status NOT IN(?, ?))))` + OR hmwp2.status NOT IN(?))))` args := []interface{}{ fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying, + mdm.ListFleetReservedWindowsProfileNames(), fleet.MDMOperationTypeInstall, - fleet.MDMDeliveryVerifying, - fleet.MDMDeliveryVerified, + mdm.ListFleetReservedWindowsProfileNames(), + []interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified}, } - return sql, args + return sqlx.In(sql, args...) } -func subqueryHostsMDMWindowsOSSettingsStatusVerified() (string, []interface{}) { +func subqueryHostsMDMWindowsOSSettingsStatusVerified() (string, []interface{}, error) { sql := ` SELECT 1 FROM host_mdm_windows_profiles hmwp @@ -796,20 +805,24 @@ func subqueryHostsMDMWindowsOSSettingsStatusVerified() (string, []interface{}) { h.uuid = hmwp.host_uuid AND hmwp.operation_type = ? AND hmwp.status = ? + AND hmwp.profile_name NOT IN(?) AND NOT EXISTS ( SELECT 1 FROM host_mdm_windows_profiles hmwp2 WHERE (h.uuid = hmwp2.host_uuid AND hmwp2.operation_type = ? + AND hmwp2.profile_name NOT IN(?) AND(hmwp2.status IS NULL OR hmwp2.status != ?)))` args := []interface{}{ fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified, + mdm.ListFleetReservedWindowsProfileNames(), fleet.MDMOperationTypeInstall, + mdm.ListFleetReservedWindowsProfileNames(), fleet.MDMDeliveryVerified, } - return sql, args + return sqlx.In(sql, args...) } func (ds *Datastore) GetMDMWindowsProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) { @@ -856,13 +869,25 @@ type statusCounts struct { func getMDMWindowsStatusCountsProfilesOnlyDB(ctx context.Context, ds *Datastore, teamID *uint) ([]statusCounts, error) { var args []interface{} - subqueryFailed, subqueryFailedArgs := subqueryHostsMDMWindowsOSSettingsStatusFailed() + subqueryFailed, subqueryFailedArgs, err := subqueryHostsMDMWindowsOSSettingsStatusFailed() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusFailed") + } args = append(args, subqueryFailedArgs...) - subqueryPending, subqueryPendingArgs := subqueryHostsMDMWindowsOSSettingsStatusPending() + subqueryPending, subqueryPendingArgs, err := subqueryHostsMDMWindowsOSSettingsStatusPending() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusPending") + } args = append(args, subqueryPendingArgs...) - subqueryVerifying, subqueryVeryingingArgs := subqueryHostsMDMWindowsOSSettingsStatusVerifying() + subqueryVerifying, subqueryVeryingingArgs, err := subqueryHostsMDMWindowsOSSettingsStatusVerifying() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusVerifying") + } args = append(args, subqueryVeryingingArgs...) - subqueryVerified, subqueryVerifiedArgs := subqueryHostsMDMWindowsOSSettingsStatusVerified() + subqueryVerified, subqueryVerifiedArgs, err := subqueryHostsMDMWindowsOSSettingsStatusVerified() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusVerified") + } args = append(args, subqueryVerifiedArgs...) teamFilter := "h.team_id IS NULL" @@ -907,7 +932,7 @@ GROUP BY ) var counts []statusCounts - err := sqlx.SelectContext(ctx, ds.reader(ctx), &counts, stmt, args...) + err = sqlx.SelectContext(ctx, ds.reader(ctx), &counts, stmt, args...) if err != nil { return nil, err } @@ -916,13 +941,25 @@ GROUP BY func getMDMWindowsStatusCountsProfilesAndBitLockerDB(ctx context.Context, ds *Datastore, teamID *uint) ([]statusCounts, error) { var args []interface{} - subqueryFailed, subqueryFailedArgs := subqueryHostsMDMWindowsOSSettingsStatusFailed() + subqueryFailed, subqueryFailedArgs, err := subqueryHostsMDMWindowsOSSettingsStatusFailed() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusFailed") + } args = append(args, subqueryFailedArgs...) - subqueryPending, subqueryPendingArgs := subqueryHostsMDMWindowsOSSettingsStatusPending() + subqueryPending, subqueryPendingArgs, err := subqueryHostsMDMWindowsOSSettingsStatusPending() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusPending") + } args = append(args, subqueryPendingArgs...) - subqueryVerifying, subqueryVeryingingArgs := subqueryHostsMDMWindowsOSSettingsStatusVerifying() + subqueryVerifying, subqueryVeryingingArgs, err := subqueryHostsMDMWindowsOSSettingsStatusVerifying() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusVerifying") + } args = append(args, subqueryVeryingingArgs...) - subqueryVerified, subqueryVerifiedArgs := subqueryHostsMDMWindowsOSSettingsStatusVerified() + subqueryVerified, subqueryVerifiedArgs, err := subqueryHostsMDMWindowsOSSettingsStatusVerified() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "subqueryHostsMDMWindowsOSSettingsStatusVerified") + } args = append(args, subqueryVerifiedArgs...) profilesStatus := fmt.Sprintf(` @@ -1030,7 +1067,7 @@ GROUP BY ) var counts []statusCounts - err := sqlx.SelectContext(ctx, ds.reader(ctx), &counts, stmt, args...) + err = sqlx.SelectContext(ctx, ds.reader(ctx), &counts, stmt, args...) if err != nil { return nil, err } @@ -1651,7 +1688,7 @@ SELECT FROM host_mdm_windows_profiles WHERE -host_uuid = ? AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', '%s'))`, +host_uuid = ? AND profile_name NOT IN(?) AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', '%s'))`, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove, fleet.MDMDeliveryPending, @@ -1659,8 +1696,13 @@ host_uuid = ? AND NOT (operation_type = '%s' AND COALESCE(status, '%s') IN('%s', fleet.MDMDeliveryVerified, ) + stmt, args, err := sqlx.In(stmt, hostUUID, mdm.ListFleetReservedWindowsProfileNames()) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "building in statement") + } + var profiles []fleet.HostMDMWindowsProfile - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, hostUUID); err != nil { + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, args...); err != nil { return nil, err } return profiles, nil diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index 2048238ea7..4e2b1c9485 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -80,3 +80,9 @@ func FleetReservedProfileNames() map[string]struct{} { FleetWindowsOSUpdatesProfileName: {}, } } + +// ListFleetReservedWindowsProfileNames returns a list of PayloadDisplayName strings +// that are reserved by Fleet for Windows. +func ListFleetReservedWindowsProfileNames() []string { + return []string{FleetWindowsOSUpdatesProfileName} +} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index b462a175d5..81f077007d 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -259,6 +259,8 @@ func (s *integrationMDMTestSuite) TearDownTest() { appCfg.MDM.WindowsEnabledAndConfigured = true // ensure global disk encryption is disabled on exit appCfg.MDM.EnableDiskEncryption = optjson.SetBool(false) + // ensure global Windows OS updates are always disabled for the next test + appCfg.MDM.WindowsUpdates = mdm_types.WindowsUpdates{} err := s.ds.SaveAppConfig(ctx, &appCfg.AppConfig) require.NoError(t, err) @@ -10049,10 +10051,68 @@ func (s *integrationMDMTestSuite) TestWindowsProfileManagement() { require.NotNil(t, p.Status) require.Equal(t, wantStatus, *p.Status, "profile", p.Name) require.Equal(t, "windows", p.Platform) + // Fleet reserved profiles (e.g., OS updates) should be screened from the host details response + require.NotContains(t, servermdm.ListFleetReservedWindowsProfileNames(), p.Name) } require.ElementsMatch(t, wantProfs, gotProfs) } + checkHostsFilteredByOSSettingsStatus := func(t *testing.T, wantHosts []string, wantStatus fleet.MDMDeliveryStatus, teamID *uint, labels ...*fleet.Label) { + var teamFilter string + if teamID != nil { + teamFilter = fmt.Sprintf("&team_id=%d", *teamID) + } + var gotHostsResp listHostsResponse + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts?os_settings=%s%s", wantStatus, teamFilter), nil, http.StatusOK, &gotHostsResp) + require.NotNil(t, gotHostsResp.Hosts) + var gotHosts []string + for _, h := range gotHostsResp.Hosts { + gotHosts = append(gotHosts, h.Hostname) + } + require.ElementsMatch(t, wantHosts, gotHosts) + + var countHostsResp countHostsResponse + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/count?os_settings=%s%s", wantStatus, teamFilter), nil, http.StatusOK, &countHostsResp) + require.Equal(t, len(wantHosts), countHostsResp.Count) + + for _, l := range labels { + gotHostsResp = listHostsResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/labels/%d/hosts?os_settings=%s%s", l.ID, wantStatus, teamFilter), nil, http.StatusOK, &gotHostsResp) + require.NotNil(t, gotHostsResp.Hosts) + gotHosts = []string{} + for _, h := range gotHostsResp.Hosts { + gotHosts = append(gotHosts, h.Hostname) + } + require.ElementsMatch(t, wantHosts, gotHosts, "label", l.Name) + + countHostsResp = countHostsResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/count?label_id=%d&os_settings=%s%s", l.ID, wantStatus, teamFilter), nil, http.StatusOK, &countHostsResp) + } + } + + getProfileUUID := func(t *testing.T, profName string, teamID *uint) string { + var profUUID string + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + var globalOrTeamID uint + if teamID != nil { + globalOrTeamID = *teamID + } + return sqlx.GetContext(ctx, tx, &profUUID, `SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`, globalOrTeamID, profName) + }) + require.NotNil(t, profUUID) + return profUUID + } + + checkHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, wantStatus fleet.MDMDeliveryStatus) { + var gotStatus fleet.MDMDeliveryStatus + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + stmt := `SELECT status FROM host_mdm_windows_profiles WHERE host_uuid = ? AND profile_uuid = ?` + err := sqlx.GetContext(context.Background(), q, &gotStatus, stmt, hostUUID, profUUID) + return err + }) + require.Equal(t, wantStatus, gotStatus) + } + // Create a host and then enroll to MDM. host, mdmDevice := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t) // trigger a profile sync @@ -10060,9 +10120,68 @@ func (s *integrationMDMTestSuite) TestWindowsProfileManagement() { checkHostsProfilesMatch(host, globalProfiles) checkHostDetails(t, host, globalProfiles, fleet.MDMDeliveryVerifying) + // create new label that includes host + label := &fleet.Label{ + Name: t.Name() + "foo", + Query: "select * from foo;", + } + label, err = s.ds.NewLabel(context.Background(), label) + require.NoError(t, err) + require.NoError(t, s.ds.RecordLabelQueryExecutions(ctx, host, map[uint]*bool{label.ID: ptr.Bool(true)}, time.Now(), false)) + + // simulate osquery reporting host mdm details (host_mdm.enrolled = 1 is condition for + // hosts filtering by os settings status and generating mdm profiles summaries) + require.NoError(t, s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "")) + checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, nil, label) + s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{ + Verifying: 1, + }, nil) + // another sync shouldn't return profiles verifyProfiles(mdmDevice, 0, false) + // make fleet add a Windows OS Updates profile + acResp := appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{"mdm": { "windows_updates": {"deadline_days": 1, "grace_period_days": 1} }}`), http.StatusOK, &acResp) + osUpdatesProf := getProfileUUID(t, servermdm.FleetWindowsOSUpdatesProfileName, nil) + + // os updates is sent via a profiles commands + verifyProfiles(mdmDevice, 1, false) + checkHostsProfilesMatch(host, append(globalProfiles, osUpdatesProf)) + // but is hidden from host details response + checkHostDetails(t, host, globalProfiles, fleet.MDMDeliveryVerifying) + + // os updates profile status doesn't matter for filtered hosts results or summaries + checkHostProfileStatus(t, host.UUID, osUpdatesProf, fleet.MDMDeliveryVerifying) + checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, nil, label) + s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{ + Verifying: 1, + }, nil) + // force os updates profile to failed, doesn't impact filtered hosts results or summaries + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + stmt := `UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ?` + _, err := q.ExecContext(context.Background(), stmt, osUpdatesProf) + return err + }) + checkHostProfileStatus(t, host.UUID, osUpdatesProf, fleet.MDMDeliveryFailed) + checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, nil, label) + s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{ + Verifying: 1, + }, nil) + // force another profile to failed, does impact filtered hosts results and summaries + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + stmt := `UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ?` + _, err := q.ExecContext(context.Background(), stmt, globalProfiles[0]) + return err + }) + checkHostProfileStatus(t, host.UUID, globalProfiles[0], fleet.MDMDeliveryFailed) + checkHostsFilteredByOSSettingsStatus(t, []string{}, fleet.MDMDeliveryVerifying, nil, label) // expect no hosts + checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryFailed, nil, label) // expect host + s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{ + Failed: 1, + Verifying: 0, + }, nil) + // add the host to a team err = s.ds.AddHostsToTeam(ctx, &tm.ID, []uint{host.ID}) require.NoError(t, err) @@ -10115,6 +10234,48 @@ func (s *integrationMDMTestSuite) TestWindowsProfileManagement() { // another sync shouldn't return profiles verifyProfiles(mdmDevice, 0, false) + + // make fleet add a Windows OS Updates profile + tmResp := teamResponse{} + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID), json.RawMessage(`{"mdm": { "windows_updates": {"deadline_days": 1, "grace_period_days": 1} }}`), http.StatusOK, &tmResp) + osUpdatesProf = getProfileUUID(t, servermdm.FleetWindowsOSUpdatesProfileName, &tm.ID) + + // os updates is sent via a profiles commands + verifyProfiles(mdmDevice, 1, false) + checkHostsProfilesMatch(host, append(teamProfiles, osUpdatesProf)) + // but is hidden from host details response + checkHostDetails(t, host, teamProfiles, fleet.MDMDeliveryVerifying) + + // os updates profile status doesn't matter for filtered hosts results or summaries + checkHostProfileStatus(t, host.UUID, osUpdatesProf, fleet.MDMDeliveryVerifying) + checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, &tm.ID, label) + s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{ + Verifying: 1, + }, nil) + // force os updates profile to failed, doesn't impact filtered hosts results or summaries + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + stmt := `UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ?` + _, err := q.ExecContext(context.Background(), stmt, osUpdatesProf) + return err + }) + checkHostProfileStatus(t, host.UUID, osUpdatesProf, fleet.MDMDeliveryFailed) + checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryVerifying, &tm.ID, label) + s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{ + Verifying: 1, + }, nil) + // force another profile to failed, does impact filtered hosts results and summaries + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + stmt := `UPDATE host_mdm_windows_profiles SET status = 'failed' WHERE profile_uuid = ?` + _, err := q.ExecContext(context.Background(), stmt, teamProfiles[0]) + return err + }) + checkHostProfileStatus(t, host.UUID, teamProfiles[0], fleet.MDMDeliveryFailed) + checkHostsFilteredByOSSettingsStatus(t, []string{}, fleet.MDMDeliveryVerifying, &tm.ID, label) // expect no hosts + checkHostsFilteredByOSSettingsStatus(t, []string{host.Hostname}, fleet.MDMDeliveryFailed, &tm.ID, label) // expect host + s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{ + Failed: 1, + Verifying: 0, + }, nil) } func (s *integrationMDMTestSuite) TestAppConfigMDMWindowsProfiles() {