Fix Windows CSP bypass issue (#48843)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #48752 

Stacked PR. Needs 48349-windows-modify branch to merge first.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed a Windows MDM loophole where scope-less or differently formatted
`LocURI` values could bypass Fleet restrictions.
* Strengthened detection and enforcement for reserved Windows targets,
including OS updates, remote wipe premium gating, and BitLocker
restrictions.
* Improved `LocURI` handling to be resilient to whitespace and alternate
formatting, including more consistent SCEP profile processing.

* **Tests**
* Added regression coverage for reserved `LocURI` matching, OS-update
targeting, and premium detection for wipe commands (including scope-less
cases).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-07-08 14:38:10 +01:00
committed by GitHub
parent b6e0cac4ae
commit 19aac451e1
14 changed files with 249 additions and 32 deletions
@@ -0,0 +1 @@
- Fixed a bug where a custom Windows configuration profile/command could bypass Fleet's checks by using a scope-less LocURI.
+14 -1
View File
@@ -1328,7 +1328,7 @@ func validateOSUpdatesProfileConflict(controls GitOpsControls) error {
if windowsConfigured {
windowsSettings, _ := controls.WindowsSettings.(fleet.WindowsSettings)
for _, profile := range windowsSettings.CustomSettings.Value {
contains, err := profileFileContains(profile.Path, syncml.FleetOSUpdateTargetLocURI)
contains, err := windowsProfileFileTargetsReservedLocURI(profile.Path, syncml.FleetOSUpdateTargetLocURI)
if err != nil {
return err
}
@@ -1376,6 +1376,19 @@ func profileFileContains(path, needle string) (bool, error) {
return bytes.Contains(fileBytes, []byte(needle)), nil
}
// windowsProfileFileTargetsReservedLocURI reports whether the Windows profile file at path targets the given Fleet-reserved
// LocURI node.
func windowsProfileFileTargetsReservedLocURI(path, reservedLocURI string) (bool, error) {
if path == "" {
return false, nil
}
fileBytes, err := os.ReadFile(path)
if err != nil {
return false, fmt.Errorf("failed to read profile file %s: %v", path, err)
}
return fleet.ProfileTargetsReservedLocURI(fileBytes, reservedLocURI), nil
}
func processControlsPathIfNeeded(controlsTop GitOpsControls, result *GitOps, controlsFilePath *string) []error {
if controlsTop.Path == nil {
result.Controls = controlsTop
+1 -2
View File
@@ -1,7 +1,6 @@
package mysql
import (
"bytes"
"context"
"database/sql"
"errors"
@@ -635,7 +634,7 @@ func batchTrackUpdateConfigProfilesDB(ctx context.Context, tx sqlx.ExtContext, t
}
for _, p := range winProfiles {
if !bytes.Contains(p.SyncML, []byte(syncml.FleetOSUpdateTargetLocURI)) {
if !fleet.ProfileTargetsReservedLocURI(p.SyncML, syncml.FleetOSUpdateTargetLocURI) {
continue
}
var profileUUID string
+3 -4
View File
@@ -1086,9 +1086,8 @@ func (ds *Datastore) MDMWindowsSaveResponse(ctx context.Context, enrolledDevice
args = append(args, enrolledDevice.ID, cmd.CommandUUID, rawResult, responseID, statusCode)
sb.WriteString("(?, ?, ?, ?, ?),")
// if the command is a Wipe, keep track of it so we can update
// host_mdm_actions accordingly.
if strings.Contains(cmd.TargetLocURI, "/Device/Vendor/MSFT/RemoteWipe/") {
// if the command is a Wipe, keep track of it so we can update host_mdm_actions accordingly.
if fleet.LocURITargetsReservedNode(cmd.TargetLocURI, syncml.FleetRemoteWipeTargetLocURI) {
wipeCmdUUID = cmd.CommandUUID
wipeCmdStatus = statusCode
}
@@ -2402,7 +2401,7 @@ INSERT INTO
// An OS-update profile is tracked as the team's OS-update profile within
// this transaction so it rolls back together on failure.
if bytes.Contains(cp.SyncML, []byte(syncml.FleetOSUpdateTargetLocURI)) {
if fleet.ProfileTargetsReservedLocURI(cp.SyncML, syncml.FleetOSUpdateTargetLocURI) {
if err := trackWindowsUpdateConfigProfileDB(ctx, tx, teamID, profileUUID); err != nil {
return err
}
+41 -9
View File
@@ -17,7 +17,8 @@ import (
)
const (
WINDOWS_SCEP_LOC_URI_PART = "/Vendor/MSFT/ClientCertificateInstall/SCEP"
// scepInstallLocURINode is the Windows SCEP ClientCertificateInstall node in scope-less form.
scepInstallLocURINode = "Vendor/MSFT/ClientCertificateInstall/SCEP"
WindowsMDMAuthNoncePrefix = "mwenonce:"
)
@@ -1161,7 +1162,9 @@ const WindowsMDMRequiresPremiumCmdMessage = "Missing or invalid license. Wipe co
func (cmd SyncMLCmd) IsPremium() bool {
// NOTE: if this implementation changes, make sure to also update the error
// message above - the WindowsMDMRequiresPremiumCmdMessage constant.
return strings.Contains(cmd.GetTargetURI(), "/Device/Vendor/MSFT/RemoteWipe/")
//
// LocURITargetsReservedNode canonicalizes the target so the premium gate matches every LocURI form Windows accepts.
return LocURITargetsReservedNode(cmd.GetTargetURI(), syncml.FleetRemoteWipeTargetLocURI)
}
// DataType returns the SyncMLDataType corresponding to the command's format.
@@ -1829,9 +1832,7 @@ func BuildDeleteCommandFromProfileBytes(profileBytes []byte, commandUUID string,
normalized := FleetVarSCEPWindowsCertificateIDRegexp.ReplaceAll(profileBytes, []byte(profileUUID))
// Mirror the install-side behavior: SCEP profiles are wrapped in <Atomic> if not already.
if strings.Contains(string(normalized), WINDOWS_SCEP_LOC_URI_PART) && !strings.Contains(string(normalized), "<Atomic>") {
normalized = fmt.Appendf([]byte{}, "<Atomic>%s</Atomic>", normalized)
}
normalized = WrapSCEPProfileInAtomic(normalized)
allURIs := ExtractLocURIsFromProfileBytes(normalized)
if len(allURIs) == 0 {
@@ -1922,16 +1923,22 @@ func CanonicalLocURI(locURI string) string {
return s
}
// WrapSCEPProfileInAtomic wraps profileBytes in <Atomic> when the profile targets the Windows SCEP ClientCertificateInstall
// node and isn't already wrapped.
func WrapSCEPProfileInAtomic(profileBytes []byte) []byte {
if bytes.Contains(profileBytes, []byte(scepInstallLocURINode)) && !bytes.Contains(profileBytes, []byte("<Atomic>")) {
return fmt.Appendf([]byte{}, "<Atomic>%s</Atomic>", profileBytes)
}
return profileBytes
}
// ExtractLocURIsFromProfileBytes returns all Target LocURIs found in the
// profile's Replace and Add commands. Exec commands are excluded (they
// trigger one-time actions, not persistent settings). For Atomic profiles,
// nested commands are inspected.
func ExtractLocURIsFromProfileBytes(profileBytes []byte) []string {
// Mirror the install-side SCEP normalization.
normalized := profileBytes
if strings.Contains(string(normalized), WINDOWS_SCEP_LOC_URI_PART) && !strings.Contains(string(normalized), "<Atomic>") {
normalized = fmt.Appendf([]byte{}, "<Atomic>%s</Atomic>", normalized)
}
normalized := WrapSCEPProfileInAtomic(profileBytes)
cmds, err := UnmarshallMultiTopLevelXMLProfile(normalized)
if err != nil || len(cmds) == 0 {
@@ -1962,3 +1969,28 @@ func ExtractLocURIsFromProfileBytes(profileBytes []byte) []string {
}
return uris
}
// LocURITargetsReservedNode reports whether locURI targets the given Fleet-reserved node, or any descendant of it, matching
// at path-segment boundaries.
func LocURITargetsReservedNode(locURI, reservedLocURI string) bool {
node := strings.Trim(reservedLocURI, "/")
// Frame both the canonicalized LocURI and the node with "/" so the substring match is boundary-safe on both ends.
return strings.Contains("/"+CanonicalLocURI(locURI)+"/", "/"+node+"/")
}
// ProfileTargetsReservedLocURI reports whether any Target LocURI in the profile targets the given Fleet-reserved node.
func ProfileTargetsReservedLocURI(profileBytes []byte, reservedLocURI string) bool {
// Quick reject without parsing: the reserved node name (minus the "/" anchors) must appear literally somewhere for any
// LocURI form, scoped or scope-less, to target it. Most Windows profiles don't reference it, so this avoids the XML parse
// below for the common case (this helper runs in loops over every profile during profile set/update flows).
if !bytes.Contains(profileBytes, []byte(strings.Trim(reservedLocURI, "/"))) {
return false
}
// Confirm a real Add/Replace Target LocURI targets the node
for _, uri := range ExtractLocURIsFromProfileBytes(profileBytes) {
if LocURITargetsReservedNode(uri, reservedLocURI) {
return true
}
}
return false
}
+107
View File
@@ -842,6 +842,113 @@ func TestCanonicalLocURI(t *testing.T) {
require.Equal(t, "DeviceLock/X", CanonicalLocURI("./DeviceLock/X"))
}
func TestLocURITargetsReservedNode(t *testing.T) {
t.Parallel()
tests := []struct {
name string
locURI string
reserved string
want bool
}{
{name: "explicit device scope", locURI: "./Device/Vendor/MSFT/BitLocker/RequireDeviceEncryption", reserved: syncml.FleetBitLockerTargetLocURI, want: true},
{name: "scope-less (regression #48752)", locURI: "Vendor/MSFT/BitLocker/RequireDeviceEncryption", reserved: syncml.FleetBitLockerTargetLocURI, want: true},
{name: "user scope matches via Contains, not a prefix check", locURI: "./User/Vendor/MSFT/BitLocker/Foo", reserved: syncml.FleetBitLockerTargetLocURI, want: true},
{name: "surrounding whitespace", locURI: " Vendor/MSFT/BitLocker/Foo ", reserved: syncml.FleetBitLockerTargetLocURI, want: true},
// Boundary safety: a longer sibling segment that merely shares the reserved-node prefix must not match, on either end.
{name: "left boundary: node ending in Vendor is not reserved", locURI: "Custom/SomeVendor/MSFT/BitLocker/Foo", reserved: syncml.FleetBitLockerTargetLocURI, want: false},
{name: "right boundary: BitLockerCustom sibling is not reserved", locURI: "Vendor/MSFT/BitLockerCustom/Foo", reserved: syncml.FleetBitLockerTargetLocURI, want: false},
{name: "unrelated node", locURI: "./Device/Vendor/MSFT/DMClient/Foo", reserved: syncml.FleetBitLockerTargetLocURI, want: false},
// The reserved node itself (no descendant leaf) matches (node-inclusive).
{name: "bare BitLocker node matches", locURI: "./Device/Vendor/MSFT/BitLocker", reserved: syncml.FleetBitLockerTargetLocURI, want: true},
// One positive smoke per reserved constant so a future typo/rename is caught.
{name: "OS update", locURI: "Vendor/MSFT/Policy/Config/Update/AllowAutoUpdate", reserved: syncml.FleetOSUpdateTargetLocURI, want: true},
{name: "RemoteWipe operation", locURI: "./Device/Vendor/MSFT/RemoteWipe/doWipe", reserved: syncml.FleetRemoteWipeTargetLocURI, want: true},
// RemoteWipe is a wipe-only subtree: the bare node matches (node-inclusive), a sibling does not (boundary).
{name: "bare RemoteWipe node matches (wipe-only subtree)", locURI: "Vendor/MSFT/RemoteWipe", reserved: syncml.FleetRemoteWipeTargetLocURI, want: true},
{name: "RemoteWipeCustom sibling is not reserved", locURI: "Vendor/MSFT/RemoteWipeCustom/doWipe", reserved: syncml.FleetRemoteWipeTargetLocURI, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, LocURITargetsReservedNode(tt.locURI, tt.reserved))
})
}
}
func TestSyncMLCmdIsPremium(t *testing.T) {
t.Parallel()
newExecCmd := func(locURI string) SyncMLCmd {
return SyncMLCmd{
XMLName: xml.Name{Local: "Exec"},
Items: []CmdItem{{Target: new(locURI)}},
}
}
tests := []struct {
name string
locURI string
want bool
}{
{name: "explicit device wipe", locURI: "./Device/Vendor/MSFT/RemoteWipe/doWipe", want: true},
{name: "scope-less wipe (regression #48752)", locURI: "Vendor/MSFT/RemoteWipe/doWipe", want: true},
{name: "non-wipe command", locURI: "./DevDetail/SwV", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, newExecCmd(tt.locURI).IsPremium())
})
}
}
func TestProfileTargetsReservedLocURI(t *testing.T) {
t.Parallel()
osUpdate := syncml.FleetOSUpdateTargetLocURI
tests := []struct {
name string
syncML string
want bool
}{
{
name: "scoped OS update profile (fast path)",
syncML: `<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/Policy/Config/Update/AllowAutoUpdate</LocURI></Target></Item></Replace>`,
want: true,
},
{
name: "scope-less OS update profile (regression #48752)",
syncML: `<Replace><Item><Target><LocURI>Vendor/MSFT/Policy/Config/Update/AllowAutoUpdate</LocURI></Target></Item></Replace>`,
want: true,
},
{
name: "scope-less OS update inside Atomic",
syncML: `<Atomic><Replace><Item><Target><LocURI>Vendor/MSFT/Policy/Config/Update/AllowAutoUpdate</LocURI></Target></Item></Replace></Atomic>`,
want: true,
},
{
name: "non-OS-update profile",
syncML: `<Replace><Item><Target><LocURI>Vendor/MSFT/BitLocker/RequireDeviceEncryption</LocURI></Target></Item></Replace>`,
want: false,
},
{
name: "node ending in Update is not reserved",
syncML: `<Replace><Item><Target><LocURI>Custom/Config/UpdatePolicy/AllowAutoUpdate</LocURI></Target></Item></Replace>`,
want: false,
},
{
// Sibling segment sharing the reserved prefix must not be flagged (mentions the node name so the quick-reject
// filter passes, forcing the boundary-aware per-LocURI check to make the call).
name: "UpdateExtra sibling segment is not reserved",
syncML: `<Replace><Item><Target><LocURI>Vendor/MSFT/Policy/Config/UpdateExtra/AllowAutoUpdate</LocURI></Target></Item></Replace>`,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, ProfileTargetsReservedLocURI([]byte(tt.syncML), osUpdate))
})
}
}
func TestIsFleetInternalCmdID(t *testing.T) {
for _, tc := range []struct {
name string
+19 -4
View File
@@ -304,9 +304,8 @@ var fleetProvidedLocURIValidationMap = map[string][]string{
}
func validateFleetProvidedLocURI(locURI string, allowCustomDiskEncryption bool) error {
sanitizedLocURI := strings.TrimSpace(locURI)
for fleetLocURI, errHints := range fleetProvidedLocURIValidationMap {
if strings.Contains(sanitizedLocURI, fleetLocURI) {
if LocURITargetsReservedNode(locURI, fleetLocURI) {
if fleetLocURI == syncml.FleetBitLockerTargetLocURI {
if allowCustomDiskEncryption {
continue
@@ -406,9 +405,9 @@ func newWindowsSCEPProfileValidator() *windowsSCEPProfileValidator {
}
func (v windowsSCEPProfileValidator) normalizeSCEPLocURI(locURI string) string {
trimmed := strings.TrimSpace(locURI)
normalized := canonicalizeSCEPScope(locURI)
// Accept braces version of the Fleet Var, and normalize it to the non-braces for validation.
return strings.ReplaceAll(trimmed, FleetVarSCEPWindowsCertificateID.WithBraces(), FleetVarSCEPWindowsCertificateID.WithPrefix())
return strings.ReplaceAll(normalized, FleetVarSCEPWindowsCertificateID.WithBraces(), FleetVarSCEPWindowsCertificateID.WithPrefix())
}
func (v *windowsSCEPProfileValidator) isSCEPProfile() bool {
@@ -508,6 +507,22 @@ func IsWindowsSCEPLocURI(locURI string) bool {
strings.HasPrefix(locURI, "./User/Vendor/MSFT/ClientCertificateInstall/SCEP/")
}
// canonicalizeSCEPScope rewrites a SCEP ClientCertificateInstall LocURI to its explicit scoped form so the SCEP validations
// (which key off the "./Device/"/"./User/" prefix) can't be bypassed by a scope-less spelling. Non-SCEP LocURIs are returned unchanged.
func canonicalizeSCEPScope(locURI string) string {
// CanonicalLocURI strips the device scope to the bare "Vendor/MSFT/..." form and preserves explicit user scope as
// "User/Vendor/MSFT/...".
canon := CanonicalLocURI(locURI)
switch {
case strings.HasPrefix(canon, scepInstallLocURINode+"/"):
return "./Device/" + canon
case strings.HasPrefix(canon, "User/"+scepInstallLocURINode+"/"):
return "./" + canon
default:
return locURI
}
}
func (v *windowsSCEPProfileValidator) finalizeValidation() error {
if !v.isSCEPProfile() {
// Cheeky validation here, to only allow Exec elements in SCEP profiles.
+28
View File
@@ -79,6 +79,19 @@ func TestValidateUserProvided(t *testing.T) {
<Target><LocURI>./Vendor/MSFT/BitLocker/Foo</LocURI></Target>
</Item>
</Replace>
`),
},
wantErr: syncml.DiskEncryptionProfileRestrictionErrMsg,
},
{
name: "Reserved LocURI with scope-less prefix",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`
<Replace>
<Item>
<Target><LocURI>Vendor/MSFT/BitLocker/RequireDeviceEncryption</LocURI></Target>
</Item>
</Replace>
`),
},
wantErr: syncml.DiskEncryptionProfileRestrictionErrMsg,
@@ -702,6 +715,21 @@ func TestValidateUserProvided(t *testing.T) {
},
wantErr: fmt.Sprintf("You must use \"$FLEET_VAR_%s\" after \"ClientCertificateInstall/SCEP/\".", FleetVarSCEPWindowsCertificateID),
},
{
name: fmt.Sprintf("scope-less SCEP LocURI is treated as Device SCEP (missing $FLEET_VAR_%s rejected)", FleetVarSCEPWindowsCertificateID),
profile: MDMWindowsConfigProfile{
SyncML: []byte(`
<Add>
<Item>
<Target>
<LocURI>Vendor/MSFT/ClientCertificateInstall/SCEP/bogus-id-that-is-not-fleet-var/Install/CAThumbprint</LocURI>
</Target>
</Item>
</Add>
`),
},
wantErr: fmt.Sprintf("You must use \"$FLEET_VAR_%s\" after \"ClientCertificateInstall/SCEP/\".", FleetVarSCEPWindowsCertificateID),
},
{
name: "SCEP Profile with missing required LocURI",
profile: MDMWindowsConfigProfile{
+3 -2
View File
@@ -172,8 +172,9 @@ const (
)
const (
FleetBitLockerTargetLocURI = "/Vendor/MSFT/BitLocker"
FleetOSUpdateTargetLocURI = "/Vendor/MSFT/Policy/Config/Update"
FleetBitLockerTargetLocURI = "/Vendor/MSFT/BitLocker"
FleetOSUpdateTargetLocURI = "/Vendor/MSFT/Policy/Config/Update"
FleetRemoteWipeTargetLocURI = "/Vendor/MSFT/RemoteWipe"
DiskEncryptionProfileRestrictionErrMsg = "Couldn't add. The configuration profile can't include BitLocker settings."
)
+1 -1
View File
@@ -2312,7 +2312,7 @@ func (svc *Service) BatchSetMDMProfiles(
}
for _, p := range windowsProfilesSlice {
if !bytes.Contains(p.SyncML, []byte(syncml.FleetOSUpdateTargetLocURI)) {
if !fleet.ProfileTargetsReservedLocURI(p.SyncML, syncml.FleetOSUpdateTargetLocURI) {
continue
}
+15 -3
View File
@@ -1187,6 +1187,16 @@ func TestEnqueueWindowsMDMCommand(t *testing.T) {
</Target>
</Item>
</Exec>`, "", "./Device/Vendor/MSFT/RemoteWipe/doWipe"},
// Regression for #48752: a scope-less wipe LocURI (which Windows still executes) must not bypass the premium gate.
{"scope-less wipe, non premium license", false, `
<Exec>
<CmdID>1</CmdID>
<Item>
<Target>
<LocURI>Vendor/MSFT/RemoteWipe/doWipe</LocURI>
</Target>
</Item>
</Exec>`, "Requires Fleet Premium license", ""},
{"non-premium command", false, `
<Exec>
<CmdID>1</CmdID>
@@ -1217,9 +1227,11 @@ func TestEnqueueWindowsMDMCommand(t *testing.T) {
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
ctx = test.UserContext(ctx, test.UserAdmin)
// Use a per-subtest context so a premium license added by one case does not leak into later cases via the
// shared outer ctx (which would mask a missing premium gate).
cmdCtx := test.UserContext(ctx, test.UserAdmin)
if c.premium {
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
cmdCtx = license.NewContext(cmdCtx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
}
var svcImpl *Service
@@ -1229,7 +1241,7 @@ func TestEnqueueWindowsMDMCommand(t *testing.T) {
case *Service:
svcImpl = v
}
res, err := svcImpl.enqueueMicrosoftMDMCommand(ctx, []byte(c.xmlCmd), []string{"uuid"})
res, err := svcImpl.enqueueMicrosoftMDMCommand(cmdCtx, []byte(c.xmlCmd), []string{"uuid"})
if c.wantErr != "" {
require.Error(t, err)
+1 -5
View File
@@ -4376,11 +4376,7 @@ func executeWindowsProfileReconcileBatch(
// Windows equivalent of Apple's Commander struct, but I'd like
// to keep it simpler for now until we understand more.
func buildCommandFromProfileBytes(profileBytes []byte, commandUUID string) (*fleet.MDMWindowsCommand, error) {
rawCommand := profileBytes
if strings.Contains(string(rawCommand), "/Vendor/MSFT/ClientCertificateInstall/SCEP") && !strings.Contains(string(rawCommand), "<Atomic>") {
// It's a SCEP profile, so wrap it with <Atomic>
rawCommand = fmt.Appendf([]byte{}, "<Atomic>%s</Atomic>", rawCommand)
}
rawCommand := fleet.WrapSCEPProfileInAtomic(profileBytes)
cmds, err := fleet.UnmarshallMultiTopLevelXMLProfile(rawCommand)
if err != nil {
return nil, fmt.Errorf("unmarshalling profile bytes: %w", err)
+14
View File
@@ -593,6 +593,20 @@ func TestBuildCommandFromProfileBytes(t *testing.T) {
string(scepCmdWithAtomic.RawCommand),
)
})
t.Run("scope-less SCEP profile is wrapped in Atomic", func(t *testing.T) {
scepLocURI := "Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID/Install/ServerURL"
cmd, err := buildCommandFromProfileBytes(syncMLForTest(scepLocURI), "uuid-scopeless")
require.NoError(t, err)
require.Contains(t, string(cmd.RawCommand), "<Atomic>")
// A non-wrapped profile unmarshalls into a single top-level command; only an <Atomic> wrapper populates both nested
// command slices, so this is a definitive check that the scope-less SCEP profile was wrapped.
wrapped := new(fleet.SyncMLCmd)
require.NoError(t, xml.Unmarshal(cmd.RawCommand, wrapped))
require.Len(t, wrapped.ReplaceCommands, 1)
require.Len(t, wrapped.AddCommands, 1)
})
}
func syncMLForTest(locURI string) []byte {
+1 -1
View File
@@ -155,7 +155,7 @@ func (svc *Service) handleWindowsProfileSoftwareUpdate(
syncML []byte,
teamID uint,
) error {
if !bytes.Contains(syncML, []byte(syncml.FleetOSUpdateTargetLocURI)) {
if !fleet.ProfileTargetsReservedLocURI(syncML, syncml.FleetOSUpdateTargetLocURI) {
return nil
}