mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
implement profile verification for windows
This commit is contained in:
parent
d7c233d54c
commit
7531a0742b
23 changed files with 1349 additions and 293 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(), ","))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
162
server/mdm/microsoft/profile_verifier.go
Normal file
162
server/mdm/microsoft/profile_verifier.go
Normal 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)
|
||||
}
|
||||
309
server/mdm/microsoft/profile_verifier_test.go
Normal file
309
server/mdm/microsoft/profile_verifier_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue