Mark "verifying" or "verified" MDM profiles as "failed" if osquery cannot confirm they are installed (#12414)

This commit is contained in:
gillespi314 2023-06-21 13:00:49 -05:00 committed by GitHub
parent b754cb096c
commit 8cc7d38300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 653 additions and 39 deletions

View File

@ -0,0 +1,2 @@
- Updated MDM detail query ingestion to switch MDM profiles from "verifying" or "verified"
status to "failed" status when osquery reports that this profile is not installed on the host.

View File

@ -1532,8 +1532,8 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
var sb strings.Builder
for _, p := range payload {
args = append(args, p.ProfileID, p.ProfileIdentifier, p.ProfileName, p.HostUUID, p.Status, p.OperationType, p.CommandUUID, p.Checksum)
sb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?),")
args = append(args, p.ProfileID, p.ProfileIdentifier, p.ProfileName, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID, p.Checksum)
sb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),")
}
stmt := fmt.Sprintf(`
@ -1544,6 +1544,7 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
host_uuid,
status,
operation_type,
detail,
command_uuid,
checksum
)
@ -1551,6 +1552,7 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
ON DUPLICATE KEY UPDATE
status = VALUES(status),
operation_type = VALUES(operation_type),
detail = VALUES(detail),
command_uuid = VALUES(command_uuid)`,
strings.TrimSuffix(sb.String(), ","),
)
@ -1578,7 +1580,7 @@ func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, prof
return err
}
func (ds *Datastore) SetVerifiedHostMacOSProfiles(ctx context.Context, host *fleet.Host, installedProfiles []*fleet.HostMacOSProfile) error {
func (ds *Datastore) UpdateVerificationHostMacOSProfiles(ctx context.Context, host *fleet.Host, installedProfiles []*fleet.HostMacOSProfile) error {
installedProfsByIdentifier := make(map[string]*fleet.HostMacOSProfile, len(installedProfiles))
for _, p := range installedProfiles {
installedProfsByIdentifier[p.Identifier] = p
@ -1601,50 +1603,130 @@ WHERE
return ctxerr.Wrap(ctx, err, "listing expected profiles to set verified host macOS profiles")
}
verifiedIdentifiers := make([]string, 0, len(expectedProfs))
foundIdentifiers := make([]string, 0, len(expectedProfs))
missingIdentifiers := make([]string, 0, len(expectedProfs))
for _, ep := range expectedProfs {
withinGracePeriod := ep.IsWithinGracePeriod(host.DetailUpdatedAt) // Note: The host detail timestamp is updated after the current set is ingested, see https://github.com/fleetdm/fleet/blob/e9fd28717d474668ca626efbacdd0615d42b2e0a/server/service/osquery.go#L950
ip, ok := installedProfsByIdentifier[ep.Identifier]
if !ok {
// TODO: expected profile is not installed on host, skip it for now
// expected profile is missing from host
if !withinGracePeriod {
missingIdentifiers = append(missingIdentifiers, ep.Identifier)
}
continue
}
if ep.UpdatedAt.After(ip.InstallDate) {
// TODO: host has an older version of expected profile installed, skip it for now
// TODO: host has an older version of expected profile installed, treat it as a missing
// profile for now but we should think about an appropriate grace period to account for
// clock skew between the host and the server or a checksum comparison instead
if !withinGracePeriod {
missingIdentifiers = append(missingIdentifiers, ep.Identifier)
}
continue
}
if ep.Name != ip.DisplayName {
// TODO: host has a different name for expected profile, skip it for now
// TODO: host has a different name for expected profile, treat it as a missing profile
// for now but we should think about a checksum comparison instead
if !withinGracePeriod {
missingIdentifiers = append(missingIdentifiers, ep.Identifier)
}
continue
}
verifiedIdentifiers = append(verifiedIdentifiers, ep.Identifier)
foundIdentifiers = append(foundIdentifiers, ep.Identifier)
}
if len(verifiedIdentifiers) == 0 {
if len(foundIdentifiers) == 0 && len(missingIdentifiers) == 0 {
// nothing to update, return early
return nil
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if err := setMDMProfilesVerifiedDB(ctx, tx, host, foundIdentifiers); err != nil {
return err
}
if err := setMDMProfilesFailedDB(ctx, tx, host, missingIdentifiers); err != nil {
return err
}
return nil
})
}
// setMDMProfilesFailedDB sets the status of the given identifiers to failed if the current status
// is verifying or verified. It also sets the detail to a message indicating that the profile was
// either verifying or verified. Only profiles with the install operation type are updated.
func setMDMProfilesFailedDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiers []string) error {
if len(identifiers) == 0 {
return nil
}
stmt := `
UPDATE
host_mdm_apple_profiles
SET
detail = if(status = ?, ?, ?),
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND profile_identifier IN(?)`
args := []interface{}{
fleet.MDMAppleDeliveryVerifying,
fleet.HostMDMProfileDetailFailedWasVerifying,
fleet.HostMDMProfileDetailFailedWasVerified,
fleet.MDMAppleDeliveryFailed,
host.UUID,
[]interface{}{fleet.MDMAppleDeliveryVerifying, fleet.MDMAppleDeliveryVerified},
fleet.MDMAppleOperationTypeInstall,
identifiers,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set failed host macOS profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting failed host macOS profiles")
}
return nil
}
// setMDMProfilesVerifiedDB sets the status of the given identifiers to verified if the current
// status is verifying. Only profiles with the install operation type are updated.
func setMDMProfilesVerifiedDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiers []string) error {
if len(identifiers) == 0 {
return nil
}
stmt := `
UPDATE
host_mdm_apple_profiles
SET
detail = '',
status = ?
WHERE
host_uuid = ?
AND status = ?
AND operation_type = 'install'
AND operation_type = ?
AND profile_identifier IN(?)`
args := []interface{}{fleet.MDMAppleDeliveryVerified, host.UUID, fleet.MDMAppleDeliveryVerifying, verifiedIdentifiers}
args := []interface{}{
fleet.MDMAppleDeliveryVerified,
host.UUID,
fleet.MDMAppleDeliveryVerifying,
fleet.MDMAppleOperationTypeInstall,
identifiers,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set verified host macOS profiles")
}
if _, err := ds.writer(ctx).ExecContext(ctx, stmt, args...); err != nil {
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting verified host macOS profiles")
}
return nil
}

