From c30c23252d77b403963a040a0543c77fc2b33f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20=C5=9Awi=C4=85tek?= Date: Mon, 11 Mar 2024 13:58:15 +0100 Subject: [PATCH] test(scripts/install.ps1): check Windows ACLs --- pkg/scripts_test/check_windows.go | 99 +++++++++++++++++++++++++++--- pkg/scripts_test/consts_windows.go | 23 +++++-- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/pkg/scripts_test/check_windows.go b/pkg/scripts_test/check_windows.go index 14c865f82a..e767ef1a0f 100644 --- a/pkg/scripts_test/check_windows.go +++ b/pkg/scripts_test/check_windows.go @@ -1,16 +1,33 @@ +//go:build windows + package sumologic_scripts_tests import ( "os" "path/filepath" "strings" + "syscall" "testing" + "unsafe" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/windows" ) +var ( + modAdvapi32 = syscall.NewLazyDLL("advapi32.dll") + procGetExplicitEntriesFromACL = modAdvapi32.NewProc("GetExplicitEntriesFromAclW") +) + +// A Windows ACL record. Windows represents these as windows.EXPLICIT_ACCESS, which comes with an impractical +// representation of trustees. Instead, we just use a string representation of SIDs. +type ACLRecord struct { + SID string + AccessPermissions windows.ACCESS_MASK + AccessMode windows.ACCESS_MODE +} + func checkAbortedDueToNoToken(c check) { require.Greater(c.test, len(c.output), 1) require.Greater(c.test, len(c.errorOutput), 1) @@ -69,23 +86,22 @@ func checkConfigFilesOwnershipAndPermissions(ownerSid string) func(c check) { paths, err := filepath.Glob(glob) require.NoError(c.test, err) for _, path := range paths { - var permissions uint32 + var aclRecords []ACLRecord info, err := os.Stat(path) require.NoError(c.test, err) if info.IsDir() { if path == opampDPath { - permissions = opampDPermissions + aclRecords = opampDPermissions } else { - permissions = configPathDirPermissions + aclRecords = configPathDirPermissions } } else { - permissions = configPathFilePermissions + aclRecords = configPathFilePermissions } - PathHasPermissions(c.test, path, permissions) - PathHasOwner(c.test, configPath, ownerSid) + PathHasWindowsACLs(c.test, path, aclRecords) + PathHasOwner(c.test, path, ownerSid) } } - PathHasPermissions(c.test, configPath, configPathFilePermissions) } } @@ -103,3 +119,72 @@ func PathHasOwner(t *testing.T, path string, ownerSID string) { require.Equal(t, ownerSID, owner.String(), "%s should be owned by user '%s'", path, ownerSID) } + +func PathHasWindowsACLs(t *testing.T, path string, expectedACLs []ACLRecord) { + securityDescriptor, err := windows.GetNamedSecurityInfo( + path, + windows.SE_FILE_OBJECT, + windows.DACL_SECURITY_INFORMATION, + ) + require.NoError(t, err) + + // get the ACL entries + acl, _, err := securityDescriptor.DACL() + require.NoError(t, err) + require.NotNil(t, acl) + entries, err := GetExplicitEntriesFromACL(acl) + require.NoError(t, err) + aclRecords := []ACLRecord{} + for _, entry := range entries { + aclRecord := ExplicitEntryToACLRecord(entry) + if aclRecord != nil { + aclRecords = append(aclRecords, *aclRecord) + } + } + assert.Equal(t, expectedACLs, aclRecords, "invalid ACLs for %s", path) +} + +// GetExplicitEntriesFromACL gets a list of explicit entries from an ACL +// This doesn't exist in golang.org/x/sys/windows so we need to define it ourselves. +func GetExplicitEntriesFromACL(acl *windows.ACL) ([]windows.EXPLICIT_ACCESS, error) { + var pExplicitEntries *windows.EXPLICIT_ACCESS + var explicitEntriesSize uint64 + // Get dacl + r1, _, err := procGetExplicitEntriesFromACL.Call( + uintptr(unsafe.Pointer(acl)), + uintptr(unsafe.Pointer(&explicitEntriesSize)), + uintptr(unsafe.Pointer(&pExplicitEntries)), + ) + if r1 != 0 { + return nil, err + } + if pExplicitEntries == nil { + return []windows.EXPLICIT_ACCESS{}, nil + } + + // convert the pointer we got from Windows to a Go slice by doing some gnarly looking pointer arithmetic + explicitEntries := make([]windows.EXPLICIT_ACCESS, explicitEntriesSize) + for i := 0; i < int(explicitEntriesSize); i++ { + elementPtr := unsafe.Pointer( + uintptr(unsafe.Pointer(pExplicitEntries)) + + uintptr(i)*unsafe.Sizeof(pExplicitEntries), + ) + explicitEntries[i] = *(*windows.EXPLICIT_ACCESS)(elementPtr) + } + return explicitEntries, nil +} + +// ExplicitEntryToACLRecord converts a windows.EXPLICIT_ACCESS to a ACLRecord. If the trustee type is not SID, +// we return nil. +func ExplicitEntryToACLRecord(entry windows.EXPLICIT_ACCESS) *ACLRecord { + trustee := entry.Trustee + if trustee.TrusteeType != windows.TRUSTEE_IS_SID { + return nil + } + trusteeSid := (*windows.SID)(unsafe.Pointer(entry.Trustee.TrusteeValue)) + return &ACLRecord{ + SID: trusteeSid.String(), + AccessMode: entry.AccessMode, + AccessPermissions: entry.AccessPermissions, + } +} diff --git a/pkg/scripts_test/consts_windows.go b/pkg/scripts_test/consts_windows.go index ebb65d1e80..301e1fe89b 100644 --- a/pkg/scripts_test/consts_windows.go +++ b/pkg/scripts_test/consts_windows.go @@ -2,6 +2,8 @@ package sumologic_scripts_tests +import "golang.org/x/sys/windows" + const ( // See: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers localSystemSID string = "S-1-5-18" @@ -23,10 +25,19 @@ const ( installTokenEnv string = "SUMOLOGIC_INSTALLATION_TOKEN" apiBaseURL string = "https://open-collectors.sumologic.com" - commonConfigPathFilePermissions uint32 = 0777 - configPathDirPermissions uint32 = 0777 - configPathFilePermissions uint32 = 0666 - confDPathFilePermissions uint32 = 0777 - etcPathPermissions uint32 = 0777 - opampDPermissions uint32 = 0777 + allFilePermissions = windows.STANDARD_RIGHTS_ALL | windows.FILE_GENERIC_READ | windows.FILE_GENERIC_WRITE | windows.FILE_GENERIC_EXECUTE | 0x00000040 +) + +var ( + pathPermissions = []ACLRecord{ + { + SID: localSystemSID, + AccessPermissions: allFilePermissions, + AccessMode: windows.GRANT_ACCESS, + }, + } + filePermissions = []ACLRecord{} + opampDPermissions = pathPermissions + configPathDirPermissions = pathPermissions + configPathFilePermissions = filePermissions )