Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update API endpoints that support os_setttings filter to include Windows MDM profiles status #15188

Merged
merged 1 commit into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/14424-hosts-filter-windows-profiles-status
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Updated API endpoints that support `os_setttings` filter to include Windows profiles status.
138 changes: 111 additions & 27 deletions server/datastore/mysql/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -1180,47 +1180,133 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis
return sql, params
}

// TODO: Look into ways we can convert some of the LEFT JOINs in the main list hosts query
// to INNER JOINs if the OSSettingsFilter is set. This would allow us to use indices
// from the `host_mdm` table, for example, to cut down on the number of rows that need
// to be scanned. For now, this method assumes that LEFT JOINs are used in the main query
// and adds extra where clauses to filter out Windows hosts that are not enrolled to Fleet MDM
// or are servers. Similar logic could be applied to macOS hosts but is not included in this
// current implementation.

sqlFmt := ` AND h.platform IN('windows', 'darwin')`
if opt.TeamFilter == nil {
// macOS settings filter is not compatible with the "all teams" option so append the "no
// team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
// OS settings filter is not compatible with the "all teams" option so append the "no team"
// filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
sqlFmt += ` AND h.team_id IS NULL`
}
var whereMacOS, whereWindows string
sqlFmt += ` AND ((h.platform = 'windows' AND (%s)) OR (h.platform = 'darwin' AND (%s)))`

// construct the WHERE for macOS
var subqueryMacOS string
var subqueryParams []interface{}
whereWindows := "FALSE"
whereMacOS := "FALSE"

var paramsMacOS []interface{}
switch opt.OSSettingsFilter {
case fleet.OSSettingsFailed:
subqueryMacOS, subqueryParams = subqueryHostsMacOSSettingsStatusFailed()
if isDiskEncryptionEnabled {
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionFailed)
}
subqueryMacOS, paramsMacOS = subqueryHostsMacOSSettingsStatusFailed()
case fleet.OSSettingsPending:
subqueryMacOS, subqueryParams = subqueryHostsMacOSSettingsStatusPending()
if isDiskEncryptionEnabled {
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing)
}
subqueryMacOS, paramsMacOS = subqueryHostsMacOSSettingsStatusPending()
case fleet.OSSettingsVerifying:
subqueryMacOS, subqueryParams = subqueryHostsMacOSSetttingsStatusVerifying()
if isDiskEncryptionEnabled {
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying)
}
subqueryMacOS, paramsMacOS = subqueryHostsMacOSSetttingsStatusVerifying()
case fleet.OSSettingsVerified:
subqueryMacOS, subqueryParams = subqueryHostsMacOSSetttingsStatusVerified()
if isDiskEncryptionEnabled {
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerified)
}
subqueryMacOS, paramsMacOS = subqueryHostsMacOSSetttingsStatusVerified()
}

if subqueryMacOS != "" {
whereMacOS = "EXISTS (" + subqueryMacOS + ")"
} else {
whereMacOS = "FALSE"
}

// construct the WHERE for windows
whereWindows = `hmdm.name = ? AND hmdm.enrolled = 1 AND hmdm.is_server = 0`
paramsWindows := []interface{}{fleet.WellKnownMDMFleet}
subqueryFailed, paramsFailed := subqueryHostsMDMWindowsOSSettingsStatusFailed()
paramsWindows = append(paramsWindows, paramsFailed...)
subqueryPending, paramsPending := subqueryHostsMDMWindowsOSSettingsStatusPending()
paramsWindows = append(paramsWindows, paramsPending...)
subqueryVerifying, paramsVerifying := subqueryHostsMDMWindowsOSSettingsStatusVerifying()
paramsWindows = append(paramsWindows, paramsVerifying...)
subqueryVerified, paramsVerified := subqueryHostsMDMWindowsOSSettingsStatusVerified()
paramsWindows = append(paramsWindows, paramsVerified...)

profilesStatus := fmt.Sprintf(`
CASE WHEN EXISTS (%s) THEN
'profiles_failed'
WHEN EXISTS (%s) THEN
'profiles_pending'
WHEN EXISTS (%s) THEN
'profiles_verifying'
WHEN EXISTS (%s) THEN
'profiles_verified'
ELSE
''
END`,
subqueryFailed,
subqueryPending,
subqueryVerifying,
subqueryVerified,
)

bitlockerStatus := `''`
if isDiskEncryptionEnabled {
bitlockerStatus = fmt.Sprintf(`
CASE WHEN (%s) THEN
'bitlocker_verified'
WHEN (%s) THEN
'bitlocker_verifying'
WHEN (%s) THEN
'bitlocker_pending'
WHEN (%s) THEN
'bitlocker_failed'
ELSE
''
END`,
ds.whereBitLockerStatus(fleet.DiskEncryptionVerified),
ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying),
ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing),
ds.whereBitLockerStatus(fleet.DiskEncryptionFailed),
)
}

return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), append(params, subqueryParams...)
whereWindows += fmt.Sprintf(` AND (
CASE (%s)
WHEN 'profiles_failed' THEN
'failed'
WHEN 'profiles_pending' THEN (
CASE (%s)
WHEN 'bitlocker_failed' THEN
'failed'
ELSE
'pending'
END)
WHEN 'profiles_verifying' THEN (
CASE (%s)
WHEN 'bitlocker_failed' THEN
'failed'
WHEN 'bitlocker_pending' THEN
'pending'
ELSE
'verifying'
END)
WHEN 'profiles_verified' THEN (
CASE (%s)
WHEN 'bitlocker_failed' THEN
'failed'
WHEN 'bitlocker_pending' THEN
'pending'
WHEN 'bitlocker_verifying' THEN
'verifying'
ELSE
'verified'
END)
ELSE
REPLACE((%s), 'bitlocker_', '')
END) = ?`, profilesStatus, bitlockerStatus, bitlockerStatus, bitlockerStatus, bitlockerStatus)

paramsWindows = append(paramsWindows, opt.OSSettingsFilter)
params = append(params, paramsWindows...)
params = append(params, paramsMacOS...)

return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), params
}

func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}, enableDiskEncryption bool) (string, []interface{}) {
Expand All @@ -1229,10 +1315,8 @@ func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt
}

sqlFmt := " AND h.platform IN('windows', 'darwin')"
// TODO: Should we add no team filter here? It isn't included for the FileVault filter but is
// for the general macOS settings filter.
if opt.TeamFilter == nil {
// macOS settings filter is not compatible with the "all teams" option so append the "no
// OS settings filter is not compatible with the "all teams" option so append the "no
// team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
sqlFmt += ` AND h.team_id IS NULL`
}
Expand Down
12 changes: 12 additions & 0 deletions server/datastore/mysql/microsoft_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,17 @@ SELECT
ELSE
'verifying'
END)
WHEN 'profiles_verified' THEN (
CASE (%s)
WHEN 'bitlocker_failed' THEN
'failed'
WHEN 'bitlocker_pending' THEN
'pending'
WHEN 'bitlocker_verifying' THEN
'verifying'
ELSE
'verified'
END)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to mirror the hosts filter. This additional case isn't strictly needed here because unlike the host filter this particular query is only used for the summary when we expect to have a bitlocker status (so we can go straight to the final else in this particular case), but I went ahead and included it here for consistency and future clarity.

ELSE
REPLACE((%s), 'bitlocker_', '')
END as status,
Expand All @@ -900,6 +911,7 @@ GROUP BY
bitlockerStatus,
bitlockerStatus,
bitlockerStatus,
bitlockerStatus,
bitlockerJoin,
fleet.WellKnownMDMFleet,
teamFilter,
Expand Down
47 changes: 39 additions & 8 deletions server/datastore/mysql/microsoft_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) {
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) {
Expand Down Expand Up @@ -223,14 +227,10 @@ func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) {
Verified: uint(len(ep[fleet.MDMDeliveryVerified])),
})

checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, expectedDE[fleet.DiskEncryptionVerified])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, expectedDE[fleet.DiskEncryptionVerifying])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, expectedDE[fleet.DiskEncryptionFailed])
var expectedPending []uint
expectedPending = append(expectedPending, expectedDE[fleet.DiskEncryptionEnforcing]...)
expectedPending = append(expectedPending, expectedDE[fleet.DiskEncryptionRemovingEnforcement]...)
expectedPending = append(expectedPending, expectedDE[fleet.DiskEncryptionActionRequired]...)
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, expectedPending)
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) {
Expand Down Expand Up @@ -292,8 +292,14 @@ func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) {
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
})

Expand Down Expand Up @@ -554,6 +560,26 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
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 _, h := range gotHosts {
gotIDs = append(gotIDs, 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) {
Expand All @@ -563,6 +589,11 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
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) {
Expand Down