View File

@ -6,6 +6,7 @@ import (
"crypto/sha256"
"database/sql"
"encoding/json"
"errors"
"fmt"
"sort"
"testing"
@ -1236,8 +1237,8 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
{Identifier: globalPfs[1].Identifier, DisplayName: globalPfs[1].Name, InstallDate: time.Now()},
{Identifier: globalPfs[2].Identifier, DisplayName: globalPfs[2].Name, InstallDate: time.Now()},
}
require.NoError(t, ds.SetVerifiedHostMacOSProfiles(ctx, host1, verified))
require.NoError(t, ds.SetVerifiedHostMacOSProfiles(ctx, host3, verified))
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, host1, verified))
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, host3, verified))
// still no profiles to install
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
@ -1552,7 +1553,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
require.Equal(t, uint(0), res.Verified)
// upsert hosts[0] filevault to verified
require.NoError(t, ds.SetVerifiedHostMacOSProfiles(ctx, hosts[0], []*fleet.HostMacOSProfile{{Identifier: fvNoTeam.Identifier, DisplayName: fvNoTeam.Name, InstallDate: time.Now()}}))
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[0], []*fleet.HostMacOSProfile{{Identifier: fvNoTeam.Identifier, DisplayName: fvNoTeam.Name, InstallDate: time.Now()}}))
res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, nil)
require.NoError(t, err)
require.NotNil(t, res)
@ -3849,7 +3850,7 @@ func testSetVerifiedMacOSProfiles(t *testing.T, ds *Datastore) {
var hosts []*fleet.Host
for i := 0; i < 3; i++ {
h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1",
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now())
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now().Add(-1*time.Hour))
hosts = append(hosts, h)
expectedHostMDMStatus[h.ID] = map[string]fleet.MDMAppleDeliveryStatus{
cp1.Identifier: fleet.MDMAppleDeliveryPending,
@ -3884,13 +3885,13 @@ func testSetVerifiedMacOSProfiles(t *testing.T, ds *Datastore) {
upsertHostCPs(hosts, []*fleet.MDMAppleConfigProfile{storedByIdentifier[cp3.Identifier]}, fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t)
checkHostMDMProfileStatuses()
// statuses don't change if profiles are missing (i.e. not installed)
require.NoError(t, ds.SetVerifiedHostMacOSProfiles(ctx, hosts[0], []*fleet.HostMacOSProfile{}))
// statuses don't change during the grace period if profiles are missing (i.e. not installed)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[0], []*fleet.HostMacOSProfile{}))
checkHostMDMProfileStatuses()
// only "verifying" status can change to "verified" so status of cp1 doesn't change (it
// remains "pending")
require.NoError(t, ds.SetVerifiedHostMacOSProfiles(ctx, hosts[0], []*fleet.HostMacOSProfile{
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[0], []*fleet.HostMacOSProfile{
{
Identifier: cp1.Identifier,
DisplayName: cp1.Name,
@ -3900,7 +3901,8 @@ func testSetVerifiedMacOSProfiles(t *testing.T, ds *Datastore) {
checkHostMDMProfileStatuses()
// if install date is before the updated at timestamp of the profile, statuses don't change
require.NoError(t, ds.SetVerifiedHostMacOSProfiles(ctx, hosts[1], []*fleet.HostMacOSProfile{
// during the grace period
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[1], []*fleet.HostMacOSProfile{
{
Identifier: cp1.Identifier,
DisplayName: cp1.Name,
@ -3921,7 +3923,7 @@ func testSetVerifiedMacOSProfiles(t *testing.T, ds *Datastore) {
// if install date is on or after the updated at timestamp of the profile, "verifying" status
// changes to "verified"
require.NoError(t, ds.SetVerifiedHostMacOSProfiles(ctx, hosts[2], []*fleet.HostMacOSProfile{
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[2], []*fleet.HostMacOSProfile{
{
Identifier: cp1.Identifier,
DisplayName: cp1.Name,
@ -3942,7 +3944,7 @@ func testSetVerifiedMacOSProfiles(t *testing.T, ds *Datastore) {
checkHostMDMProfileStatuses()
// repeated call doesn't change statuses
require.NoError(t, ds.SetVerifiedHostMacOSProfiles(ctx, hosts[2], []*fleet.HostMacOSProfile{
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[2], []*fleet.HostMacOSProfile{
{
Identifier: cp1.Identifier,
DisplayName: cp1.Name,
@ -3960,6 +3962,49 @@ func testSetVerifiedMacOSProfiles(t *testing.T, ds *Datastore) {
},
}))
checkHostMDMProfileStatuses()
// simulate expired grace period by setting updated_at timestamp of profiles back by 24 hours
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx,
`UPDATE mdm_apple_configuration_profiles SET updated_at = ? WHERE profile_id IN(?, ?, ?)`,
time.Now().Add(-24*time.Hour),
cp1.ProfileID, cp2.ProfileID, cp3.ProfileID,
)
return err
})
// after the grace period, status changes to "failed" if a profile is missing (i.e. not installed)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[2], []*fleet.HostMacOSProfile{
{
Identifier: cp1.Identifier,
DisplayName: cp1.Name,
InstallDate: time.Now(),
},
{
Identifier: cp2.Identifier,
DisplayName: cp2.Name,
InstallDate: time.Now(),
},
}))
expectedHostMDMStatus[hosts[2].ID][cp3.Identifier] = fleet.MDMAppleDeliveryFailed // cp3 is missing
checkHostMDMProfileStatuses()
// after the grace period, status changes to "failed" if a profile is outdated (i.e. installed
// before the updated at timestamp of the profile)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[2], []*fleet.HostMacOSProfile{
{
Identifier: cp1.Identifier,
DisplayName: cp1.Name,
InstallDate: time.Now(),
},
{
Identifier: cp2.Identifier,
DisplayName: cp2.Name,
InstallDate: time.Now().Add(-48 * time.Hour),
},
}))
expectedHostMDMStatus[hosts[2].ID][cp2.Identifier] = fleet.MDMAppleDeliveryFailed // cp2 is outdated
checkHostMDMProfileStatuses()
}
func TestHostDEPAssignments(t *testing.T) {
@ -4461,3 +4506,307 @@ func testMDMAppleDeleteHostDEPAssignments(t *testing.T, ds *Datastore) {
})
}
}
func TestMDMProfileVerification(t *testing.T) {
ds := CreateMySQLDS(t)
ctx := context.Background()
now := time.Now()
twoMinutesAgo := now.Add(-2 * time.Minute)
twoHoursAgo := now.Add(-2 * time.Hour)
twoDaysAgo := now.Add(-2 * 24 * time.Hour)
type testCase struct {
name string
initialStatus fleet.MDMAppleDeliveryStatus
expectedStatus fleet.MDMAppleDeliveryStatus
expectedDetail string
}
setupTestProfile := func(t *testing.T, suffix string) *fleet.MDMAppleConfigProfile {
cp, err := ds.NewMDMAppleConfigProfile(ctx, *configProfileForTest(t,
fmt.Sprintf("name-test-profile-%s", suffix),
fmt.Sprintf("identifier-test-profile-%s", suffix),
fmt.Sprintf("uuid-test-profile-%s", suffix)))
require.NoError(t, err)
return cp
}
setProfileUpdatedAt := func(t *testing.T, cp *fleet.MDMAppleConfigProfile, ua time.Time) {
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, `UPDATE mdm_apple_configuration_profiles SET updated_at = ? WHERE profile_id = ?`, ua, cp.ProfileID)
return err
})
}
checkHostStatus := func(t *testing.T, h *fleet.Host, expectedStatus fleet.MDMAppleDeliveryStatus, expectedDetail string) error {
gotProfs, err := ds.GetHostMDMProfiles(ctx, h.UUID)
if err != nil {
return err
}
if len(gotProfs) != 1 {
return errors.New("expected exactly one profile")
}
if gotProfs[0].Status == nil {
return errors.New("expected status to be non-nil")
}
if *gotProfs[0].Status != expectedStatus {
return fmt.Errorf("expected status %s, got %s", expectedStatus, *gotProfs[0].Status)
}
if gotProfs[0].Detail != expectedDetail {
return fmt.Errorf("expected detail %s, got %s", expectedDetail, gotProfs[0].Detail)
}
return nil
}
t.Run("MissingProfile", func(t *testing.T) {
// missing profile, verifying and verified statuses should change to failed after the grace period
cases := []testCase{
{
name: "PendingThenMissing",
initialStatus: fleet.MDMAppleDeliveryPending,
expectedStatus: fleet.MDMAppleDeliveryPending, // no change
expectedDetail: "",
},
{
name: "VerifyingThenMissing",
initialStatus: fleet.MDMAppleDeliveryVerifying,
expectedStatus: fleet.MDMAppleDeliveryFailed, // change to failed
expectedDetail: string(fleet.HostMDMProfileDetailFailedWasVerifying),
},
{
name: "VerifiedThenMissing",
initialStatus: fleet.MDMAppleDeliveryVerified,
expectedStatus: fleet.MDMAppleDeliveryFailed, // change to failed
expectedDetail: string(fleet.HostMDMProfileDetailFailedWasVerified),
},
{
name: "FailedThenMissing",
initialStatus: fleet.MDMAppleDeliveryFailed,
expectedStatus: fleet.MDMAppleDeliveryFailed, // no change
expectedDetail: "",
},
}
for i, tc := range cases {
// setup
h := test.NewHost(t, ds, tc.name, tc.name, tc.name, tc.name, twoMinutesAgo)
cp := setupTestProfile(t, fmt.Sprintf("%s-%d", tc.name, i))
var reportedProfiles []*fleet.HostMacOSProfile // no profiles reported for this test
// initialize
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &tc.initialStatus, ctx, ds, t)
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, ""))
// within grace period
setProfileUpdatedAt(t, cp, twoMinutesAgo)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, h, reportedProfiles))
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, "")) // if missing within grace period, no change
// reinitialize
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &tc.initialStatus, ctx, ds, t)
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, ""))
// outside grace period
setProfileUpdatedAt(t, cp, twoHoursAgo)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, h, reportedProfiles))
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // grace period expired, check expected status
}
})
t.Run("OutdatedProfile", func(t *testing.T) {
// found profile with the expected identifier, but it's outdated (i.e. the install date is
// before the last update date) so treat it as missing the expected profile verifying and
// verified statuses should change to failed after the grace period)
cases := []testCase{
{
name: "PendingThenFoundOutdated",
initialStatus: fleet.MDMAppleDeliveryPending,
expectedStatus: fleet.MDMAppleDeliveryPending, // no change
expectedDetail: "",
},
{
name: "VerifyingThenFoundOutdated",
initialStatus: fleet.MDMAppleDeliveryVerifying,
expectedStatus: fleet.MDMAppleDeliveryFailed, // change to failed
expectedDetail: string(fleet.HostMDMProfileDetailFailedWasVerifying),
},
{
name: "VerifiedThenFoundOutdated",
initialStatus: fleet.MDMAppleDeliveryVerified,
expectedStatus: fleet.MDMAppleDeliveryFailed, // change to failed
expectedDetail: string(fleet.HostMDMProfileDetailFailedWasVerified),
},
{
name: "FailedThenFoundOutdated",
initialStatus: fleet.MDMAppleDeliveryFailed,
expectedStatus: fleet.MDMAppleDeliveryFailed, // no change
expectedDetail: "",
},
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// setup
h := test.NewHost(t, ds, tc.name, tc.name, tc.name, tc.name, twoMinutesAgo)
cp := setupTestProfile(t, fmt.Sprintf("%s-%d", tc.name, i))
reportedProfiles := []*fleet.HostMacOSProfile{
{
DisplayName: cp.Name,
Identifier: cp.Identifier,
InstallDate: twoDaysAgo,
},
}
// initialize
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &tc.initialStatus, ctx, ds, t)
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, ""))
// within grace period
setProfileUpdatedAt(t, cp, twoMinutesAgo)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, h, reportedProfiles))
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, "")) // outdated profiles are treated similar to missing profiles so status doesn't change if within grace period
// reinitalize
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &tc.initialStatus, ctx, ds, t)
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, ""))
// outside grace period
setProfileUpdatedAt(t, cp, twoHoursAgo)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, h, reportedProfiles))
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // grace period expired, check expected status
})
}
})
t.Run("ExpectedProfile", func(t *testing.T) {
// happy path, expected profile found so verifying should change to verified
cases := []testCase{
{
name: "PendingThenFoundExpected",
initialStatus: fleet.MDMAppleDeliveryPending,
expectedStatus: fleet.MDMAppleDeliveryPending, // no change
expectedDetail: "",
},
{
name: "VerifyingThenFoundExpected",
initialStatus: fleet.MDMAppleDeliveryVerifying,
expectedStatus: fleet.MDMAppleDeliveryVerified, // change to verified
expectedDetail: "",
},
{
name: "VerifiedThenFoundExpected",
initialStatus: fleet.MDMAppleDeliveryVerified,
expectedStatus: fleet.MDMAppleDeliveryVerified, // no change
expectedDetail: "",
},
{
name: "FailedThenFoundExpected",
initialStatus: fleet.MDMAppleDeliveryFailed,
expectedStatus: fleet.MDMAppleDeliveryFailed, // no change
expectedDetail: "",
},
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// setup
h := test.NewHost(t, ds, tc.name, tc.name, tc.name, tc.name, twoMinutesAgo)
cp := setupTestProfile(t, fmt.Sprintf("%s-%d", tc.name, i))
reportedProfiles := []*fleet.HostMacOSProfile{
{
DisplayName: cp.Name,
Identifier: cp.Identifier,
InstallDate: now,
},
}
// initialize
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &tc.initialStatus, ctx, ds, t)
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, ""))
// within grace period
setProfileUpdatedAt(t, cp, twoMinutesAgo)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, h, reportedProfiles))
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // if found within grace period, verifying status can become verified so check expected status
// reinitialize
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &tc.initialStatus, ctx, ds, t)
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, ""))
// outside grace period
setProfileUpdatedAt(t, cp, twoHoursAgo)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, h, reportedProfiles))
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // grace period expired, check expected status
})
}
})
t.Run("UnexpectedProfile", func(t *testing.T) {
// unexpected profile is ignored and doesn't change status of existing profile
cases := []testCase{
{
name: "PendingThenFoundExpectedAndUnexpected",
initialStatus: fleet.MDMAppleDeliveryPending,
expectedStatus: fleet.MDMAppleDeliveryPending, // no change
expectedDetail: "",
},
{
name: "VerifyingThenFoundExpectedAndUnexpected",
initialStatus: fleet.MDMAppleDeliveryVerifying,
expectedStatus: fleet.MDMAppleDeliveryVerified, // no change
expectedDetail: "",
},
{
name: "VerifiedThenFounExpectedAnddUnexpected",
initialStatus: fleet.MDMAppleDeliveryVerified,
expectedStatus: fleet.MDMAppleDeliveryVerified, // no change
expectedDetail: "",
},
{
name: "FailedThenFoundExpectedAndUnexpected",
initialStatus: fleet.MDMAppleDeliveryFailed,
expectedStatus: fleet.MDMAppleDeliveryFailed, // no change
expectedDetail: "",
},
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// setup
h := test.NewHost(t, ds, tc.name, tc.name, tc.name, tc.name, twoMinutesAgo)
cp := setupTestProfile(t, fmt.Sprintf("%s-%d", tc.name, i))
reportedProfiles := []*fleet.HostMacOSProfile{
{
DisplayName: "unexpected-name",
Identifier: "unexpected-identifier",
InstallDate: now,
},
{
DisplayName: cp.Name,
Identifier: cp.Identifier,
InstallDate: now,
},
}
// initialize
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &tc.initialStatus, ctx, ds, t)
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, ""))
// within grace period
setProfileUpdatedAt(t, cp, twoMinutesAgo)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, h, reportedProfiles))
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // if found within grace period, verifying status can become verified so check expected status
// reinitialize
upsertHostCPs([]*fleet.Host{h}, []*fleet.MDMAppleConfigProfile{cp}, fleet.MDMAppleOperationTypeInstall, &tc.initialStatus, ctx, ds, t)
require.NoError(t, checkHostStatus(t, h, tc.initialStatus, ""))
// outside grace period
setProfileUpdatedAt(t, cp, twoHoursAgo)
require.NoError(t, ds.UpdateVerificationHostMacOSProfiles(ctx, h, reportedProfiles))
require.NoError(t, checkHostStatus(t, h, tc.expectedStatus, tc.expectedDetail)) // grace period expired, check expected status
})
}
})
}

