-
Notifications
You must be signed in to change notification settings - Fork 452
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes issue verifying Windows CSP profiles that contain ADMX policies. (
#25528) For #24790 Support verifying Windows CSPs with ADMX policies. https://learn.microsoft.com/en-us/windows/client-management/understanding-admx-backed-policies # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality
- Loading branch information
Showing
5 changed files
with
339 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Fixes issue verifying Windows CSP profiles that contain ADMX policies. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Package admx handles ADMX (Administrative Template File) policies for Microsoft MDM server. | ||
// See: https://learn.microsoft.com/en-us/windows/client-management/understanding-admx-backed-policies | ||
// | ||
// ADMX policy payload example: | ||
// <![CDATA[ | ||
// | ||
// <enabled/> | ||
// <data id="Publishing_Server2_Name_Prompt" value="Name"/> | ||
// <data id="Publishing_Server_URL_Prompt" value="http://someuri"/> | ||
// <data id="Global_Publishing_Refresh_Options" value="1"/> | ||
// | ||
// ]]> | ||
package admx | ||
|
||
import ( | ||
"encoding/xml" | ||
"fmt" | ||
"slices" | ||
"strings" | ||
) | ||
|
||
func IsADMX(text string) bool { | ||
// We try to unmarshal the string to see if it looks like a valid ADMX policy | ||
policy, err := unmarshal(text) | ||
if err != nil { | ||
return false | ||
} | ||
return policy.Enabled.Local == "enabled" || policy.Disabled.Local == "disabled" || len(policy.Data) > 0 | ||
} | ||
|
||
func Equal(a, b string) (bool, error) { | ||
aPolicy, err := unmarshal(a) | ||
if err != nil { | ||
return false, fmt.Errorf("unmarshalling ADMX policy a: %w", err) | ||
} | ||
bPolicy, err := unmarshal(b) | ||
if err != nil { | ||
return false, fmt.Errorf("unmarshalling ADMX policy b: %w", err) | ||
} | ||
return aPolicy.Equal(bPolicy), nil | ||
} | ||
|
||
func unmarshal(a string) (admxPolicy, error) { | ||
// We unmarshal into a string to get the CDATA content and decode XML escape characters. | ||
// We wrap the policy in an <admx> tag to ensure it can be unmarshalled by the XML decoder. | ||
var unescaped string | ||
err := xml.Unmarshal([]byte(`<admx>`+a+`</admx>`), &unescaped) | ||
if err != nil { | ||
return admxPolicy{}, fmt.Errorf("unmarshalling ADMX policy to string: %w", err) | ||
} | ||
// ADMX policy elements are not case-sensitive. For example: <enabled/> and <Enabled/> are equivalent | ||
// For simplicity, we compare everything in lowercase. | ||
var policy admxPolicy | ||
err = xml.Unmarshal([]byte(`<admx>`+strings.ToLower(unescaped)+`</admx>`), &policy) | ||
if err != nil { | ||
return admxPolicy{}, fmt.Errorf("unmarshalling ADMX policy: %w", err) | ||
} | ||
return policy, nil | ||
} | ||
|
||
type admxPolicy struct { | ||
Enabled xml.Name `xml:"enabled,omitempty"` | ||
Disabled xml.Name `xml:"disabled,omitempty"` | ||
Data []admxPolicyItem `xml:"data"` | ||
} | ||
|
||
func (a admxPolicy) Equal(b admxPolicy) bool { | ||
if a.Disabled.Local != b.Disabled.Local { | ||
return false | ||
} | ||
if a.Disabled.Local == "disabled" { | ||
// If the ADMX policy is disabled, the data is not relevant | ||
return true | ||
} | ||
if a.Enabled.Local != b.Enabled.Local { | ||
return false | ||
} | ||
if len(a.Data) != len(b.Data) { | ||
return false | ||
} | ||
a.sortData() | ||
b.sortData() | ||
for i := range a.Data { | ||
if !a.Data[i].Equal(b.Data[i]) { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
func (a *admxPolicy) sortData() { | ||
slices.SortFunc(a.Data, func(i, j admxPolicyItem) int { | ||
return strings.Compare(i.ID, j.ID) | ||
}) | ||
} | ||
|
||
type admxPolicyItem struct { | ||
ID string `xml:"id,attr"` | ||
Value string `xml:"value,attr"` | ||
} | ||
|
||
func (a admxPolicyItem) Equal(b admxPolicyItem) bool { | ||
return a.ID == b.ID && a.Value == b.Value | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
package admx | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestIsADMX(t *testing.T) { | ||
t.Parallel() | ||
assert.False(t, IsADMX("")) | ||
assert.False(t, IsADMX("not an ADMX policy")) | ||
assert.False(t, IsADMX(`<![CDATA[bozo]]>`)) | ||
assert.False(t, IsADMX(`<![CDATA[<bozo/>]]>`)) | ||
assert.False(t, IsADMX(`<![CDATA[<bozo]]>`)) | ||
assert.True(t, IsADMX(`<![CDATA[<enabled/>]]>`)) | ||
assert.True(t, IsADMX(`<![CDATA[<disabled/>]]>`)) | ||
assert.True(t, IsADMX(`<![CDATA[<data id="id" value="value"/>]]>`)) | ||
assert.True(t, IsADMX( | ||
` <![CDATA[ | ||
<enabled/> | ||
]]>`)) | ||
assert.True(t, | ||
IsADMX("<Enabled/><Data id=\"EnableScriptBlockInvocationLogging\" value=\"true\"/><Data id=\"ExecutionPolicy\" value=\"AllSigned\"/><Data id=\"Listbox_ModuleNames\" value=\"*\"/><Data id=\"OutputDirectory\" value=\"false\"/><Data id=\"SourcePathForUpdateHelp\" value=\"false\"/>")) | ||
assert.True(t, IsADMX( | ||
`<Enabled/> | ||
<![CDATA[<data id="ExecutionPolicy" value="AllSigned"/>]]> | ||
<![CDATA[<data id="Listbox_ModuleNames" value="*"/> | ||
<data id="OutputDirectory" value="false"/> | ||
<data id="EnableScriptBlockInvocationLogging" value="true"/> | ||
<data id="SourcePathForUpdateHelp" value="false"/>]]>`)) | ||
} | ||
|
||
func TestEqual(t *testing.T) { | ||
t.Parallel() | ||
testCases := []struct { | ||
name, a, b, errorContains string | ||
equal bool | ||
}{ | ||
{ | ||
name: "empty policies", | ||
a: "", | ||
b: "", | ||
equal: true, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "enabled policies", | ||
a: "<![CDATA[<enabled/>]]>", | ||
b: "<Enabled/>", | ||
equal: true, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "disabled policies", | ||
a: "<![CDATA[<disabled/>]]>", | ||
b: "<Disabled/>", | ||
equal: true, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "unequal policies", | ||
a: "<![CDATA[<disabled/>]]>", | ||
b: "<enabled/>", | ||
equal: false, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "enabled policies with data", | ||
a: `<![CDATA[<enabled/> | ||
<data id="ExecutionPolicy" value="AllSigned"/> | ||
<data id="Listbox_ModuleNames" value="*"/> | ||
<data id="OutputDirectory" value="false"/> | ||
<data id="EnableScriptBlockInvocationLogging" value="true"/> | ||
<data id="SourcePathForUpdateHelp" value="false"/>]]>`, | ||
b: "<Enabled/><Data id=\"EnableScriptBlockInvocationLogging\" value=\"true\"/><Data id=\"ExecutionPolicy\" value=\"AllSigned\"/><Data id=\"Listbox_ModuleNames\" value=\"*\"/><Data id=\"OutputDirectory\" value=\"false\"/><Data id=\"SourcePathForUpdateHelp\" value=\"false\"/>", | ||
equal: true, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "enabled policies with data and nonstandard format", | ||
a: `<Enabled/> | ||
<![CDATA[<data id="ExecutionPolicy" value="AllSigned"/>]]> | ||
<![CDATA[<data id="Listbox_ModuleNames" value="*"/> | ||
<data id="OutputDirectory" value="false"/> | ||
<data id="EnableScriptBlockInvocationLogging" value="true"/> | ||
<data id="SourcePathForUpdateHelp" value="false"/>]]>`, | ||
b: "<Enabled/><Data id=\"EnableScriptBlockInvocationLogging\" value=\"true\"/><Data id=\"ExecutionPolicy\" value=\"AllSigned\"/><Data id=\"Listbox_ModuleNames\" value=\"*\"/><Data id=\"OutputDirectory\" value=\"false\"/><Data id=\"SourcePathForUpdateHelp\" value=\"false\"/>", | ||
equal: true, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "disabled policies with data", | ||
a: `<![CDATA[<disabled/> | ||
<data id="ExecutionPolicy" value="AllSigned"/> | ||
<data id="SourcePathForUpdateHelp" value="false"/>]]>`, | ||
b: "<Disabled/><Data id=\"EnableScriptBlockInvocationLogging\" value=\"true\"/><Data id=\"ExecutionPolicy\" value=\"AllSigned\"/><Data id=\"Listbox_ModuleNames\" value=\"*\"/><Data id=\"OutputDirectory\" value=\"false\"/><Data id=\"SourcePathForUpdateHelp\" value=\"false\"/>", | ||
equal: true, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "unparsable policy a 1", | ||
a: "<bozo", | ||
b: "", | ||
equal: false, | ||
errorContains: "unmarshalling ADMX policy", | ||
}, | ||
{ | ||
name: "unparsable policy a 2", | ||
a: "<bozo", | ||
b: "", | ||
equal: false, | ||
errorContains: "unmarshalling ADMX policy", | ||
}, | ||
{ | ||
name: "unparsable policy b 1", | ||
a: "", | ||
b: "<bozo", | ||
equal: false, | ||
errorContains: "unmarshalling ADMX policy", | ||
}, | ||
{ | ||
name: "unparsable policy b 2", | ||
a: "", | ||
b: "<bozo", | ||
equal: false, | ||
errorContains: "unmarshalling ADMX policy", | ||
}, | ||
{ | ||
name: "unequal policies with missing enable", | ||
a: `<![CDATA[<Xenabled/> | ||
<data id="ExecutionPolicy" value="AllSigned"/> | ||
<data id="Listbox_ModuleNames" value="*"/> | ||
<data id="OutputDirectory" value="false"/> | ||
<data id="EnableScriptBlockInvocationLogging" value="true"/> | ||
<data id="SourcePathForUpdateHelp" value="false"/>]]>`, | ||
b: "<Enabled/><Data id=\"EnableScriptBlockInvocationLogging\" value=\"true\"/><Data id=\"ExecutionPolicy\" value=\"AllSigned\"/><Data id=\"Listbox_ModuleNames\" value=\"*\"/><Data id=\"OutputDirectory\" value=\"false\"/><Data id=\"SourcePathForUpdateHelp\" value=\"false\"/>", | ||
equal: false, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "unequal policies with data 1", | ||
a: `<![CDATA[<enabled/> | ||
<data id="EnableScriptBlockInvocationLogging" value="true"/> | ||
<data id="SourcePathForUpdateHelp" value="false"/>]]>`, | ||
b: "<Enabled/><Data id=\"EnableScriptBlockInvocationLogging\" value=\"true\"/><Data id=\"ExecutionPolicy\" value=\"AllSigned\"/><Data id=\"Listbox_ModuleNames\" value=\"*\"/><Data id=\"OutputDirectory\" value=\"false\"/><Data id=\"SourcePathForUpdateHelp\" value=\"false\"/>", | ||
equal: false, | ||
errorContains: "", | ||
}, | ||
{ | ||
name: "unequal policies with data 2", | ||
a: `<![CDATA[<enabled/> | ||
<data id="ExecutionPolicy" value="XXXX"/> | ||
<data id="Listbox_ModuleNames" value="*"/> | ||
<data id="OutputDirectory" value="false"/> | ||
<data id="EnableScriptBlockInvocationLogging" value="true"/> | ||
<data id="SourcePathForUpdateHelp" value="false"/>]]>`, | ||
b: "<Enabled/><Data id=\"EnableScriptBlockInvocationLogging\" value=\"true\"/><Data id=\"ExecutionPolicy\" value=\"AllSigned\"/><Data id=\"Listbox_ModuleNames\" value=\"*\"/><Data id=\"OutputDirectory\" value=\"false\"/><Data id=\"SourcePathForUpdateHelp\" value=\"false\"/>", | ||
equal: false, | ||
errorContains: "", | ||
}, | ||
} | ||
for _, tt := range testCases { | ||
t.Run(tt.name, func(t *testing.T) { | ||
equal, err := Equal(tt.a, tt.b) | ||
if tt.errorContains == "" { | ||
assert.NoError(t, err) | ||
} else { | ||
assert.ErrorContains(t, err, tt.errorContains) | ||
} | ||
assert.Equal(t, tt.equal, equal) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters