implement profile verification for windows

This commit is contained in:
Roberto Dip 2023-11-27 10:21:29 -03:00
parent d7c233d54c
commit 7531a0742b
23 changed files with 1349 additions and 293 deletions

View file

@ -726,7 +726,7 @@ the way that the Fleet server works.
}
}
if appCfg.MDM.EnabledAndConfigured {
if appCfg.MDM.EnabledAndConfigured || appCfg.MDM.WindowsEnabledAndConfigured {
if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
return newMDMProfileManager(
ctx,

View file

@ -13,6 +13,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"

View file

@ -1730,209 +1730,6 @@ func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, prof
return err
}
func (ds *Datastore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if err := setMDMProfilesVerifiedDB(ctx, tx, host, toVerify); err != nil {
return err
}
if err := setMDMProfilesFailedDB(ctx, tx, host, toFail); err != nil {
return err
}
if err := setMDMProfilesRetryDB(ctx, tx, host, toRetry); err != nil {
return err
}
return nil
})
}
// setMDMProfilesRetryDB sets the status of the given identifiers to retry (nil) and increments the retry count
func setMDMProfilesRetryDB(ctx context.Context, tx sqlx.ExtContext, hostUUID string, identifiers []string) error {
if len(identifiers) == 0 {
return nil
}
stmt := `
UPDATE
host_mdm_apple_profiles
SET
status = NULL,
detail = '',
retries = retries + 1
WHERE
host_uuid = ?
AND operation_type = ?
AND profile_identifier IN(?)`
args := []interface{}{
hostUUID,
fleet.MDMOperationTypeInstall,
identifiers,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set retry host macOS profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting retry host macOS profiles")
}
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, hostUUID string, 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.MDMDeliveryVerifying,
fleet.HostMDMProfileDetailFailedWasVerifying,
fleet.HostMDMProfileDetailFailedWasVerified,
fleet.MDMDeliveryFailed,
hostUUID,
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified},
fleet.MDMOperationTypeInstall,
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, hostUUID string, identifiers []string) error {
if len(identifiers) == 0 {
return nil
}
stmt := `
UPDATE
host_mdm_apple_profiles
SET
detail = '',
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND profile_identifier IN(?)`
args := []interface{}{
fleet.MDMDeliveryVerified,
hostUUID,
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryFailed},
fleet.MDMOperationTypeInstall,
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 := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting verified host macOS profiles")
}
return nil
}
func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
var teamID uint
if host.TeamID != nil {
teamID = *host.TeamID
}
stmt := `
SELECT
identifier,
earliest_install_date
FROM
mdm_apple_configuration_profiles macp
JOIN (
SELECT
checksum,
min(updated_at) AS earliest_install_date
FROM
mdm_apple_configuration_profiles
GROUP BY
checksum) cs
ON macp.checksum = cs.checksum
WHERE
macp.team_id = ?`
var rows []*fleet.ExpectedMDMProfile
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host %d", host.ID))
}
byIdentifier := make(map[string]*fleet.ExpectedMDMProfile, len(rows))
for _, r := range rows {
byIdentifier[r.Identifier] = r
}
return byIdentifier, nil
}
func (ds *Datastore) GetHostMDMProfilesRetryCounts(ctx context.Context, hostUUID string) ([]fleet.HostMDMProfileRetryCount, error) {
stmt := `
SELECT
profile_identifier,
retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?`
var dest []fleet.HostMDMProfileRetryCount
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, hostUUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry counts for host %s", hostUUID))
}
return dest, nil
}
func (ds *Datastore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, hostUUID, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
stmt := `
SELECT
profile_identifier, retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?
AND hmap.command_uuid = ?`
var dest fleet.HostMDMProfileRetryCount
if err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, hostUUID, cmdUUID); err != nil {
if err == sql.ErrNoRows {
return dest, notFound("HostMDMCommand").WithMessage(fmt.Sprintf("command uuid %s not found for host uuid %s", cmdUUID, hostUUID))
}
return dest, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry count for host %s command uuid %s", hostUUID, cmdUUID))
}
return dest, nil
}
func subqueryHostsMacOSSettingsStatusFailed() (string, []interface{}) {
sql := `
SELECT

View file

@ -4242,7 +4242,7 @@ func testMDMAppleDeleteHostDEPAssignments(t *testing.T, ds *Datastore) {
}
}
func TestMDMProfileVerification(t *testing.T) {
func TestMDMAppleProfileVerification(t *testing.T) {
ds := CreateMySQLDS(t)
ctx := context.Background()

View file

@ -2,6 +2,7 @@ package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
@ -314,3 +315,306 @@ WHERE
return nil
})
}
func (ds *Datastore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if err := setMDMProfilesVerifiedDB(ctx, tx, host, toVerify); err != nil {
return err
}
if err := setMDMProfilesFailedDB(ctx, tx, host, toFail); err != nil {
return err
}
if err := setMDMProfilesRetryDB(ctx, tx, host, toRetry); err != nil {
return err
}
return nil
})
}
// setMDMProfilesRetryDB sets the status of the given identifiers to retry (nil) and increments the retry count
func setMDMProfilesRetryDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
status = NULL,
detail = '',
retries = retries + 1
WHERE
host_uuid = ?
AND operation_type = ?
AND %s IN(?)`
args := []interface{}{
host.UUID,
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
var stmt string
switch host.Platform {
case "darwin":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set retry host profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting retry host profiles")
}
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, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
detail = if(status = ?, ?, ?),
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND %s IN(?)`
var stmt string
switch host.Platform {
case "darwin":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
args := []interface{}{
fleet.MDMDeliveryVerifying,
fleet.HostMDMProfileDetailFailedWasVerifying,
fleet.HostMDMProfileDetailFailedWasVerified,
fleet.MDMDeliveryFailed,
host.UUID,
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified},
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set failed host profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting failed host 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, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
detail = '',
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND %s IN(?)`
var stmt string
switch host.Platform {
case "darwin":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
args := []interface{}{
fleet.MDMDeliveryVerified,
host.UUID,
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryFailed},
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
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 := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting verified host profiles")
}
return nil
}
func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
var teamID uint
if host.TeamID != nil {
teamID = *host.TeamID
}
switch host.Platform {
case "darwin":
return ds.getHostMDMAppleProfilesExpectedForVerification(ctx, teamID)
case "windows":
return ds.getHostMDMWindowsProfilesExpectedForVerification(ctx, teamID)
default:
return nil, fmt.Errorf("unsupported platform: %s", host.Platform)
}
}
func (ds *Datastore) getHostMDMWindowsProfilesExpectedForVerification(ctx context.Context, teamID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
stmt := `
SELECT name, syncml as raw_profile, updated_at as earliest_install_date
FROM mdm_windows_configuration_profiles mwcp
WHERE mwcp.team_id = ?
`
var profiles []*fleet.ExpectedMDMProfile
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, teamID)
if err != nil {
return nil, err
}
byName := make(map[string]*fleet.ExpectedMDMProfile, len(profiles))
for _, r := range profiles {
byName[r.Name] = r
}
return byName, nil
}
func (ds *Datastore) getHostMDMAppleProfilesExpectedForVerification(ctx context.Context, teamID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
stmt := `
SELECT
identifier,
earliest_install_date
FROM
mdm_apple_configuration_profiles macp
JOIN (
SELECT
checksum,
min(updated_at) AS earliest_install_date
FROM
mdm_apple_configuration_profiles
GROUP BY
checksum) cs
ON macp.checksum = cs.checksum
WHERE
macp.team_id = ?`
var rows []*fleet.ExpectedMDMProfile
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host in team %d", teamID))
}
byIdentifier := make(map[string]*fleet.ExpectedMDMProfile, len(rows))
for _, r := range rows {
byIdentifier[r.Identifier] = r
}
return byIdentifier, nil
}
func (ds *Datastore) GetHostMDMProfilesRetryCounts(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error) {
const darwinStmt = `
SELECT
profile_identifier,
retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?`
const windowsStmt = `
SELECT
profile_name,
retries
FROM
host_mdm_windows_profiles hmwp
WHERE
hmwp.host_uuid = ?`
var stmt string
switch host.Platform {
case "darwin":
stmt = darwinStmt
case "windows":
stmt = windowsStmt
default:
return nil, fmt.Errorf("unsupported platform %s", host.Platform)
}
var dest []fleet.HostMDMProfileRetryCount
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, host.UUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry counts for host %s", host.UUID))
}
return dest, nil
}
func (ds *Datastore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
const darwinStmt = `
SELECT
profile_identifier, retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?
AND hmap.command_uuid = ?`
const windowsStmt = `
SELECT
profile_uuid, retries
FROM
host_mdm_windows_profiles hmwp
WHERE
hmwp.host_uuid = ?
AND hmwp.command_uuid = ?`
var stmt string
switch host.Platform {
case "darwin":
stmt = darwinStmt
case "windows":
stmt = windowsStmt
default:
return fleet.HostMDMProfileRetryCount{}, fmt.Errorf("unsupported platform %s", host.Platform)
}
var dest fleet.HostMDMProfileRetryCount
if err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, host.UUID, cmdUUID); err != nil {
if err == sql.ErrNoRows {
return dest, notFound("HostMDMCommand").WithMessage(fmt.Sprintf("command uuid %s not found for host uuid %s", cmdUUID, host.UUID))
}
return dest, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry count for host %s command uuid %s", host.UUID, cmdUUID))
}
return dest, nil
}

View file

@ -221,6 +221,8 @@ WHERE
return commands, nil
}
// TODO(roberto): much of this logic should be living in the service layer,
// would be nice to get the time to properly plan and implement.
func (ds *Datastore) MDMWindowsSaveResponse(ctx context.Context, deviceID string, fullResponse *fleet.SyncML) error {
if len(fullResponse.Raw) == 0 {
return ctxerr.New(ctx, "empty raw response")
@ -357,6 +359,8 @@ ON DUPLICATE KEY UPDATE
// updateMDMWindowsHostProfileStatusFromResponseDB takes a slice of potential
// profile payloads and updates the corresponding `status` and `detail` columns
// in `host_mdm_windows_profiles`
// TODO(roberto): much of this logic should be living in the service layer,
// would be nice to get the time to properly plan and implement.
func updateMDMWindowsHostProfileStatusFromResponseDB(
ctx context.Context,
tx sqlx.ExtContext,
@ -372,16 +376,17 @@ func updateMDMWindowsHostProfileStatusFromResponseDB(
// update their detail and status.
const updateHostProfilesStmt = `
INSERT INTO host_mdm_windows_profiles
(host_uuid, profile_uuid, detail, status, command_uuid)
(host_uuid, profile_uuid, detail, status, retries, command_uuid)
VALUES %s
ON DUPLICATE KEY UPDATE
detail = VALUES(detail),
status = VALUES(status)`
status = VALUES(status),
retries = VALUES(retries)`
// MySQL will use the `host_uuid` part of the primary key as a first
// pass, and then filter that subset by `command_uuid`.
const getMatchingHostProfilesStmt = `
SELECT host_uuid, profile_uuid, command_uuid
SELECT host_uuid, profile_uuid, command_uuid, retries
FROM host_mdm_windows_profiles
WHERE host_uuid = ? AND command_uuid IN (?)`
@ -410,13 +415,24 @@ func updateMDMWindowsHostProfileStatusFromResponseDB(
return ctxerr.Wrap(ctx, err, "running query to get matching profiles")
}
// batch-update the matching entries with the desired detail and status>
// batch-update the matching entries with the desired detail and status
var sb strings.Builder
args = args[:0]
for _, hp := range matchingHostProfiles {
payload := uuidsToPayloads[hp.CommandUUID]
args = append(args, hp.HostUUID, hp.ProfileUUID, payload.Detail, payload.Status)
sb.WriteString("(?, ?, ?, ?, command_uuid),")
if payload.Status != nil && *payload.Status == fleet.MDMDeliveryFailed {
if hp.Retries < mdm.MaxProfileRetries {
// if we haven't hit the max retries, we set
// the host profile status to nil (which causes
// an install profile command to be enqueued
// the next time the profile manager cron runs)
// and increment the retry count
payload.Status = nil
hp.Retries++
}
}
args = append(args, hp.HostUUID, hp.ProfileUUID, payload.Detail, payload.Status, hp.Retries)
sb.WriteString("(?, ?, ?, ?, ?, command_uuid),")
}
stmt = fmt.Sprintf(updateHostProfilesStmt, strings.TrimSuffix(sb.String(), ","))

View file

@ -726,15 +726,15 @@ type Datastore interface {
// should be verified, failed, and retried, respectively. For each profile in the toRetry slice,
// the retries count is incremented by 1 and the status is set to null so that an install
// profile command is enqueued the next time the profile manager cron runs.
UpdateHostMDMProfilesVerification(ctx context.Context, hostUUID string, toVerify, toFail, toRetry []string) error
UpdateHostMDMProfilesVerification(ctx context.Context, host *Host, toVerify, toFail, toRetry []string) error
// GetHostMDMProfilesExpected returns the expected MDM profiles for a given host. The map is
// keyed by the profile identifier.
GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *Host) (map[string]*ExpectedMDMProfile, error)
// GetHostMDMProfilesRetryCounts returns a list of MDM profile retry counts for a given host.
GetHostMDMProfilesRetryCounts(ctx context.Context, hostUUID string) ([]HostMDMProfileRetryCount, error)
GetHostMDMProfilesRetryCounts(ctx context.Context, host *Host) ([]HostMDMProfileRetryCount, error)
// GetHostMDMProfileRetryCountByCommandUUID returns the retry count for the specified
// host UUID and command UUID.
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, hostUUID, cmdUUID string) (HostMDMProfileRetryCount, error)
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *Host, cmdUUID string) (HostMDMProfileRetryCount, error)
// SetOrUpdateHostOrbitInfo inserts of updates the orbit info for a host
SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error
@ -1192,6 +1192,31 @@ const (
DefaultMunkiIssuesBatchSize = 100
)
// ProfileVerificationStore is the minimal interface required to get and update the verification
// status of a host's MDM profiles. The Fleet Datastore satisfies this interface.
type ProfileVerificationStore interface {
// GetHostMDMProfilesExpectedForVerification returns the expected MDM profiles for a given host. The map is
// keyed by the profile identifier.
GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *Host) (map[string]*ExpectedMDMProfile, error)
// GetHostMDMProfilesRetryCounts returns the retry counts for the specified host.
GetHostMDMProfilesRetryCounts(ctx context.Context, host *Host) ([]HostMDMProfileRetryCount, error)
// GetHostMDMProfileRetryCountByCommandUUID returns the retry count for the specified
// host UUID and command UUID.
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *Host, commandUUID string) (HostMDMProfileRetryCount, error)
// UpdateHostMDMProfilesVerification updates status of macOS profiles installed on a given
// host. The toVerify, toFail, and toRetry slices contain the identifiers of the profiles that
// should be verified, failed, and retried, respectively. For each profile in the toRetry slice,
// the retries count is incremented by 1 and the status is set to null so that an install
// profile command is enqueued the next time the profile manager cron runs.
UpdateHostMDMProfilesVerification(ctx context.Context, host *Host, toVerify, toFail, toRetry []string) error
// UpdateOrDeleteHostMDMAppleProfile updates information about a single
// profile status. It deletes the row if the profile operation is "remove"
// and the status is "verifying" (i.e. successfully removed).
UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profile *HostMDMAppleProfile) error
}
var _ ProfileVerificationStore = (Datastore)(nil)
type PolicyFailure struct {
PolicyID uint
Host PolicySetHost

View file

@ -103,7 +103,10 @@ func (e MDMAppleEULA) AuthzType() string {
// ExpectedMDMProfile represents an MDM profile that is expected to be installed on a host.
type ExpectedMDMProfile struct {
// Identifier is the unique identifier used by macOS profiles
Identifier string `db:"identifier"`
// Name is the unique name used by Windows profiles
Name string `db:"name"`
// EarliestInstallDate is the earliest updated_at of all team profiles with the same checksum.
// It is used to assess the case where a host has installed a profile with the identifier
// expected by the host's current team, but the host's install_date is earlier than the
@ -113,6 +116,8 @@ type ExpectedMDMProfile struct {
// Ideally, we would simply compare the checksums of the installed and expected profiles, but
// the checksums are not available in the osquery profiles table.
EarliestInstallDate time.Time `db:"earliest_install_date"`
// RawProfile contains the raw profile contents
RawProfile []byte `db:"raw_profile"`
}
// IsWithinGracePeriod returns true if the host is within the grace period for the profile.
@ -133,8 +138,11 @@ func (ep ExpectedMDMProfile) IsWithinGracePeriod(hostDetailUpdatedAt time.Time)
// HostMDMProfileRetryCount represents the number of times Fleet has attempted to install
// the identified profile on a host.
type HostMDMProfileRetryCount struct {
// Identifier is the unique identifier used by macOS profiles
ProfileIdentifier string `db:"profile_identifier"`
Retries uint `db:"retries"`
// ProfileName is the unique name used by Windows profiles
ProfileName string `db:"profile_name"`
Retries uint `db:"retries"`
}
// TeamIDSetter defines the method to set a TeamID value on a struct,

View file

@ -920,13 +920,19 @@ type ProtoCmdOperation struct {
// Protocol Command
type SyncMLCmd struct {
XMLName xml.Name `xml:",omitempty"`
CmdID string `xml:"CmdID"`
MsgRef *string `xml:"MsgRef,omitempty"`
CmdRef *string `xml:"CmdRef,omitempty"`
Cmd *string `xml:"Cmd,omitempty"`
Data *string `xml:"Data,omitempty"`
Items []CmdItem `xml:"Item,omitempty"`
XMLName xml.Name `xml:",omitempty"`
CmdID string `xml:"CmdID"`
MsgRef *string `xml:"MsgRef,omitempty"`
CmdRef *string `xml:"CmdRef,omitempty"`
Cmd *string `xml:"Cmd,omitempty"`
Data *string `xml:"Data,omitempty"`
Items []CmdItem `xml:"Item,omitempty"`
// ReplaceCommands is a catch-all for any nested <Replace> commands,
// which can be found under <Atomic> elements.
//
// NOTE: in theory Atomics can have anything except Get verbs, but
// for the moment we're not allowing anything besides Replaces
ReplaceCommands []SyncMLCmd `xml:"Replace,omitempty"`
}
@ -1303,11 +1309,7 @@ func (cmd *SyncMLCmd) IsValid() bool {
// IsEmpty checks if there are not items in the command
func (cmd *SyncMLCmd) IsEmpty() bool {
if len(cmd.Items) == 0 {
return true
}
return false
return len(cmd.Items) == 0
}
// GetTargetURI returns the first protocol commands target URI from the items list

View file

@ -104,6 +104,7 @@ type MDMWindowsProfilePayload struct {
OperationType MDMOperationType `db:"operation_type"`
Detail string `db:"detail"`
CommandUUID string `db:"command_uuid"`
Retries int `db:"retries"`
}
type MDMWindowsBulkUpsertHostProfilePayload struct {

View file

@ -37,39 +37,10 @@ import (
// if the profile should be retried (in which case, a new install profile command will be enqueued by the server)
// or marked as "failed" and updates the datastore accordingly.
// maxRetries is the maximum times an install profile command may be retried, after which marked as failed and no further
// attempts will be made to install the profile.
const maxRetries = 1
// ProfileVerificationStore is the minimal interface required to get and update the verification
// status of a host's MDM profiles. The Fleet Datastore satisfies this interface.
type ProfileVerificationStore interface {
// GetHostMDMProfilesExpectedForVerification returns the expected MDM profiles for a given host. The map is
// keyed by the profile identifier.
GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error)
// GetHostMDMProfilesRetryCounts returns the retry counts for the specified host.
GetHostMDMProfilesRetryCounts(ctx context.Context, hostUUID string) ([]fleet.HostMDMProfileRetryCount, error)
// GetHostMDMProfileRetryCountByCommandUUID returns the retry count for the specified
// host UUID and command UUID.
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, hostUUID, commandUUID string) (fleet.HostMDMProfileRetryCount, error)
// UpdateHostMDMProfilesVerification updates status of macOS profiles installed on a given
// host. The toVerify, toFail, and toRetry slices contain the identifiers of the profiles that
// should be verified, failed, and retried, respectively. For each profile in the toRetry slice,
// the retries count is incremented by 1 and the status is set to null so that an install
// profile command is enqueued the next time the profile manager cron runs.
UpdateHostMDMProfilesVerification(ctx context.Context, hostUUID string, toVerify, toFail, toRetry []string) error
// UpdateOrDeleteHostMDMAppleProfile updates information about a single
// profile status. It deletes the row if the profile operation is "remove"
// and the status is "verifying" (i.e. successfully removed).
UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profile *fleet.HostMDMAppleProfile) error
}
var _ ProfileVerificationStore = (fleet.Datastore)(nil)
// VerifyHostMDMProfiles performs the verification of the MDM profiles installed on a host and
// updates the verification status in the datastore. It is intended to be called by Fleet osquery
// service when the Fleet server ingests host details.
func VerifyHostMDMProfiles(ctx context.Context, ds ProfileVerificationStore, host *fleet.Host, installed map[string]*fleet.HostMacOSProfile) error {
func VerifyHostMDMProfiles(ctx context.Context, ds fleet.ProfileVerificationStore, host *fleet.Host, installed map[string]*fleet.HostMacOSProfile) error {
expected, err := ds.GetHostMDMProfilesExpectedForVerification(ctx, host)
if err != nil {
return err
@ -100,7 +71,7 @@ func VerifyHostMDMProfiles(ctx context.Context, ds ProfileVerificationStore, hos
toFail := make([]string, 0, len(missing))
toRetry := make([]string, 0, len(missing))
if len(missing) > 0 {
counts, err := ds.GetHostMDMProfilesRetryCounts(ctx, host.UUID)
counts, err := ds.GetHostMDMProfilesRetryCounts(ctx, host)
if err != nil {
return err
}
@ -109,7 +80,7 @@ func VerifyHostMDMProfiles(ctx context.Context, ds ProfileVerificationStore, hos
retriesByProfileIdentifier[r.ProfileIdentifier] = r.Retries
}
for _, key := range missing {
if retriesByProfileIdentifier[key] < maxRetries {
if retriesByProfileIdentifier[key] < mdm.MaxProfileRetries {
// if we haven't hit the max retries, we set the host profile status to nil (which
// causes an install profile command to be enqueued the next time the profile
// manager cron runs) and increment the retry count
@ -121,24 +92,25 @@ func VerifyHostMDMProfiles(ctx context.Context, ds ProfileVerificationStore, hos
}
}
return ds.UpdateHostMDMProfilesVerification(ctx, host.UUID, verified, toFail, toRetry)
return ds.UpdateHostMDMProfilesVerification(ctx, host, verified, toFail, toRetry)
}
// HandleHostMDMProfileInstallResult ingests the result of an install profile command reported via
// the MDM protocol and updates the verification status in the datastore. It is intended to be
// called by the Fleet MDM checkin and command service install profile request handler.
func HandleHostMDMProfileInstallResult(ctx context.Context, ds ProfileVerificationStore, hostUUID string, cmdUUID string, status *fleet.MDMDeliveryStatus, detail string) error {
func HandleHostMDMProfileInstallResult(ctx context.Context, ds fleet.ProfileVerificationStore, hostUUID string, cmdUUID string, status *fleet.MDMDeliveryStatus, detail string) error {
host := &fleet.Host{UUID: hostUUID, Platform: "darwin"}
if status != nil && *status == fleet.MDMDeliveryFailed {
m, err := ds.GetHostMDMProfileRetryCountByCommandUUID(ctx, hostUUID, cmdUUID)
m, err := ds.GetHostMDMProfileRetryCountByCommandUUID(ctx, host, cmdUUID)
if err != nil {
return err
}
if m.Retries < maxRetries {
if m.Retries < mdm.MaxProfileRetries {
// if we haven't hit the max retries, we set the host profile status to nil (which
// causes an install profile command to be enqueued the next time the profile
// manager cron runs) and increment the retry count
return ds.UpdateHostMDMProfilesVerification(ctx, hostUUID, nil, nil, []string{m.ProfileIdentifier})
return ds.UpdateHostMDMProfilesVerification(ctx, host, nil, nil, []string{m.ProfileIdentifier})
}
}

View file

@ -9,6 +9,11 @@ import (
"go.mozilla.org/pkcs7"
)
// MaxProfileRetries is the maximum times an install profile command may be
// retried, after which marked as failed and no further attempts will be made
// to install the profile.
const MaxProfileRetries = 1
// DecryptBase64CMS decrypts a base64 encoded pkcs7-encrypted value using the
// provided certificate and private key.
func DecryptBase64CMS(p7Base64 string, cert *x509.Certificate, key crypto.PrivateKey) ([]byte, error) {

View file

@ -0,0 +1,162 @@
package microsoft_mdm
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"hash/fnv"
"io"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
)
// LoopHostMDMLocURIs loops all the <LocURI> values on all the profiles for a
// given host. It provides to the callback function:
//
// - An `ExpectedMDMProfile` that references the profile owning the LocURI
// - A hash that's unique for each profile/uri combination
// - The LocURI string
// - The data (if any) of the first <Item> element of the current LocURI
func LoopHostMDMLocURIs(
ctx context.Context,
ds fleet.ProfileVerificationStore,
host *fleet.Host,
fn func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string),
) error {
profileMap, err := ds.GetHostMDMProfilesExpectedForVerification(ctx, host)
if err != nil {
return fmt.Errorf("getting host profiles for verification: %w", err)
}
for _, expectedProf := range profileMap {
var prof fleet.SyncMLCmd
wrappedBytes := fmt.Sprintf("<Atomic>%s</Atomic>", expectedProf.RawProfile)
if err := xml.Unmarshal([]byte(wrappedBytes), &prof); err != nil {
return fmt.Errorf("unmarshalling profile %s: %w", expectedProf.Name, err)
}
for _, rc := range prof.ReplaceCommands {
locURI := rc.GetTargetURI()
data := rc.GetTargetData()
ref := HashLocURI(expectedProf.Name, locURI)
fn(expectedProf, ref, locURI, data)
}
}
return nil
}
// HashLocURI creates a unique, consistent hash for a given profileName +
// locURI combination.
//
// FIXME: the mdm_bridge table decodes CmdID as `int`,
// so we encode the reference as an int32.
func HashLocURI(profileName, locURI string) string {
hash := fnv.New32a()
hash.Write([]byte(profileName + locURI))
return fmt.Sprint(hash.Sum32())
}
// VerifyHostMDMProfiles performs the verification of the MDM profiles installed on a host and
// updates the verification status in the datastore. It is intended to be called by Fleet osquery
// service when the Fleet server ingests host details.
func VerifyHostMDMProfiles(ctx context.Context, ds fleet.ProfileVerificationStore, host *fleet.Host, rawSyncML []byte) error {
var syncML fleet.SyncML
decoder := xml.NewDecoder(bytes.NewReader(rawSyncML))
// the DLL used by the `mdm_bridge` extension sends the response with
// <?xml version="1.0" encoding="utf-16"?>, however if you use
// `charset.NewReaderLabel` it fails to unmarshal (!?) for now, I'm
// relying on this hack.
decoder.CharsetReader = func(encoding string, input io.Reader) (io.Reader, error) {
return input, nil
}
if err := decoder.Decode(&syncML); err != nil {
return fmt.Errorf("decoding provided syncML: %w", err)
}
// TODO: what if more than one profile has the same
// target uri but a different value? (product question)
refToStatus := map[string]string{}
refToResult := map[string]string{}
for _, r := range syncML.GetOrderedCmds() {
if r.Cmd.CmdRef == nil {
continue
}
ref := *r.Cmd.CmdRef
if r.Verb == fleet.CmdStatus && r.Cmd.Data != nil {
refToStatus[ref] = *r.Cmd.Data
}
if r.Verb == fleet.CmdResults {
refToResult[ref] = r.Cmd.GetTargetData()
}
}
missing := map[string]struct{}{}
verified := map[string]struct{}{}
err := LoopHostMDMLocURIs(ctx, ds, host, func(profile *fleet.ExpectedMDMProfile, ref, locURI, wantData string) {
// if we didn't get a status for a LocURI, mark the profile as
// missing.
gotStatus, ok := refToStatus[ref]
if !ok {
missing[profile.Name] = struct{}{}
}
// it's okay if we didn't get a result
gotResults := refToResult[ref]
// non-200 status don't have results. Consider it failed
// TODO: should we be more granular instead? eg: special case
// `4xx` responses? I'm sure there are edge cases we're not
// accounting for here, but it's unclear at this moment.
if !strings.HasPrefix(gotStatus, "2") || wantData != gotResults {
withinGracePeriod := profile.IsWithinGracePeriod(host.DetailUpdatedAt)
if !withinGracePeriod {
missing[profile.Name] = struct{}{}
}
return
}
verified[profile.Name] = struct{}{}
})
if err != nil {
return fmt.Errorf("looping host mdm LocURIs: %w", err)
}
toFail := make([]string, 0, len(missing))
toRetry := make([]string, 0, len(missing))
if len(missing) > 0 {
counts, err := ds.GetHostMDMProfilesRetryCounts(ctx, host)
if err != nil {
return fmt.Errorf("getting host profiles retry counts: %w", err)
}
retriesByProfileUUID := make(map[string]uint, len(counts))
for _, r := range counts {
retriesByProfileUUID[r.ProfileName] = r.Retries
}
for key := range missing {
// if the profile is in missing, we failed to validate at
// least one LocURI, delete it from the verified map
delete(verified, key)
if retriesByProfileUUID[key] < mdm.MaxProfileRetries {
// if we haven't hit the max retries, we set
// the host profile status to nil (which causes
// an install profile command to be enqueued
// the next time the profile manager cron runs)
// and increment the retry count
toRetry = append(toRetry, key)
continue
}
// otherwise we set the host profile status to failed
toFail = append(toFail, key)
}
}
i := 0
verifiedSlice := make([]string, len(verified))
for k := range verified {
verifiedSlice[i] = k
i++
}
return ds.UpdateHostMDMProfilesVerification(ctx, host, verifiedSlice, toFail, toRetry)
}

View file

@ -0,0 +1,309 @@
package microsoft_mdm
import (
"context"
"encoding/xml"
"io"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestLoopHostMDMLocURIs(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
ds.GetHostMDMProfilesExpectedForVerificationFunc = func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
return map[string]*fleet.ExpectedMDMProfile{
"N1": {Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{"L1": "D1"})},
"N2": {Name: "N2", RawProfile: syncml.ForTestWithData(map[string]string{"L2": "D2"})},
"N3": {Name: "N3", RawProfile: syncml.ForTestWithData(map[string]string{"L3": "D3", "L3.1": "D3.1"})},
}, nil
}
type wantStruct struct {
locURI string
data string
profileUUID string
uniqueHash string
}
got := []wantStruct{}
err := LoopHostMDMLocURIs(ctx, ds, &fleet.Host{}, func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string) {
got = append(got, wantStruct{
locURI: locURI,
data: data,
profileUUID: profile.Name,
uniqueHash: hash,
})
})
require.NoError(t, err)
require.ElementsMatch(
t,
[]wantStruct{
{"L1", "D1", "N1", "1255198959"},
{"L2", "D2", "N2", "2736786183"},
{"L3", "D3", "N3", "894211447"},
{"L3.1", "D3.1", "N3", "3410477854"},
},
got,
)
}
func TestHashLocURI(t *testing.T) {
testCases := []struct {
name string
profileName string
locURI string
expectNotEmpty bool
}{
{
name: "basic functionality",
profileName: "profile1",
locURI: "uri1",
expectNotEmpty: true,
},
{
name: "empty strings",
profileName: "",
locURI: "",
expectNotEmpty: true,
},
{
name: "special characters",
profileName: "profile!@#",
locURI: "uri$%^",
expectNotEmpty: true,
},
{
name: "long string input",
profileName: string(make([]rune, 1000)),
locURI: string(make([]rune, 1000)),
expectNotEmpty: true,
},
{
name: "non-ASCII characters",
profileName: "プロファイル",
locURI: "URI",
expectNotEmpty: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hash := HashLocURI(tc.profileName, tc.locURI)
if tc.expectNotEmpty {
require.NotEmpty(t, hash, "hash should not be empty")
}
})
}
}
func TestVerifyHostMDMProfilesErrors(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
host := &fleet.Host{}
err := VerifyHostMDMProfiles(ctx, ds, host, []byte{})
require.ErrorIs(t, err, io.EOF)
}
func TestVerifyHostMDMProfilesHappyPaths(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
host := &fleet.Host{
DetailUpdatedAt: time.Now(),
}
type osqueryReport struct {
Name string
Status string
LocURI string
Data string
}
type hostProfile struct {
Name string
RawContents []byte
RetryCount uint
}
cases := []struct {
name string
hostProfiles []hostProfile
report []osqueryReport
toVerify []string
toFail []string
toRetry []string
withinGracePeriod bool
}{
{
name: "profile reported, but host doesn't have any",
hostProfiles: nil,
report: []osqueryReport{{"N1", "200", "L1", "D1"}},
toVerify: []string{},
toFail: []string{},
toRetry: []string{},
},
{
name: "single profile reported and verified",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1"}), 0},
},
report: []osqueryReport{{"N1", "200", "L1", "D1"}},
toVerify: []string{"N1"},
toFail: []string{},
toRetry: []string{},
},
{
name: "Get succeeds but has missing data",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1"}), 0},
{"N2", syncml.ForTestWithData(map[string]string{"L2": "D2"}), 1},
{"N3", syncml.ForTestWithData(map[string]string{"L3": "D3"}), 0},
{"N4", syncml.ForTestWithData(map[string]string{"L4": "D4"}), 1},
},
report: []osqueryReport{
{"N1", "200", "L1", ""},
{"N2", "200", "L2", ""},
{"N3", "200", "L3", "D3"},
{"N4", "200", "L4", "D4"},
},
toVerify: []string{"N3", "N4"},
toFail: []string{"N2"},
toRetry: []string{"N1"},
},
{
name: "Get fails",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1"}), 0},
{"N2", syncml.ForTestWithData(map[string]string{"L2": "D2"}), 1},
{"N3", syncml.ForTestWithData(map[string]string{"L3": "D3"}), 0},
{"N4", syncml.ForTestWithData(map[string]string{"L4": "D4"}), 1},
},
report: []osqueryReport{
{"N1", "400", "L1", ""},
{"N2", "500", "L2", ""},
{"N3", "200", "L3", "D3"},
{"N4", "200", "L4", "D4"},
},
toVerify: []string{"N3", "N4"},
toFail: []string{"N2"},
toRetry: []string{"N1"},
},
{
name: "missing report",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1"}), 0},
{"N2", syncml.ForTestWithData(map[string]string{"L2": "D2"}), 1},
},
report: []osqueryReport{},
toVerify: []string{},
toFail: []string{"N2"},
toRetry: []string{"N1"},
},
{
name: "profiles with multiple locURIs",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1", "L1.1": "D1.1"}), 0},
{"N2", syncml.ForTestWithData(map[string]string{"L2": "D2", "L2.1": "D2.1"}), 1},
{"N3", syncml.ForTestWithData(map[string]string{"L3": "D3", "L3.1": "D3.1"}), 0},
{"N4", syncml.ForTestWithData(map[string]string{"L4": "D4", "L4.1": "D4.1"}), 1},
},
report: []osqueryReport{
{"N1", "400", "L1", ""},
{"N1", "200", "L1.1", "D1.1"},
{"N2", "500", "L2", ""},
{"N2", "200", "L2.1", "D2.1"},
{"N3", "200", "L3", "D3"},
{"N3", "200", "L3.1", "D3.1"},
{"N4", "200", "L4", "D4"},
},
toVerify: []string{"N3"},
toFail: []string{"N2", "N4"},
toRetry: []string{"N1"},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
var msg fleet.SyncML
msg.Xmlns = syncml.SyncCmdNamespace
msg.SyncHdr = fleet.SyncHdr{
VerDTD: syncml.SyncMLSupportedVersion,
VerProto: syncml.SyncMLVerProto,
SessionID: "2",
MsgID: "2",
}
for _, p := range tt.report {
ref := HashLocURI(p.Name, p.LocURI)
msg.AppendCommand(fleet.MDMRaw, fleet.SyncMLCmd{
XMLName: xml.Name{Local: fleet.CmdStatus},
CmdID: uuid.NewString(),
CmdRef: &ref,
Data: ptr.String(p.Status),
})
// the protocol can respond with only a `Status`
// command if the status failed
if p.Status != "200" || p.Data != "" {
msg.AppendCommand(fleet.MDMRaw, fleet.SyncMLCmd{
XMLName: xml.Name{Local: fleet.CmdResults},
CmdID: uuid.NewString(),
CmdRef: &ref,
Items: []fleet.CmdItem{
{Target: ptr.String(p.LocURI), Data: ptr.String(p.Data)},
},
})
}
}
ds.GetHostMDMProfilesExpectedForVerificationFunc = func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
installDate := host.DetailUpdatedAt.Add(-2 * time.Hour)
if tt.withinGracePeriod {
installDate = host.DetailUpdatedAt
}
out := map[string]*fleet.ExpectedMDMProfile{}
for _, profile := range tt.hostProfiles {
out[profile.Name] = &fleet.ExpectedMDMProfile{
Name: profile.Name,
RawProfile: profile.RawContents,
EarliestInstallDate: installDate,
}
}
return out, nil
}
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error {
require.ElementsMatch(t, tt.toVerify, toVerify, "profiles to verify don't match")
require.ElementsMatch(t, tt.toFail, toFail, "profiles to fail don't match")
require.ElementsMatch(t, tt.toRetry, toRetry, "profiles to retry don't match")
return nil
}
ds.GetHostMDMProfilesRetryCountsFunc = func(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error) {
out := []fleet.HostMDMProfileRetryCount{}
for _, profile := range tt.hostProfiles {
out = append(out, fleet.HostMDMProfileRetryCount{
ProfileName: profile.Name,
Retries: profile.RetryCount,
})
}
return out, nil
}
out, err := xml.Marshal(msg)
require.NoError(t, err)
require.NoError(t, VerifyHostMDMProfiles(ctx, ds, host, out))
require.True(t, ds.UpdateHostMDMProfilesVerificationFuncInvoked)
require.True(t, ds.GetHostMDMProfilesExpectedForVerificationFuncInvoked)
ds.UpdateHostMDMProfilesVerificationFuncInvoked = false
ds.GetHostMDMProfilesExpectedForVerificationFuncInvoked = false
})
}
}

View file

@ -1,5 +1,10 @@
package syncml
import (
"bytes"
"fmt"
)
// XML Namespaces and type URLs used by the Microsoft Device Enrollment v2 protocol (MS-MDE2)
const (
DiscoverNS = "http://schemas.microsoft.com/windows/management/2012/01/enrollment"
@ -330,3 +335,19 @@ const (
// SyncML ver protocol version
SyncMLVerProto = "DM/" + SyncMLSupportedVersion
)
func ForTestWithData(locURIs map[string]string) []byte {
var syncMLBuf bytes.Buffer
for locURI, data := range locURIs {
syncMLBuf.WriteString(fmt.Sprintf(`
<Replace>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
<Data>%s</Data>
</Item>
</Replace>`, locURI, data))
}
return syncMLBuf.Bytes()
}

View file

@ -502,13 +502,13 @@ type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet
type SetDiskEncryptionResetStatusFunc func(ctx context.Context, hostID uint, status bool) error
type UpdateHostMDMProfilesVerificationFunc func(ctx context.Context, hostUUID string, toVerify []string, toFail []string, toRetry []string) error
type UpdateHostMDMProfilesVerificationFunc func(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error
type GetHostMDMProfilesExpectedForVerificationFunc func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error)
type GetHostMDMProfilesRetryCountsFunc func(ctx context.Context, hostUUID string) ([]fleet.HostMDMProfileRetryCount, error)
type GetHostMDMProfilesRetryCountsFunc func(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error)
type GetHostMDMProfileRetryCountByCommandUUIDFunc func(ctx context.Context, hostUUID string, cmdUUID string) (fleet.HostMDMProfileRetryCount, error)
type GetHostMDMProfileRetryCountByCommandUUIDFunc func(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error)
type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string) error
@ -3556,11 +3556,11 @@ func (s *DataStore) SetDiskEncryptionResetStatus(ctx context.Context, hostID uin
return s.SetDiskEncryptionResetStatusFunc(ctx, hostID, status)
}
func (s *DataStore) UpdateHostMDMProfilesVerification(ctx context.Context, hostUUID string, toVerify []string, toFail []string, toRetry []string) error {
func (s *DataStore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error {
s.mu.Lock()
s.UpdateHostMDMProfilesVerificationFuncInvoked = true
s.mu.Unlock()
return s.UpdateHostMDMProfilesVerificationFunc(ctx, hostUUID, toVerify, toFail, toRetry)
return s.UpdateHostMDMProfilesVerificationFunc(ctx, host, toVerify, toFail, toRetry)
}
func (s *DataStore) GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
@ -3570,18 +3570,18 @@ func (s *DataStore) GetHostMDMProfilesExpectedForVerification(ctx context.Contex
return s.GetHostMDMProfilesExpectedForVerificationFunc(ctx, host)
}
func (s *DataStore) GetHostMDMProfilesRetryCounts(ctx context.Context, hostUUID string) ([]fleet.HostMDMProfileRetryCount, error) {
func (s *DataStore) GetHostMDMProfilesRetryCounts(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error) {
s.mu.Lock()
s.GetHostMDMProfilesRetryCountsFuncInvoked = true
s.mu.Unlock()
return s.GetHostMDMProfilesRetryCountsFunc(ctx, hostUUID)
return s.GetHostMDMProfilesRetryCountsFunc(ctx, host)
}
func (s *DataStore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, hostUUID string, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
func (s *DataStore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
s.mu.Lock()
s.GetHostMDMProfileRetryCountByCommandUUIDFuncInvoked = true
s.mu.Unlock()
return s.GetHostMDMProfileRetryCountByCommandUUIDFunc(ctx, hostUUID, cmdUUID)
return s.GetHostMDMProfileRetryCountByCommandUUIDFunc(ctx, host, cmdUUID)
}
func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {

View file

@ -2417,6 +2417,13 @@ func ReconcileAppleProfiles(
commander *apple_mdm.MDMAppleCommander,
logger kitlog.Logger,
) error {
appConfig, err := ds.AppConfig(ctx)
if err != nil {
return fmt.Errorf("reading app config: %w", err)
}
if !appConfig.MDM.EnabledAndConfigured {
return nil
}
if err := ensureFleetdConfig(ctx, ds, logger); err != nil {
logger.Log("err", "unable to ensure a fleetd configuration profiles are in place", "details", err)
}

View file

@ -1242,13 +1242,13 @@ func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) {
require.Equal(t, c.want, profile)
return nil
}
ds.GetHostMDMProfileRetryCountByCommandUUIDFunc = func(ctx context.Context, hstUUID, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
require.Equal(t, hostUUID, hstUUID)
ds.GetHostMDMProfileRetryCountByCommandUUIDFunc = func(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
require.Equal(t, hostUUID, host.UUID)
require.Equal(t, commandUUID, cmdUUID)
return fleet.HostMDMProfileRetryCount{ProfileIdentifier: profileIdentifier, Retries: c.prevRetries}, nil
}
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, hostUUID string, toVerify, toFail, toRetry []string) error {
require.Equal(t, hostUUID, hostUUID)
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
require.Equal(t, hostUUID, host.UUID)
require.Nil(t, toVerify)
require.Nil(t, toFail)
require.ElementsMatch(t, toRetry, []string{profileIdentifier})
@ -2199,6 +2199,7 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
appCfg := &fleet.AppConfig{}
appCfg.ServerSettings.ServerURL = "https://test.example.com"
appCfg.MDM.EnabledAndConfigured = true
return appCfg, nil
}

View file

@ -648,7 +648,7 @@ func (s *integrationMDMTestSuite) TestAppleProfileManagement() {
s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary) // host still verifying team profiles
}
func (s *integrationMDMTestSuite) TestProfileRetries() {
func (s *integrationMDMTestSuite) TestAppleProfileRetries() {
t := s.T()
ctx := context.Background()
@ -687,7 +687,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
mobileconfig.FleetdConfigPayloadIdentifier: 0,
}
checkRetryCounts := func(t *testing.T) {
counts, err := s.ds.GetHostMDMProfilesRetryCounts(ctx, h.UUID)
counts, err := s.ds.GetHostMDMProfilesRetryCounts(ctx, h)
require.NoError(t, err)
require.Len(t, counts, len(expectedRetryCounts))
for _, c := range counts {
@ -934,6 +934,270 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
})
}
func (s *integrationMDMTestSuite) TestWindowsProfileRetries() {
t := s.T()
ctx := context.Background()
testProfiles := map[string][]byte{
"N1": syncml.ForTestWithData(map[string]string{"L1": "D1"}),
"N2": syncml.ForTestWithData(map[string]string{"L2": "D2", "L3": "D3"}),
}
h, mdmDevice := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
expectedProfileStatuses := map[string]fleet.MDMDeliveryStatus{
"N1": fleet.MDMDeliveryVerifying,
"N2": fleet.MDMDeliveryVerifying,
}
checkProfilesStatus := func(t *testing.T) {
storedProfs, err := s.ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
require.NoError(t, err)
require.Len(t, storedProfs, len(expectedProfileStatuses))
for _, p := range storedProfs {
want, ok := expectedProfileStatuses[p.Name]
require.True(t, ok, "unexpected profile: %s", p.Name)
require.Equal(t, want, *p.Status, "expected status %s but got %s for profile: %s", want, *p.Status, p.Name)
}
}
expectedRetryCounts := map[string]uint{
"N1": 0,
"N2": 0,
}
checkRetryCounts := func(t *testing.T) {
counts, err := s.ds.GetHostMDMProfilesRetryCounts(ctx, h)
require.NoError(t, err)
require.Len(t, counts, len(expectedRetryCounts))
for _, c := range counts {
want, ok := expectedRetryCounts[c.ProfileName]
require.True(t, ok, "unexpected profile: %s", c.ProfileName)
require.Equal(t, want, c.Retries, "expected retry count %d but got %d for profile: %s", want, c.Retries, c.ProfileName)
}
}
type profileData struct {
Status string
LocURI string
Data string
}
hostProfileReports := map[string][]profileData{
"N1": {{"200", "L1", "D1"}},
"N2": {{"200", "L2", "D2"}, {"200", "L3", "D3"}},
}
reportHostProfs := func(t *testing.T, profileNames ...string) {
var responseOps []*mdm_types.SyncMLCmd
for _, profileName := range profileNames {
report, ok := hostProfileReports[profileName]
require.True(t, ok)
for _, p := range report {
ref := microsoft_mdm.HashLocURI(profileName, p.LocURI)
responseOps = append(responseOps, &mdm_types.SyncMLCmd{
XMLName: xml.Name{Local: mdm_types.CmdStatus},
CmdID: uuid.NewString(),
CmdRef: &ref,
Data: ptr.String(p.Status),
})
// the protocol can respond with only a `Status`
// command if the status failed
if p.Status != "200" || p.Data != "" {
responseOps = append(responseOps, &mdm_types.SyncMLCmd{
XMLName: xml.Name{Local: mdm_types.CmdResults},
CmdID: uuid.NewString(),
CmdRef: &ref,
Items: []mdm_types.CmdItem{
{Target: ptr.String(p.LocURI), Data: ptr.String(p.Data)},
},
})
}
}
}
msg, err := createSyncMLMessage("2", "2", "foo", "bar", responseOps)
require.NoError(t, err)
out, err := xml.Marshal(msg)
require.NoError(t, err)
require.NoError(t, microsoft_mdm.VerifyHostMDMProfiles(ctx, s.ds, h, out))
}
verifyCommands := func(wantProfileInstalls int, status string) {
s.awaitTriggerProfileSchedule(t)
cmds, err := mdmDevice.StartManagementSession()
require.NoError(t, err)
// profile installs + 2 protocol commands acks
require.Len(t, cmds, wantProfileInstalls+2)
msgID, err := mdmDevice.GetCurrentMsgID()
require.NoError(t, err)
atomicCmds := 0
for _, c := range cmds {
if c.Verb == "Atomic" {
atomicCmds++
}
mdmDevice.AppendResponse(fleet.SyncMLCmd{
XMLName: xml.Name{Local: mdm_types.CmdStatus},
MsgRef: &msgID,
CmdRef: ptr.String(c.Cmd.CmdID),
Cmd: ptr.String(c.Verb),
Data: ptr.String(status),
Items: nil,
CmdID: uuid.NewString(),
})
}
require.Equal(t, wantProfileInstalls, atomicCmds)
cmds, err = mdmDevice.SendResponse()
require.NoError(t, err)
// the ack of the message should be the only returned command
require.Len(t, cmds, 1)
}
t.Run("retry after verifying", func(t *testing.T) {
// upload test profiles then simulate expired grace period by setting updated_at timestamp of profiles back by 48 hours
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
// profiles to install + 2 boilerplate <Status>
verifyCommands(len(testProfiles), syncml.CmdStatusOK)
checkProfilesStatus(t) // all profiles verifying
checkRetryCounts(t) // no retries yet
// report osquery results with N2 missing and confirm N2 marked
// as verifying and other profiles are marked as verified
reportHostProfs(t, "N1")
expectedProfileStatuses["N2"] = fleet.MDMDeliveryPending
expectedProfileStatuses["N1"] = fleet.MDMDeliveryVerified
checkProfilesStatus(t)
expectedRetryCounts["N2"] = 1
checkRetryCounts(t)
// report osquery results with N2 present and confirm that all profiles are verified
verifyCommands(1, syncml.CmdStatusOK)
reportHostProfs(t, "N1", "N2")
expectedProfileStatuses["N2"] = fleet.MDMDeliveryVerified
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that no profiles were sent
verifyCommands(0, syncml.CmdStatusOK)
})
t.Run("retry after verification", func(t *testing.T) {
// report osquery results with N1 missing and confirm that the N1 marked as pending (initial retry)
reportHostProfs(t, "N2")
expectedProfileStatuses["N1"] = fleet.MDMDeliveryPending
checkProfilesStatus(t)
expectedRetryCounts["N1"] = 1
checkRetryCounts(t)
// trigger a profile sync and confirm that the install profile command for N1 was resent
verifyCommands(1, syncml.CmdStatusOK)
// report osquery results with N1 missing again and confirm that the N1 marked as failed (max retries exceeded)
reportHostProfs(t, "N2")
expectedProfileStatuses["N1"] = fleet.MDMDeliveryFailed
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile command for N1 was not resent
verifyCommands(0, syncml.CmdStatusOK)
})
t.Run("retry after device error", func(t *testing.T) {
// add another profile
newProfile := syncml.ForTestWithData(map[string]string{"L3": "D3"})
testProfiles["N3"] = newProfile
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
// trigger a profile sync and confirm that the install profile command for N3 was sent and
// simulate a device error
verifyCommands(1, syncml.CmdStatusAtomicFailed)
expectedProfileStatuses["N3"] = fleet.MDMDeliveryPending
checkProfilesStatus(t)
expectedRetryCounts["N3"] = 1
checkRetryCounts(t)
// trigger a profile sync and confirm that the install profile command for N3 was sent and
// simulate a device ack
verifyCommands(1, syncml.CmdStatusOK)
expectedProfileStatuses["N3"] = fleet.MDMDeliveryVerifying
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// report osquery results with N3 missing and confirm that the N3 marked as failed (max
// retries exceeded)
reportHostProfs(t, "N2")
expectedProfileStatuses["N3"] = fleet.MDMDeliveryFailed
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile command for N3 was not resent
verifyCommands(0, syncml.CmdStatusOK)
})
t.Run("repeated device error", func(t *testing.T) {
// add another profile
testProfiles["N4"] = syncml.ForTestWithData(map[string]string{"L4": "D4"})
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
// trigger a profile sync and confirm that the install profile command for N4 was sent and
// simulate a device error
verifyCommands(1, syncml.CmdStatusAtomicFailed)
expectedProfileStatuses["N4"] = fleet.MDMDeliveryPending
checkProfilesStatus(t)
expectedRetryCounts["N4"] = 1
checkRetryCounts(t)
// trigger a profile sync and confirm that the install profile
// command for N4 was sent and simulate a second device error
verifyCommands(1, syncml.CmdStatusAtomicFailed)
expectedProfileStatuses["N4"] = fleet.MDMDeliveryFailed
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile
// command for N4 was not resent
verifyCommands(0, syncml.CmdStatusOK)
})
t.Run("retry count does not reset", func(t *testing.T) {
// add another profile
testProfiles["N5"] = syncml.ForTestWithData(map[string]string{"L5": "D5"})
//hostProfsByIdent["N5"] = &fleet.HostMacOSProfile{Identifier: "N5", DisplayName: "N5", InstallDate: time.Now()}
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
// trigger a profile sync and confirm that the install profile
// command for N5 was sent and simulate a device error
verifyCommands(1, syncml.CmdStatusAtomicFailed)
expectedProfileStatuses["N5"] = fleet.MDMDeliveryPending
checkProfilesStatus(t)
expectedRetryCounts["N5"] = 1
checkRetryCounts(t)
// trigger a profile sync and confirm that the install profile
// command for N5 was sent and simulate a device ack
verifyCommands(1, syncml.CmdStatusOK)
expectedProfileStatuses["N5"] = fleet.MDMDeliveryVerifying
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// report osquery results with N5 found and confirm that the N5 marked as verified
hostProfileReports["N5"] = []profileData{{"200", "L5", "D5"}}
reportHostProfs(t, "N2", "N5")
expectedProfileStatuses["N5"] = fleet.MDMDeliveryVerified
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile command for N5 was not resent
verifyCommands(0, syncml.CmdStatusOK)
// report osquery results again, this time N5 is missing and confirm that the N5 marked as
// failed (max retries exceeded)
reportHostProfs(t, "N2")
expectedProfileStatuses["N5"] = fleet.MDMDeliveryFailed
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile command for N5 was not resent
verifyCommands(0, syncml.CmdStatusOK)
})
}
func checkNextPayloads(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, forceDeviceErr bool) ([][]byte, []string) {
var cmd *micromdm.CommandPayload
var err error
@ -3866,6 +4130,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h, err := s.ds.LoadHostByOrbitNodeKey(ctx, orbitNodeKey)
require.NoError(t, err)
h.OrbitNodeKey = &orbitNodeKey
h.Platform = "darwin"
err = mdmDevice.Enroll()
require.NoError(t, err)
@ -9445,12 +9710,24 @@ func (s *integrationMDMTestSuite) TestWindowsProfileManagement() {
verifyHostProfileStatus := func(cmds []fleet.ProtoCmdOperation, wantStatus string) {
for _, cmd := range cmds {
var gotStatus string
var gotProfile struct {
Status string `db:"status"`
Retries int `db:"retries"`
}
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
stmt := `SELECT status FROM host_mdm_windows_profiles WHERE command_uuid = ?`
return sqlx.GetContext(context.Background(), q, &gotStatus, stmt, cmd.Cmd.CmdID)
stmt := `
SELECT COALESCE(status, 'pending') as status, retries
FROM host_mdm_windows_profiles
WHERE command_uuid = ?`
return sqlx.GetContext(context.Background(), q, &gotProfile, stmt, cmd.Cmd.CmdID)
})
require.EqualValues(t, fleet.WindowsResponseToDeliveryStatus(wantStatus), gotStatus, "command_uuid", cmd.Cmd.CmdID)
wantDeliveryStatus := fleet.WindowsResponseToDeliveryStatus(wantStatus)
if gotProfile.Retries <= servermdm.MaxProfileRetries && wantDeliveryStatus == mdm_types.MDMDeliveryFailed {
require.EqualValues(t, "pending", gotProfile.Status, "command_uuid", cmd.Cmd.CmdID)
} else {
require.EqualValues(t, wantDeliveryStatus, gotProfile.Status, "command_uuid", cmd.Cmd.CmdID)
}
}
}
@ -9584,6 +9861,10 @@ func (s *integrationMDMTestSuite) TestWindowsProfileManagement() {
// check that we deleted the old profile in the DB
checkHostsProfilesMatch(host, teamProfiles)
// a second sync gets the profile again, because of delivery retries.
// Succeed that one
verifyProfiles(mdmDevice, 1, false)
// another sync shouldn't return profiles
verifyProfiles(mdmDevice, 0, false)
}

View file

@ -394,11 +394,11 @@ func TestBuildCommandFromProfileBytes(t *testing.T) {
func syncMLForTest(locURI string) []byte {
return []byte(fmt.Sprintf(`
<Replace>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
</Item>
</Replace>`, locURI))
<Replace>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
</Item>
</Replace>`, locURI))
}

View file

@ -709,6 +709,9 @@ func (svc *Service) detailQueriesForHost(ctx context.Context, host *fleet.Host)
if query.RunsForPlatform(host.Platform) {
queryName := hostDetailQueryPrefix + name
queries[queryName] = query.Query
if query.QueryFunc != nil && query.Query == "" {
queries[queryName] = query.QueryFunc(ctx, svc.logger, host, svc.ds)
}
discoveryQuery := query.Discovery
if discoveryQuery == "" {
discoveryQuery = alwaysTrueQuery

View file

@ -19,6 +19,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/publicip"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async"
"github.com/go-kit/kit/log"
@ -29,6 +30,8 @@ import (
type DetailQuery struct {
// Query is the SQL query string.
Query string
// QueryFunc is optionally used to dynamically build a query.
QueryFunc func(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore) string
// Discovery is the SQL query that defines whether the query will run on the host or not.
// If not set, Fleet makes sure the query will always run.
Discovery string
@ -586,6 +589,12 @@ var mdmQueries = map[string]DetailQuery{
DirectIngestFunc: directIngestMacOSProfiles,
Discovery: discoveryTable("macos_profiles"),
},
"mdm_config_profiles_windows": {
QueryFunc: buildConfigProfilesWindowsQuery,
Platforms: []string{"windows"},
DirectIngestFunc: directIngestWindowsProfiles,
Discovery: discoveryTable("mdm_bridge"),
},
// There are two mutually-exclusive queries used to read the FileVaultPRK depending on which
// extension tables are discovered on the agent. The preferred query uses the newer custom
// `filevault_prk` extension table rather than the macadmins `file_lines` table. It is preferred
@ -1677,7 +1686,7 @@ func GetDetailQueries(
generatedMap["scheduled_query_stats"] = scheduledQueryStats
}
if appConfig != nil && appConfig.MDM.EnabledAndConfigured {
if appConfig != nil && (appConfig.MDM.EnabledAndConfigured || appConfig.MDM.WindowsEnabledAndConfigured) {
for key, query := range mdmQueries {
if slices.Equal(query.Platforms, []string{"windows"}) && !appConfig.MDM.WindowsEnabledAndConfigured {
continue
@ -1722,3 +1731,69 @@ func splitCleanSemicolonSeparated(s string) []string {
}
return cleaned
}
func buildConfigProfilesWindowsQuery(
ctx context.Context,
logger log.Logger,
host *fleet.Host,
ds fleet.Datastore,
) string {
var sb strings.Builder
sb.WriteString("<SyncBody>")
gotProfiles := false
err := microsoft_mdm.LoopHostMDMLocURIs(ctx, ds, host, func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string) {
// Per the [docs][1], to `<Get>` configurations you must
// replace `/Policy/Config` with `Policy/Result`
// [1]: https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-configuration-service-provider
locURI = strings.Replace(locURI, "/Policy/Config", "/Policy/Result", 1)
sb.WriteString(
// NOTE: intentionally building the xml as a one-liner
// to prevent any errors in the query.
fmt.Sprintf(
"<Get><CmdID>%s</CmdID><Item><Target><LocURI>%s</LocURI></Target></Item></Get>",
hash,
locURI,
))
gotProfiles = true
})
if err != nil {
logger.Log(
"component", "service",
"method", "QueryFunc - windows config profiles",
"err", err,
)
return ""
}
if !gotProfiles {
logger.Log(
"component", "service",
"method", "QueryFunc - windows config profiles",
"info", "host doesn't have profiles to check",
)
return ""
}
sb.WriteString("</SyncBody>")
return fmt.Sprintf("SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input = '%s';", sb.String())
}
func directIngestWindowsProfiles(
ctx context.Context,
logger log.Logger,
host *fleet.Host,
ds fleet.Datastore,
rows []map[string]string,
) error {
if len(rows) == 0 {
return nil
}
if len(rows) > 1 {
return ctxerr.Errorf(ctx, "directIngestWindowsProfiles invalid number of rows: %d", len(rows))
}
rawResponse := []byte(rows[0]["raw_mdm_command_output"])
if len(rawResponse) == 0 {
return ctxerr.Errorf(ctx, "directIngestWindowsProfiles host %s got an empty SyncML response", host.UUID)
}
return microsoft_mdm.VerifyHostMDMProfiles(ctx, ds, host, rawResponse)
}

View file

@ -7,8 +7,10 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"regexp"
"slices"
"sort"
"strings"
@ -19,6 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async"
@ -1292,8 +1295,8 @@ func TestDirectIngestHostMacOSProfiles(t *testing.T) {
}
return expected, nil
}
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, hostUUID string, toVerify, toFailed, toRetry []string) error {
require.Equal(t, h.UUID, hostUUID)
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, host *fleet.Host, toVerify, toFailed, toRetry []string) error {
require.Equal(t, h.UUID, host.UUID)
require.Equal(t, len(installedProfiles), len(toVerify))
require.Len(t, toFailed, 0)
require.Len(t, toRetry, 0)
@ -1537,3 +1540,66 @@ func TestSanitizeSoftware(t *testing.T) {
})
}
}
func TestDirectIngestWindowsProfiles(t *testing.T) {
ctx := context.Background()
logger := log.NewNopLogger()
ds := new(mock.Store)
for _, tc := range []struct {
hostProfiles []*fleet.ExpectedMDMProfile
want string
}{
{nil, ""},
{
[]*fleet.ExpectedMDMProfile{
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{})},
},
"",
},
{
[]*fleet.ExpectedMDMProfile{
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{"L1": "D1"})},
},
"SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input = '<SyncBody><Get><CmdID>1255198959</CmdID><Item><Target><LocURI>L1</LocURI></Target></Item></Get></SyncBody>';",
},
{
[]*fleet.ExpectedMDMProfile{
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{"L1": "D1"})},
{Name: "N2", RawProfile: syncml.ForTestWithData(map[string]string{"L2": "D2"})},
{Name: "N3", RawProfile: syncml.ForTestWithData(map[string]string{"L3": "D3", "L3.1": "D3.1"})},
},
"SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input = '<SyncBody><Get><CmdID>1255198959</CmdID><Item><Target><LocURI>L1</LocURI></Target></Item></Get><Get><CmdID>2736786183</CmdID><Item><Target><LocURI>L2</LocURI></Target></Item></Get><Get><CmdID>894211447</CmdID><Item><Target><LocURI>L3</LocURI></Target></Item></Get><Get><CmdID>3410477854</CmdID><Item><Target><LocURI>L3.1</LocURI></Target></Item></Get></SyncBody>';",
},
} {
ds.GetHostMDMProfilesExpectedForVerificationFunc = func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
result := map[string]*fleet.ExpectedMDMProfile{}
for _, p := range tc.hostProfiles {
result[p.Name] = p
}
return result, nil
}
gotQuery := buildConfigProfilesWindowsQuery(ctx, logger, &fleet.Host{}, ds)
if tc.want != "" {
require.Contains(t, gotQuery, "SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input =")
re := regexp.MustCompile(`'<(.*?)>'`)
gotMatches := re.FindStringSubmatch(gotQuery)
require.NotEmpty(t, gotMatches)
wantMatches := re.FindStringSubmatch(tc.want)
require.NotEmpty(t, wantMatches)
var extractedStruct, expectedStruct fleet.SyncBody
err := xml.Unmarshal([]byte(gotMatches[0]), &extractedStruct)
require.NoError(t, err)
err = xml.Unmarshal([]byte(wantMatches[0]), &expectedStruct)
require.NoError(t, err)
require.ElementsMatch(t, expectedStruct.Get, extractedStruct.Get)
} else {
require.Equal(t, gotQuery, tc.want)
}
}
}