View File

@ -322,6 +322,18 @@ func (cp MDMAppleConfigProfile) ValidateUserProvided() error {
return cp.Mobileconfig.ScreenPayloads()
}
// IsWithinGracePeriod returns true if the host is within the grace period for the profile.
//
// The grace period is defined as 1 hour after the profile was updated. It is checked against the
// host's detail_updated_at timestamp to allow for the host to check in at least once before the
// profile is considered failed. If the host is online, it should report detail queries hourly by
// default. If the host is offline, it should report detail queries shortly after it comes back
// online.
func (cp MDMAppleConfigProfile) IsWithinGracePeriod(hostDetailUpdatedAt time.Time) bool {
gracePeriod := 1 * time.Hour
return hostDetailUpdatedAt.Before(cp.UpdatedAt.Add(gracePeriod))
}
// HostMDMAppleProfile represents the status of an Apple MDM profile in a host.
type HostMDMAppleProfile struct {
HostUUID string `db:"host_uuid" json:"-"`
@ -345,6 +357,25 @@ func (p HostMDMAppleProfile) IgnoreMDMClientError() bool {
return false
}
type HostMDMProfileDetail string
const (
HostMDMProfileDetailFailedWasVerified HostMDMProfileDetail = "Failed, was verified"
HostMDMProfileDetailFailedWasVerifying HostMDMProfileDetail = "Failed, was verifying"
)
// Message returns a human-friendly message for the detail.
func (d HostMDMProfileDetail) Message() string {
switch d {
case HostMDMProfileDetailFailedWasVerified:
return "This setting had been verified by osquery, but has since been found missing on the host."
case HostMDMProfileDetailFailedWasVerifying:
return "The MDM protocol returned a success but the setting couldnt be verified by osquery."
default:
return string(d)
}
}
type MDMAppleProfilePayload struct {
ProfileID uint `db:"profile_id"`
ProfileIdentifier string `db:"profile_identifier"`
@ -361,6 +392,7 @@ type MDMAppleBulkUpsertHostProfilePayload struct {
CommandUUID string
OperationType MDMAppleOperationType
Status *MDMAppleDeliveryStatus
Detail string
Checksum []byte
}

View File

@ -1,6 +1,7 @@
package fleet
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
@ -326,3 +327,50 @@ func TestHostDEPAssignment(t *testing.T) {
})
}
}
func TestMDMProfileIsWithinGracePeriod(t *testing.T) {
// create a test profile
var b bytes.Buffer
params := mobileconfig.FleetdProfileOptions{
EnrollSecret: t.Name(),
ServerURL: "https://example.com",
PayloadType: mobileconfig.FleetdConfigPayloadIdentifier,
}
err := mobileconfig.FleetdProfileTemplate.Execute(&b, params)
require.NoError(t, err)
testProfile, err := NewMDMAppleConfigProfile(b.Bytes(), nil)
require.NoError(t, err)
// set profile updated at 2 hours ago
testProfile.UpdatedAt = time.Now().Truncate(time.Second).Add(-2 * time.Hour)
// set profile created at 24 hours ago (irrelevant but included for completeness)
testProfile.CreatedAt = testProfile.UpdatedAt.Add(-24 * time.Hour)
cases := []struct {
testName string
hostDetailUpdatedAt time.Time
expect bool
}{
{
testName: "outside grace period",
hostDetailUpdatedAt: testProfile.UpdatedAt.Add(61 * time.Minute), // more than 1 hour grace period
expect: false,
},
{
testName: "online host within grace period",
hostDetailUpdatedAt: testProfile.UpdatedAt.Add(59 * time.Minute), // less than 1 hour grace period
expect: true,
},
{
testName: "offline host within grace period",
hostDetailUpdatedAt: testProfile.UpdatedAt.Add(-48 * time.Hour), // grace period doesn't start until host is online (i.e. host detail updated at is after profile updated at)
expect: true,
},
}
for _, c := range cases {
t.Run(c.testName, func(t *testing.T) {
require.Equal(t, c.expect, testProfile.IsWithinGracePeriod(c.hostDetailUpdatedAt))
})
}
}

