Add logic for Windows profiles validation (#15120)

related to #14361 and #14366 this adds validations to user-provided
Windows profiles
This commit is contained in:
Roberto Dip
2023-11-13 17:35:26 -03:00
committed by GitHub
parent 1f73ea6d6a
commit bdadd5c288
5 changed files with 228 additions and 0 deletions
+58
View File
@@ -1,7 +1,14 @@
package fleet
import (
"errors"
"fmt"
"strings"
"time"
"github.com/beevik/etree"
"github.com/fleetdm/fleet/v4/server/mdm"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
)
// MDMWindowsBitLockerSummary reports the number of Windows hosts being managed by Fleet with
@@ -29,6 +36,57 @@ type MDMWindowsConfigProfile struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// ValidateUserProvided ensures that the SyncML content in the profile is valid
// for Windows.
//
// It checks that all top-level elements are <Replace> and none of the <LocURI>
// elements within <Target> are reserved URIs.
//
// Returns an error if these conditions are not met.
func (m *MDMWindowsConfigProfile) ValidateUserProvided() error {
if mdm.GetRawProfilePlatform(m.SyncML) != "windows" {
return errors.New("Only <Replace> supported as a top level element. Make sure you dont have other top level elements.")
}
doc := etree.NewDocument()
if err := doc.ReadFromBytes(m.SyncML); err != nil {
return fmt.Errorf("Couldnt upload. The file should include valid XML: %w", err)
}
for _, element := range doc.ChildElements() {
if element.Tag != CmdReplace {
return errors.New("Only <Replace> supported as a top level element. Make sure you dont have other top level elements.")
}
for _, target := range element.FindElements("Target") {
locURI := target.FindElement("LocURI")
if locURI != nil {
if err := validateFleetProvidedLocURI(locURI.Text()); err != nil {
return err
}
}
}
}
return nil
}
var fleetProvidedLocURIValidationMap = map[string][2]string{
microsoft_mdm.FleetBitLockerTargetLocURI: {"BitLocker", "mdm.enable_disk_encryption"},
microsoft_mdm.FleetOSUpdateTargetLocURI: {"Windows updates", "mdm.windows_updates"},
}
func validateFleetProvidedLocURI(locURI string) error {
sanitizedLocURI := strings.TrimSpace(locURI)
for fleetLocURI, errHints := range fleetProvidedLocURIValidationMap {
if strings.Contains(sanitizedLocURI, fleetLocURI) {
return fmt.Errorf("Custom configuration profiles cant include %s settings. To control these settings, use the %s option.", errHints[0], errHints[1])
}
}
return nil
}
type MDMWindowsProfilePayload struct {
ProfileUUID string `db:"profile_uuid"`
ProfileName string `db:"profile_name"`
+83
View File
@@ -0,0 +1,83 @@
package fleet
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestValidateUserProvided(t *testing.T) {
tests := []struct {
name string
profile MDMWindowsConfigProfile
wantErr bool
}{
{
name: "Valid XML with Replace",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace>`),
},
wantErr: false,
},
{
name: "Invalid Platform",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<SyncML xmlns="SYNCML:SYNCML1.2"><Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace></SyncML>`),
},
wantErr: true,
},
{
name: "Invalid XML Structure",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Add><Target><LocURI>Custom/URI</LocURI></Target></Add>`),
},
wantErr: true,
},
{
name: "Reserved LocURI",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>./Device/Vendor/MSFT/BitLocker/Foo</LocURI></Target></Replace>`),
},
wantErr: true,
},
{
name: "XML with Multiple Replace Elements",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI1</LocURI></Target></Replace><Replace><Target><LocURI>Custom/URI2</LocURI></Target></Replace>`),
},
wantErr: false,
},
{
name: "Empty XML",
profile: MDMWindowsConfigProfile{
SyncML: []byte(``),
},
wantErr: true,
},
{
name: "XML with Multiple Replace Elements, One with Reserved LocURI",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace><Replace><Target><LocURI>./Device/Vendor/MSFT/BitLocker/Bar</LocURI></Target></Replace>`),
},
wantErr: true,
},
{
name: "XML with Mixed Replace and Add",
profile: MDMWindowsConfigProfile{
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace><Add><Target><LocURI>Another/URI</LocURI></Target></Add>`),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.profile.ValidateUserProvided()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
+21
View File
@@ -1,6 +1,7 @@
package mdm
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
@@ -23,3 +24,23 @@ func DecryptBase64CMS(p7Base64 string, cert *x509.Certificate, key crypto.Privat
return p7.Decrypt(cert, key)
}
func GetRawProfilePlatform(profile []byte) string {
trimmedProfile := bytes.TrimSpace(profile)
if len(trimmedProfile) == 0 {
return ""
}
darwinPrefix := []byte("<?xml")
if len(trimmedProfile) >= len(darwinPrefix) && bytes.EqualFold(darwinPrefix, trimmedProfile[:len(darwinPrefix)]) {
return "darwin"
}
windowsPrefix := []byte("<replace")
if len(trimmedProfile) >= len(windowsPrefix) && bytes.EqualFold(windowsPrefix, trimmedProfile[:len(windowsPrefix)]) {
return "windows"
}
return ""
}
+61
View File
@@ -117,3 +117,64 @@ oHwpyQbv9Qs+3bjPOQ7DkwekT+w1cptEKudBCC3WQKui1P0NNL0R
// prevent static analysis tools from raising issues due to detection of private key
// in code.
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
func TestGetRawProfilePlatform(t *testing.T) {
testCases := []struct {
name string
input []byte
expected string
}{
{
name: "Darwin case sensitive",
input: []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"),
expected: "darwin",
},
{
name: "Darwin case insensitive",
input: []byte("<?XML version=\"1.0\" encoding=\"UTF-8\"?>"),
expected: "darwin",
},
{
name: "Windows case sensitive",
input: []byte("<Replace this=\"that\">"),
expected: "windows",
},
{
name: "Windows case insensitive",
input: []byte("<REPLACE this=\"that\">"),
expected: "windows",
},
{
name: "Whitespace before prefix",
input: []byte(" <?xml version=\"1.0\"?>"),
expected: "darwin",
},
{
name: "Non-matching prefix",
input: []byte("<nonmatching>"),
expected: "",
},
{
name: "Empty input",
input: []byte(""),
expected: "",
},
{
name: "Only whitespaces",
input: []byte(" "),
expected: "",
},
{
name: "Partial match",
input: []byte("<?x"),
expected: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := GetRawProfilePlatform(tt.input)
require.Equal(t, tt.expected, got)
})
}
}
+5
View File
@@ -395,6 +395,11 @@ const (
CmdAlertGeneric = "1226"
)
const (
FleetBitLockerTargetLocURI = "./Device/Vendor/MSFT/BitLocker"
FleetOSUpdateTargetLocURI = "./Device/Vendor/MSFT/Policy/Config/Update/"
)
func ResolveWindowsMDMDiscovery(serverURL string) (string, error) {
return commonmdm.ResolveURL(serverURL, MDE2DiscoveryPath, false)
}