View File

@ -698,8 +698,8 @@ type Datastore interface {
SetDiskEncryptionResetStatus(ctx context.Context, hostID uint, status bool) error
// SetVerifiedHostMacOSProfiles updates status of macOS profiles installed on a given host to verified.
SetVerifiedHostMacOSProfiles(ctx context.Context, host *Host, installedProfiles []*HostMacOSProfile) error
// UpdateVerificationHostMacOSProfiles updates status of macOS profiles installed on a given host to verified.
UpdateVerificationHostMacOSProfiles(ctx context.Context, host *Host, installedProfiles []*HostMacOSProfile) error
// SetOrUpdateHostOrbitInfo inserts of updates the orbit info for a host
SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error

View File

@ -486,7 +486,7 @@ type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet
type SetDiskEncryptionResetStatusFunc func(ctx context.Context, hostID uint, status bool) error
type SetVerifiedHostMacOSProfilesFunc func(ctx context.Context, host *fleet.Host, installedProfiles []*fleet.HostMacOSProfile) error
type UpdateVerificationHostMacOSProfilesFunc func(ctx context.Context, host *fleet.Host, installedProfiles []*fleet.HostMacOSProfile) error
type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string) error
@ -1352,8 +1352,8 @@ type DataStore struct {
SetDiskEncryptionResetStatusFunc SetDiskEncryptionResetStatusFunc
SetDiskEncryptionResetStatusFuncInvoked bool
SetVerifiedHostMacOSProfilesFunc SetVerifiedHostMacOSProfilesFunc
SetVerifiedHostMacOSProfilesFuncInvoked bool
UpdateVerificationHostMacOSProfilesFunc UpdateVerificationHostMacOSProfilesFunc
UpdateVerificationHostMacOSProfilesFuncInvoked bool
SetOrUpdateHostOrbitInfoFunc SetOrUpdateHostOrbitInfoFunc
SetOrUpdateHostOrbitInfoFuncInvoked bool
@ -3240,11 +3240,11 @@ func (s *DataStore) SetDiskEncryptionResetStatus(ctx context.Context, hostID uin
return s.SetDiskEncryptionResetStatusFunc(ctx, hostID, status)
}
func (s *DataStore) SetVerifiedHostMacOSProfiles(ctx context.Context, host *fleet.Host, installedProfiles []*fleet.HostMacOSProfile) error {
func (s *DataStore) UpdateVerificationHostMacOSProfiles(ctx context.Context, host *fleet.Host, installedProfiles []*fleet.HostMacOSProfile) error {
s.mu.Lock()
s.SetVerifiedHostMacOSProfilesFuncInvoked = true
s.UpdateVerificationHostMacOSProfilesFuncInvoked = true
s.mu.Unlock()
return s.SetVerifiedHostMacOSProfilesFunc(ctx, host, installedProfiles)
return s.UpdateVerificationHostMacOSProfilesFunc(ctx, host, installedProfiles)
}
func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {

View File

@ -899,13 +899,14 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
policies = &hp
}
// If Fleet MDM is enabled and configured, we want to include MDM profiles
// and host's disk encryption status.
var profiles []fleet.HostMDMAppleProfile
// If Fleet MDM is enabled and configured, we want to include MDM profiles,
// disk encryption status, and macOS setup details.
ac, err := svc.ds.AppConfig(ctx)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get app config for host mdm profiles")
return nil, ctxerr.Wrap(ctx, err, "get app config for host mdm details")
}
var profiles []fleet.HostMDMAppleProfile
if ac.MDM.EnabledAndConfigured {
profs, err := svc.ds.GetHostMDMProfiles(ctx, host.UUID)
if err != nil {
@ -920,6 +921,7 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
if p.Identifier == mobileconfig.FleetFileVaultPayloadIdentifier {
p.Status = host.MDM.ProfileStatusFromDiskEncryptionState(p.Status)
}
p.Detail = fleet.HostMDMProfileDetail(p.Detail).Message()
profiles = append(profiles, p)
}
}

View File

@ -981,3 +981,102 @@ func TestHostEncryptionKey(t *testing.T) {
require.Error(t, err)
})
}
func TestHostMDMProfileDetail(t *testing.T) {
ds := new(mock.Store)
testBMToken := &nanodep_client.OAuth1Tokens{
ConsumerKey: "test_consumer",
ConsumerSecret: "test_secret",
AccessToken: "test_access_token",
AccessSecret: "test_access_secret",
AccessTokenExpiry: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
}
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
require.NoError(t, err)
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
fleetCfg := config.TestConfig()
config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, testBMToken)
svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
return &fleet.Host{
ID: 1,
}, nil
}
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
return nil
}
ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) {
return nil, nil
}
ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) {
return nil, nil
}
ds.ListHostBatteriesFunc = func(ctx context.Context, hid uint) ([]*fleet.HostBattery, error) {
return nil, nil
}
ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) {
return nil, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
},
}, nil
}
cases := []struct {
name string
storedDetail string
expectedDetail string
}{
{
name: "no detail",
storedDetail: "",
expectedDetail: "",
},
{
name: "other detail",
storedDetail: "other detail",
expectedDetail: "other detail",
},
{
name: "failed was verifying",
storedDetail: string(fleet.HostMDMProfileDetailFailedWasVerifying),
expectedDetail: fleet.HostMDMProfileDetailFailedWasVerifying.Message(),
},
{
name: "failed was verified",
storedDetail: string(fleet.HostMDMProfileDetailFailedWasVerified),
expectedDetail: fleet.HostMDMProfileDetailFailedWasVerified.Message(),
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ds.GetHostMDMProfilesFunc = func(ctx context.Context, host_uuid string) ([]fleet.HostMDMAppleProfile, error) {
return []fleet.HostMDMAppleProfile{
{
Name: "test",
Identifier: "test",
OperationType: fleet.MDMAppleOperationTypeInstall,
Status: &fleet.MDMAppleDeliveryFailed,
Detail: tt.storedDetail,
},
}, nil
}
h, err := svc.GetHost(ctx, uint(1), fleet.HostDetailOptions{})
require.NoError(t, err)
require.NotNil(t, h.MDM.Profiles)
profs := *h.MDM.Profiles
require.Len(t, profs, 1)
require.Equal(t, tt.expectedDetail, profs[0].Detail)
})
}
}

View File

@ -2110,7 +2110,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleDiskEncryptionAggregate() {
require.Equal(t, uint(0), fvsResp.RemovingEnforcement)
// verified status for host 1
require.NoError(t, s.ds.SetVerifiedHostMacOSProfiles(ctx, hosts[0], []*fleet.HostMacOSProfile{{Identifier: prof.Identifier, DisplayName: prof.Name, InstallDate: time.Now()}}))
require.NoError(t, s.ds.UpdateVerificationHostMacOSProfiles(ctx, hosts[0], []*fleet.HostMacOSProfile{{Identifier: prof.Identifier, DisplayName: prof.Name, InstallDate: time.Now()}}))
fvsResp = getMDMAppleFileVauleSummaryResponse{}
s.DoJSON("GET", "/api/latest/fleet/mdm/apple/filevault/summary", nil, http.StatusOK, &fvsResp, "team_id", strconv.Itoa(int(tm.ID)))
require.Equal(t, uint(2), fvsResp.Verifying)
@ -2971,7 +2971,7 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesStatus() {
})
// h1 verified one of the profiles
require.NoError(t, s.ds.SetVerifiedHostMacOSProfiles(context.Background(), h1, []*fleet.HostMacOSProfile{
require.NoError(t, s.ds.UpdateVerificationHostMacOSProfiles(context.Background(), h1, []*fleet.HostMacOSProfile{
{Identifier: "G2b", DisplayName: "G2b", InstallDate: time.Now()},
}))
s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{

View File

@ -1521,7 +1521,7 @@ func directIngestMacOSProfiles(
})
}
return ds.SetVerifiedHostMacOSProfiles(ctx, host, mapping)
return ds.UpdateVerificationHostMacOSProfiles(ctx, host, mapping)
}
//go:generate go run gen_queries_doc.go ../../../docs/Using-Fleet/Detail-Queries-Summary.md

View File

@ -1124,7 +1124,7 @@ func TestDirectIngestHostMacOSProfiles(t *testing.T) {
h := &fleet.Host{ID: 1}
var expectedProfiles []*fleet.HostMacOSProfile
ds.SetVerifiedHostMacOSProfilesFunc = func(ctx context.Context, host *fleet.Host, installedProfiles []*fleet.HostMacOSProfile) error {
ds.UpdateVerificationHostMacOSProfilesFunc = func(ctx context.Context, host *fleet.Host, installedProfiles []*fleet.HostMacOSProfile) error {
require.Equal(t, h.ID, host.ID)
require.Len(t, installedProfiles, len(expectedProfiles))
expectedByIdentifier := make(map[string]*fleet.HostMacOSProfile, len(expectedProfiles))