From 0f081ef10dfa960958aacae91d02705c6585553d Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Wed, 21 Jan 2026 01:15:48 +0000 Subject: [PATCH 01/16] introduced extension policy file to lib --- .../extensionruntimepolicy.go | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 pkg/extensionruntimepolicy/extensionruntimepolicy.go diff --git a/pkg/extensionruntimepolicy/extensionruntimepolicy.go b/pkg/extensionruntimepolicy/extensionruntimepolicy.go new file mode 100644 index 0000000..9044ff7 --- /dev/null +++ b/pkg/extensionruntimepolicy/extensionruntimepolicy.go @@ -0,0 +1,62 @@ +package extensionruntimepolicy + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/Azure/azure-extension-platform/pkg/extensionerrors" + "github.com/Azure/azure-extension-platform/pkg/logging" + "github.com/Azure/azure-extension-platform/pkg/status" +) + +const extensionRuntimePolicyFileName = "ExtensionRuntimePolicy.json" // Lourdes come back to this. + +// ScriptType is consistent with the ScriptType defined in CRP. +type ScriptType string + +const ( + Inline ScriptType = "Inline" + Downloaded ScriptType = "Downloaded" + Gallery ScriptType = "Gallery" + Diagnostic ScriptType = "Diagnostic" + CommandId ScriptType = "CommandId" + None ScriptType = "None" +) + +// FileType here refers to files types that can be signed. +type FileType string + +const ( + All FileType = "All" + None FileType = "None" + Script FileType = "Script" +) + +type AllowedScriptTypes struct { + AllowedCommandId bool + Gallery bool + Diagnostic bool + Inline bool + AllowedDownloaded bool + AllowAll bool +} + +// ExtensionRuntimePolicy is internal structure used to deserialize the extension runtime policy file +type extensionRuntimePolicy struct { + RequireSigning FileType + FileRootCert string + DownloadedScriptsAllowList []string + CommandIdAllowList []string + RunAsUser string + LimitScripts AllowedScriptTypes + DisableOutputBlobs bool + DisallowDomainPasswordChange bool + ApplicationAllowList []string +} + +type extensionRuntimePolicyFile struct { + RuntimePolicy []extensionRuntimePolicyContainer `json:"runtimeSettings"` +} + +//func GetExtensionRunTimePolicy(el logging.ILogger, statusFolder string, seqNo uint) (erp *extensionRuntimePolicy, _ error) { \ No newline at end of file From ff00ec8e10ac377d6c816fa6d4ae219a53e522cc Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Wed, 18 Feb 2026 01:04:50 +0000 Subject: [PATCH 02/16] initial commit for common code --- .../extensionpolicysettings.go | 167 ++++++++++++++++++ .../extensionpolicysettings_test.go | 0 .../extensionruntimepolicy.go | 62 ------- 3 files changed, 167 insertions(+), 62 deletions(-) create mode 100644 pkg/extensionpolicysettings/extensionpolicysettings.go create mode 100644 pkg/extensionpolicysettings/extensionpolicysettings_test.go delete mode 100644 pkg/extensionruntimepolicy/extensionruntimepolicy.go diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go new file mode 100644 index 0000000..3cd08f7 --- /dev/null +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -0,0 +1,167 @@ +package extensionpolicysettings + +import ( + "encoding/json" + "fmt" + "path/filepath" + "os" + "io/ioutil" + + "github.com/Azure/azure-extension-platform/pkg/extensionerrors" + "github.com/Azure/azure-extension-platform/pkg/logging" + "github.com/Azure/azure-extension-platform/pkg/status" +) + +const extensionPolicySettingsFileName = "ExtensionRuntimePolicy.json" // Lourdes come back to this. This name is consistent with the filename defined by linux GA. + +// ScriptType is consistent with the ScriptType defined in CRP. + + +// want: +// extendion policy settings struct passed in by client +// must satisfy validate (like the actual policy is properly set, or no) + +type ExtensionPolicySettings interface { + Validate() error +} + +type ExtensionPolicySettingsManager[T ExtensionPolicySettings] struct { + settingsFilePath string + logger logging.Logger + settings T +} + +func NewExtensionPolicySettingsManager[T ExtensionPolicySettings](configFolder string, logger logging.ILogger) (*ExtensionPolicySettingsManager[T], error) { + settingsFilePath := filepath.Join(configFolder, extensionPolicySettingsFileName) + return &ExtensionPolicySettingsManager[T]{ + settingsFilePath: settingsFilePath, + logger: logger, // settings is not loaded until LoadExtensionPolicySettings is called + }, nil +} + +func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() error { + epsm.logger.LogInfo(fmt.Sprintf("Loading extension policy settings from file: %s", epsm.settingsFilePath)) + + // If an extension has a default policy configuration in case the file does not exist, they should handle that logic before calling this function. + if _, err := os.Stat(epsm.settingsFilePath); os.IsNotExist(err) { + return fmt.Errorf("extension policy settings file does not exist at path: %s", epsm.settingsFilePath) + } else if err != nil { + return fmt.Errorf("error checking extension policy settings file: %w", err) + } + + fileContent, err := ioutil.ReadFile(epsm.settingsFilePath) + if err != nil { + return fmt.Errorf("failed to read extension policy settings file: %w", err) // lourdes: check if this is the correct error handling pattern for your project + } + + if len(fileContent) == 0 { + return fmt.Errorf("extension policy settings file is empty") + } + + var settings T + err := json.Unmarshal(fileContent, &settings) + if err != nil { + return fmt.Errorf("failed to unmarshal extension policy settings: %w", err) + } + + if err := settings.Validate(); err != nil { + return fmt.Errorf("extension policy settings validation failed: %w", err) + } + + epsm.settings = settings + epsm.logger.LogInfo("Extension policy settings loaded and validated successfully.") + return nil +} + +func (epsm *ExtensionPolicySettingsManager[T]) GetSettings() (T, error) { + return epsm.settings, nil +} + +// Validation Helper Functions +type fileInputType int +const ( + filepath fileInputType = iota + fileContents +) + +type hashType int +const ( + noHash hashType = iota + sha1 + sha256 +) + +func ValidateAgainstAllowlist(logger logging.ILogger, value string, allowlist []string, inputOpt fileInputType, hashOpt hashType) (bool, error) { + // If extensions want special behavior when a list is empty, they should handle that before calling this function. For security purposes, we want to make sure that if an allowlist is expected, it should not be empty. + if allowlist == nil || len(allowlist) == 0 { + return false, fmt.Errorf("allowlist is empty") + } + + // first, make sure we have the content we're working with + if inputOpt == filepath { + if value == "" { + return false, fmt.Errorf("file path cannot be empty") + } + content, err := ioutil.ReadFile(value) + if err != nil { + return false, fmt.Errorf("failed to read file for validation: %w", err) + } + value = string(content) // lourdes: this replaces the file path with the file content. + } + + if value == "" { + return false, fmt.Errorf("contents of file to validate cannot be empty") // lourdes: or maybe they can? man idk + } + + // second, handle the hash scenario. + + if hashOpt != noHash { + logger.LogInfo("Computing hash of the value for validation.") + value, err := ComputeFileHash(logger, value, hashOpt) + if err != nil { + return false, fmt.Errorf("failed to compute hash for validation: %w", err) + } + logger.LogInfo(fmt.Sprintf("Computed hash value: %s", value)) + } + + // finally, check if the value (or its hash) is in the allowlist. + for _, allowlistValue := range allowlist { + if value == allowlistValue { + logger.LogInfo("Validation successful: file is in the allowlist.") + return true, nil + } + } + + logger.LogInfo("Validation failed: file is not in the allowlist.") + return false, nil +} + +// ComputeFileHash computes the hash of a file or leaves string as is. +func ComputeFileHash(logger logging.ILogger, contents string, hashOpt hashType) (string, error) { + logger.Info("Computing hash for file contents") + + if contents == "" { + return "", fmt.Errorf("contents cannot be empty") + } + + var hashStr string + switch hashOpt { + case sha1: + hash := sha1.Sum([]byte(contents)) + hashStr = hex.EncodeToString(hash[:]) + case sha256: + hash := sha256.Sum256([]byte(contents)) + hashStr = hex.EncodeToString(hash[:]) + default: + return "", fmt.Errorf("Invalid hash option") + } + + logger.Info("Computed hash: %s", hashStr) + return hashStr, nil +} + +// func ValidateFileEmbeddedSignature() error { +// } + +// func ValidateCatalogSignature() error { +// } diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_test.go b/pkg/extensionpolicysettings/extensionpolicysettings_test.go new file mode 100644 index 0000000..e69de29 diff --git a/pkg/extensionruntimepolicy/extensionruntimepolicy.go b/pkg/extensionruntimepolicy/extensionruntimepolicy.go deleted file mode 100644 index 9044ff7..0000000 --- a/pkg/extensionruntimepolicy/extensionruntimepolicy.go +++ /dev/null @@ -1,62 +0,0 @@ -package extensionruntimepolicy - -import ( - "encoding/json" - "fmt" - "path/filepath" - - "github.com/Azure/azure-extension-platform/pkg/extensionerrors" - "github.com/Azure/azure-extension-platform/pkg/logging" - "github.com/Azure/azure-extension-platform/pkg/status" -) - -const extensionRuntimePolicyFileName = "ExtensionRuntimePolicy.json" // Lourdes come back to this. - -// ScriptType is consistent with the ScriptType defined in CRP. -type ScriptType string - -const ( - Inline ScriptType = "Inline" - Downloaded ScriptType = "Downloaded" - Gallery ScriptType = "Gallery" - Diagnostic ScriptType = "Diagnostic" - CommandId ScriptType = "CommandId" - None ScriptType = "None" -) - -// FileType here refers to files types that can be signed. -type FileType string - -const ( - All FileType = "All" - None FileType = "None" - Script FileType = "Script" -) - -type AllowedScriptTypes struct { - AllowedCommandId bool - Gallery bool - Diagnostic bool - Inline bool - AllowedDownloaded bool - AllowAll bool -} - -// ExtensionRuntimePolicy is internal structure used to deserialize the extension runtime policy file -type extensionRuntimePolicy struct { - RequireSigning FileType - FileRootCert string - DownloadedScriptsAllowList []string - CommandIdAllowList []string - RunAsUser string - LimitScripts AllowedScriptTypes - DisableOutputBlobs bool - DisallowDomainPasswordChange bool - ApplicationAllowList []string -} - -type extensionRuntimePolicyFile struct { - RuntimePolicy []extensionRuntimePolicyContainer `json:"runtimeSettings"` -} - -//func GetExtensionRunTimePolicy(el logging.ILogger, statusFolder string, seqNo uint) (erp *extensionRuntimePolicy, _ error) { \ No newline at end of file From ac4d421a06e6833c50dd39c82a80d72a4e8f89d3 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Thu, 19 Feb 2026 23:34:58 +0000 Subject: [PATCH 03/16] added UTs and cleaned up code for allowlist validation --- pkg/extensionerrors/extensionerrors.go | 20 ++ .../extensionpolicysettings.go | 179 ++++++++---------- .../extensionpolicysettings_test.go | 163 ++++++++++++++++ .../testutils/testscripts/script1.sh | 3 + .../testutils/testscripts/script2.sh | 3 + .../testutils/testscripts/script3.sh | 3 + 6 files changed, 272 insertions(+), 99 deletions(-) create mode 100644 pkg/extensionpolicysettings/testutils/testscripts/script1.sh create mode 100644 pkg/extensionpolicysettings/testutils/testscripts/script2.sh create mode 100644 pkg/extensionpolicysettings/testutils/testscripts/script3.sh diff --git a/pkg/extensionerrors/extensionerrors.go b/pkg/extensionerrors/extensionerrors.go index c2ca68f..ed6ad0c 100644 --- a/pkg/extensionerrors/extensionerrors.go +++ b/pkg/extensionerrors/extensionerrors.go @@ -44,4 +44,24 @@ var ( ErrNotFound = errors.New("NotFound") ErrInvalidOperationName = errors.New("operation name is invalid") + + ErrMissingPolicyFile = errors.New("policy file is missing") + + ErrInvalidPolicyFile = errors.New("policy file is invalid") + + ErrEmptyPolicyFile = errors.New("policy file is empty") + + ErrFailedToUnmarshalPolicyFile = errors.New("failed to unmarshal policy file") + + ErrPolicyValidationFailed = errors.New("policy validation failed") + + ErrPolicyAllowlistEmpty = errors.New("policy allowlist is empty") + + ErrItemNotInAllowlist = errors.New("item is not in the allowlist") + + ErrEmptyFilepathToValidate = errors.New("filepath to validate file cannot be empty") + + ErrFailedToReadFileToValidate = errors.New("failed to read file to validate") + + ErrContentsToValidateEmpty = errors.New("contents to validate cannot be empty") ) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index 3cd08f7..5888404 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -1,167 +1,148 @@ package extensionpolicysettings import ( + "crypto/sha1" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" - "path/filepath" "os" - "io/ioutil" "github.com/Azure/azure-extension-platform/pkg/extensionerrors" "github.com/Azure/azure-extension-platform/pkg/logging" - "github.com/Azure/azure-extension-platform/pkg/status" ) -const extensionPolicySettingsFileName = "ExtensionRuntimePolicy.json" // Lourdes come back to this. This name is consistent with the filename defined by linux GA. - -// ScriptType is consistent with the ScriptType defined in CRP. - - -// want: -// extendion policy settings struct passed in by client -// must satisfy validate (like the actual policy is properly set, or no) - type ExtensionPolicySettings interface { - Validate() error + ValidateFormat() error } type ExtensionPolicySettingsManager[T ExtensionPolicySettings] struct { - settingsFilePath string - logger logging.Logger - settings T + settingsFilePath string + logger logging.ILogger + settings *T } -func NewExtensionPolicySettingsManager[T ExtensionPolicySettings](configFolder string, logger logging.ILogger) (*ExtensionPolicySettingsManager[T], error) { - settingsFilePath := filepath.Join(configFolder, extensionPolicySettingsFileName) +func NewExtensionPolicySettingsManager[T ExtensionPolicySettings](policyFilePath string, logger logging.ILogger) *ExtensionPolicySettingsManager[T] { return &ExtensionPolicySettingsManager[T]{ - settingsFilePath: settingsFilePath, - logger: logger, // settings is not loaded until LoadExtensionPolicySettings is called - }, nil + settingsFilePath: policyFilePath, + logger: logger, // settings is not loaded until LoadExtensionPolicySettings is called + } } func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() error { - epsm.logger.LogInfo(fmt.Sprintf("Loading extension policy settings from file: %s", epsm.settingsFilePath)) + epsm.logger.Info(fmt.Sprintf("Loading extension policy settings from file: %s", epsm.settingsFilePath)) // If an extension has a default policy configuration in case the file does not exist, they should handle that logic before calling this function. if _, err := os.Stat(epsm.settingsFilePath); os.IsNotExist(err) { - return fmt.Errorf("extension policy settings file does not exist at path: %s", epsm.settingsFilePath) + return extensionerrors.ErrMissingPolicyFile } else if err != nil { return fmt.Errorf("error checking extension policy settings file: %w", err) } - - fileContent, err := ioutil.ReadFile(epsm.settingsFilePath) + + // Read the file content: check: what if the file is locked? What if we don't have permissions to read? + + fileContent, err := os.ReadFile(epsm.settingsFilePath) if err != nil { - return fmt.Errorf("failed to read extension policy settings file: %w", err) // lourdes: check if this is the correct error handling pattern for your project + return fmt.Errorf("failed to read extension policy settings file: %w", err) // Should we have retry logic? } if len(fileContent) == 0 { - return fmt.Errorf("extension policy settings file is empty") + return extensionerrors.ErrEmptyPolicyFile } - var settings T - err := json.Unmarshal(fileContent, &settings) - if err != nil { + var settings *T = new(T) + if err := json.Unmarshal(fileContent, settings); err != nil { return fmt.Errorf("failed to unmarshal extension policy settings: %w", err) } - if err := settings.Validate(); err != nil { + // Extensions themselves must decide the criteria for valid policy settings (i.e., if they can be null etc.). + if err := (*settings).ValidateFormat(); err != nil { return fmt.Errorf("extension policy settings validation failed: %w", err) } epsm.settings = settings - epsm.logger.LogInfo("Extension policy settings loaded and validated successfully.") + epsm.logger.Info("Extension policy settings loaded and validated successfully.") return nil } -func (epsm *ExtensionPolicySettingsManager[T]) GetSettings() (T, error) { - return epsm.settings, nil +func (epsm *ExtensionPolicySettingsManager[T]) GetSettings() *T { + if epsm.settings == nil { + epsm.logger.Info("Extension policy settings have not been loaded yet. Returning nil.") + } + return epsm.settings } // Validation Helper Functions -type fileInputType int -const ( - filepath fileInputType = iota - fileContents -) +type HashType int -type hashType int const ( - noHash hashType = iota - sha1 - sha256 + HashTypeNone HashType = iota + HashTypeSHA1 + HashTypeSHA256 ) -func ValidateAgainstAllowlist(logger logging.ILogger, value string, allowlist []string, inputOpt fileInputType, hashOpt hashType) (bool, error) { - // If extensions want special behavior when a list is empty, they should handle that before calling this function. For security purposes, we want to make sure that if an allowlist is expected, it should not be empty. - if allowlist == nil || len(allowlist) == 0 { - return false, fmt.Errorf("allowlist is empty") +func ValidateValueInAllowlist(logger logging.ILogger, value string, allowlist []string) error { + if len(allowlist) == 0 { + return extensionerrors.ErrPolicyAllowlistEmpty } - // first, make sure we have the content we're working with - if inputOpt == filepath { - if value == "" { - return false, fmt.Errorf("file path cannot be empty") - } - content, err := ioutil.ReadFile(value) - if err != nil { - return false, fmt.Errorf("failed to read file for validation: %w", err) + for _, allowlistValue := range allowlist { + if value == allowlistValue { + logger.Info("Validation successful: item is in the allowlist.") + return nil } - value = string(content) // lourdes: this replaces the file path with the file content. + } + logger.Info("validation failed: item is not in the allowlist.") + return extensionerrors.ErrItemNotInAllowlist +} + +func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowlist []string, hashOpt HashType) error { + if len(allowlist) == 0 { + return extensionerrors.ErrPolicyAllowlistEmpty + } + + if filePath == "" { + return extensionerrors.ErrEmptyFilepathToValidate } - if value == "" { - return false, fmt.Errorf("contents of file to validate cannot be empty") // lourdes: or maybe they can? man idk + content, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file %s for validation: %w", filePath, err) } - // second, handle the hash scenario. + value := string(content) // What if content is empty? Do we want to treat that as an error or just compute the hash of an empty string? For now, we'll compute the hash of an empty string, but this is something to consider based on the specific use case and security requirements. - if hashOpt != noHash { - logger.LogInfo("Computing hash of the value for validation.") + if hashOpt != HashTypeNone { + logger.Info(fmt.Sprintf("Computing hash of file %s for validation.", filePath)) value, err := ComputeFileHash(logger, value, hashOpt) if err != nil { - return false, fmt.Errorf("failed to compute hash for validation: %w", err) - } - logger.LogInfo(fmt.Sprintf("Computed hash value: %s", value)) - } - - // finally, check if the value (or its hash) is in the allowlist. - for _, allowlistValue := range allowlist { - if value == allowlistValue { - logger.LogInfo("Validation successful: file is in the allowlist.") - return true, nil + return fmt.Errorf("failed to compute hash for file %s for validation: %w", filePath, err) } + logger.Info(fmt.Sprintf("Computed hash value for file %s: %s", filePath, value)) + return ValidateValueInAllowlist(logger, value, allowlist) } - logger.LogInfo("Validation failed: file is not in the allowlist.") - return false, nil + return ValidateValueInAllowlist(logger, value, allowlist) } // ComputeFileHash computes the hash of a file or leaves string as is. -func ComputeFileHash(logger logging.ILogger, contents string, hashOpt hashType) (string, error) { - logger.Info("Computing hash for file contents") - - if contents == "" { - return "", fmt.Errorf("contents cannot be empty") - } - - var hashStr string - switch hashOpt { - case sha1: - hash := sha1.Sum([]byte(contents)) - hashStr = hex.EncodeToString(hash[:]) - case sha256: - hash := sha256.Sum256([]byte(contents)) - hashStr = hex.EncodeToString(hash[:]) - default: - return "", fmt.Errorf("Invalid hash option") - } - - logger.Info("Computed hash: %s", hashStr) - return hashStr, nil -} +func ComputeFileHash(logger logging.ILogger, contents string, hashOpt HashType) (string, error) { + logger.Info("Computing hash for file contents") -// func ValidateFileEmbeddedSignature() error { -// } + if contents == "" { + return "", extensionerrors.ErrContentsToValidateEmpty + } -// func ValidateCatalogSignature() error { -// } + var hashStr string + switch hashOpt { + case HashTypeSHA1: + hash := sha1.Sum([]byte(contents)) + hashStr = hex.EncodeToString(hash[:]) + default: + hash := sha256.Sum256([]byte(contents)) + hashStr = hex.EncodeToString(hash[:]) + } + + logger.Info(fmt.Sprintf("Computed hash: %s", hashStr)) + return hashStr, nil +} diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_test.go b/pkg/extensionpolicysettings/extensionpolicysettings_test.go index e69de29..fabd2b4 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_test.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_test.go @@ -0,0 +1,163 @@ +// filepath: /home/anasanc/repos/azure-extension-platform/pkg/extensionpolicysettings/extensionpolicysettings_test.go +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +package extensionpolicysettings + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-extension-platform/pkg/extensionerrors" + "github.com/Azure/azure-extension-platform/pkg/logging" + "github.com/stretchr/testify/require" +) + +var extensionLogger = logging.New(nil) + +const extensionRuntimePolicySettingsFilePath = "./testutils/runtime_policy.json" + +// This is a sample struct for an example extension's policy settings. Each extension will define their own struct that implements the ExtensionPolicySettings interface according to their needs. +type TestPolicy struct { + RequiresSigning string `json:"requiresigning"` + AllowedScripts []string `json:"allowedscripts"` +} + +func (tp TestPolicy) ValidateFormat() error { + // In a real extension, you would implement logic to validate the policy was correctly loaded. + return nil +} + +func TestNewExtensionPolicySettingsManager(t *testing.T) { + // Create a new ExtensionPolicySettingsManager + manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + require.NotNil(t, manager) + require.Equal(t, extensionRuntimePolicySettingsFilePath, manager.settingsFilePath) + require.Equal(t, extensionLogger, manager.logger) + require.Nil(t, manager.settings) // settings should not be loaded until LoadExtensionPolicySettings is called +} + +func TestLoadExtensionPolicySettings(t *testing.T) { + // Setup test parameters + manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + + // Test cases: + // 1. Valid policy file: we should be able to load the settings without error + validPolicyContent := `{ + "requiresigning": "true", + "allowedscripts": [] + }` + writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) + defer cleanupFile(extensionRuntimePolicySettingsFilePath) + + // Call LoadExtensionPolicySettings and check for errors + err := manager.LoadExtensionPolicySettings() + require.NoError(t, err) + require.NotNil(t, manager.settings) + require.Equal(t, "true", manager.settings.RequiresSigning) + require.Empty(t, manager.settings.AllowedScripts) + + // 2. Invalid policy file (e.g. not valid json): we should get an error when trying to load the settings + invalidPolicyContent := `{` + writeToFile(extensionRuntimePolicySettingsFilePath, invalidPolicyContent) + err = manager.LoadExtensionPolicySettings() + require.Error(t, err) + + // 3. Empty policy file: we should get an error indicating the policy file is empty + writeToFile(extensionRuntimePolicySettingsFilePath, "") + err = manager.LoadExtensionPolicySettings() + require.ErrorIs(t, err, extensionerrors.ErrEmptyPolicyFile) + + // 5. Locked policy file: we should get an error indicating the file cannot be accessed. + // modify the file permissions to simulate a locked file (read-only file) + os.Chmod(extensionRuntimePolicySettingsFilePath, 0200) // write-only permissions + err = manager.LoadExtensionPolicySettings() + require.Error(t, err) + + // 5. Missing policy file: we should get an error indicating the policy file is missing + cleanupFile(extensionRuntimePolicySettingsFilePath) + err = manager.LoadExtensionPolicySettings() + require.ErrorIs(t, err, extensionerrors.ErrMissingPolicyFile) +} + +func TestGetSettings(t *testing.T) { + // Setup test parameters + manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + validPolicyContent := `{ + "requiresigning": "true", + "allowedscripts": [] + }` + writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) + defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test + + // Call LoadExtensionPolicySettings and check for errors + err := manager.LoadExtensionPolicySettings() + require.NoError(t, err) + require.NotNil(t, manager.settings) + require.Equal(t, "true", manager.settings.RequiresSigning) + + // Call GetSettings and check for errors + settings := manager.GetSettings() + require.NotNil(t, settings) +} + +func TestValidateAgainstAllowlist(t *testing.T) { + // Setup test parameters + manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test + + script1Hash := hashHelper("./testutils/testscripts/script1.sh") + script2Hash := hashHelper("./testutils/testscripts/script2.sh") + //script3Hash := hashHelper("./testutils/testscripts/script3.sh") + + // Some scripts are allowed + validPolicyContent := fmt.Sprintf(`{ + "requiresigning": "true", + "allowedscripts": ["%s", "%s"] + }`, script1Hash, script2Hash) + writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) + + // Call LoadExtensionPolicySettings and check for errors + err := manager.LoadExtensionPolicySettings() + require.NoError(t, err) + require.NotNil(t, manager.settings) + require.Equal(t, "true", manager.settings.RequiresSigning) + require.NotEmpty(t, manager.settings.AllowedScripts) + + require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script1.sh", manager.settings.AllowedScripts, HashTypeSHA256)) + require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script2.sh", manager.settings.AllowedScripts, HashTypeSHA256)) + require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script3.sh", manager.settings.AllowedScripts, HashTypeSHA256), extensionerrors.ErrItemNotInAllowlist) + require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "", manager.settings.AllowedScripts, HashTypeSHA256), extensionerrors.ErrEmptyFilepathToValidate) + require.Error(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/missing.sh", manager.settings.AllowedScripts, HashTypeSHA256)) + + // Now, empty list. + require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script1.sh", []string{}, HashTypeSHA256), extensionerrors.ErrPolicyAllowlistEmpty) +} + +func writeToFile(filePath, content string) { + err := os.WriteFile(filePath, []byte(content), 0644) + if err != nil { + panic(err) + } +} + +func cleanupFile(path string) { + // Do not remove missingPolicyFilePath as it simulates a missing file + if _, err := os.Stat(path); err == nil { + os.Remove(path) + } +} + +func hashHelper(filePath string) string { + contents, err := os.ReadFile(filePath) + + if err != nil { + panic(err) + } + + hash := sha256.Sum256([]byte(contents)) + hashStr := hex.EncodeToString(hash[:]) + return hashStr +} diff --git a/pkg/extensionpolicysettings/testutils/testscripts/script1.sh b/pkg/extensionpolicysettings/testutils/testscripts/script1.sh new file mode 100644 index 0000000..11a48d9 --- /dev/null +++ b/pkg/extensionpolicysettings/testutils/testscripts/script1.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# This is a simple shell script +echo "Hello, World! I am script1.sh" \ No newline at end of file diff --git a/pkg/extensionpolicysettings/testutils/testscripts/script2.sh b/pkg/extensionpolicysettings/testutils/testscripts/script2.sh new file mode 100644 index 0000000..937a424 --- /dev/null +++ b/pkg/extensionpolicysettings/testutils/testscripts/script2.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# This is a simple shell script +echo "Hello, World! I am script 2" \ No newline at end of file diff --git a/pkg/extensionpolicysettings/testutils/testscripts/script3.sh b/pkg/extensionpolicysettings/testutils/testscripts/script3.sh new file mode 100644 index 0000000..2d27484 --- /dev/null +++ b/pkg/extensionpolicysettings/testutils/testscripts/script3.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# This is a simple shell script +echo "I am a banned script." \ No newline at end of file From db0e0879231089917f2bdd8887d302b62e975101 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Tue, 24 Feb 2026 23:28:03 +0000 Subject: [PATCH 04/16] adding new linux and windows files --- pkg/extensionpolicysettings/extensionpolicysettings_linux.go | 0 pkg/extensionpolicysettings/extensionpolicysettings_windows.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pkg/extensionpolicysettings/extensionpolicysettings_linux.go create mode 100644 pkg/extensionpolicysettings/extensionpolicysettings_windows.go diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_linux.go b/pkg/extensionpolicysettings/extensionpolicysettings_linux.go new file mode 100644 index 0000000..e69de29 diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_windows.go b/pkg/extensionpolicysettings/extensionpolicysettings_windows.go new file mode 100644 index 0000000..e69de29 From 16d4915fbf0d3e1f2b2151353b717bc84fcaeb13 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Tue, 3 Mar 2026 19:21:20 +0000 Subject: [PATCH 05/16] WIP --- .../extensionpolicysettings_linux.go | 31 ++++++ .../extensionpolicysettings_windows.go | 1 + pkg/internal/cert/cert_windows.go | 104 ++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 pkg/internal/cert/cert_windows.go diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_linux.go b/pkg/extensionpolicysettings/extensionpolicysettings_linux.go index e69de29..13f8687 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_linux.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_linux.go @@ -0,0 +1,31 @@ +package extensionpolicysettings + +import ( + "bytes" + "fmt" + "os/exec" +) + +// declare function (obviously) +// write the passed-in signature to a file +// get the cert location (assuming cert gonna be passed in ) + +//create base command for open ssl + +// check and see if c groups are enabled; if so , scope the command using systemd just in case +// if it fails, then a) disable c groups and b)just run the comman directly. + +func ValidateFileSignature(filePath string, signature []byte, certPath string) (isValid bool, err error) { + // os.eexec + cmd := exec.Command("openssl", "cms", "-verify", "-content", filePath, "-certfile", certPath, "-signature", "-") + + cmd.Stdin = bytes.NewReader(signature) + var bOut, bErr bytes.Buffer + cmd.Stdout = &bOut + cmd.Stderr = &bErr + + if err := cmd.Run(); err != nil { + return false, fmt.Errorf("signature validation failed: error=%v stderr=%s", err, string(bErr.Bytes())) + } + return true, nil +} diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_windows.go b/pkg/extensionpolicysettings/extensionpolicysettings_windows.go index e69de29..8d62619 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_windows.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_windows.go @@ -0,0 +1 @@ +package extensionpolicysettings diff --git a/pkg/internal/cert/cert_windows.go b/pkg/internal/cert/cert_windows.go new file mode 100644 index 0000000..8a06a15 --- /dev/null +++ b/pkg/internal/cert/cert_windows.go @@ -0,0 +1,104 @@ +package cert + +import ( + "syscall" +) + +const ( + WINTRUST_ACTION_GENERIC_VERIFY_V2_GUID = "00AAC56B-CD44-11d0-8CC2-00C04FC295EE" +) + +var ( + Modwintrust = syscall.NewLazyDLL("wintrust.dll") + procWinVerifyTrust = Modwintrust.NewProc("WinVerifyTrust") +) + +type WTD_UI uint + +const ( + WTD_UI_ALL WTD_UI = 1 + WTD_UI_NONE WTD_UI = 2 + WTD_UI_NOBAD WTD_UI = 3 + WTD_UI_NOGOOD WTD_UI = 4 +) + +type WTD_REVOKE_FLAGS uint + +const ( + WTD_REVOKE_NONE WTD_REVOKE_FLAGS = 0x00000000 + WTD_REVOKE_WHOLECHAIN WTD_REVOKE_FLAGS = 0x00000001 +) + +type WTD_CHOICE uint + +const ( + WTD_CHOICE_FILE WTD_CHOICE = 1 + WTD_CHOICE_CATALOG WTD_CHOICE = 2 + WTD_CHOICE_BLOB WTD_CHOICE = 3 + WTD_CHOICE_SIGNER WTD_CHOICE = 4 + WTD_CHOICE_CERT WTD_CHOICE = 5 +) + +type WTD_STATE_ACTION uint + +const ( + WTD_STATEACTION_IGNORE WTD_STATE_ACTION = 0x00000000 + WTD_STATEACTION_VERIFY WTD_STATE_ACTION = 0x00000001 + WTD_STATEACTION_CLOSE WTD_STATE_ACTION = 0x00000002 + WTD_STATEACTION_AUTO_CACHE WTD_STATE_ACTION = 0x00000003 + WTD_STATEACTION_AUTO_CACHE_FLUSH WTD_STATE_ACTION = 0x00000004 +) + +type WTD_PROVIDER_FLAGS uint + +const ( + WTD_PROV_FLAGS_MASK = 0x0000FFFF + WTD_USE_IE4_TRUST_FLAG WTD_PROVIDER_FLAGS = 0x1 + WTD_NO_IE4_CHAIN_FLAG WTD_PROVIDER_FLAGS = 0x2 + WTD_NO_POLICY_USAGE_FLAG WTD_PROVIDER_FLAGS = 0x4 + WTD_REVOCATION_CHECK_NONE WTD_PROVIDER_FLAGS = 0x10 + WTD_REVOCATION_CHECK_END_CERT WTD_PROVIDER_FLAGS = 0x20 + WTD_REVOCATION_CHECK_CHAIN WTD_PROVIDER_FLAGS = 0x40 + WTD_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT WTD_PROVIDER_FLAGS = 0x80 + WTD_SAFER_FLAG WTD_PROVIDER_FLAGS = 0x100 + WTD_HASH_ONLY_FLAG WTD_PROVIDER_FLAGS = 0x200 + WTD_USE_DEFAULT_OSVER_CHECK WTD_PROVIDER_FLAGS = 0x400 + WTD_LIFETIME_SIGNING_FLAG WTD_PROVIDER_FLAGS = 0x800 +) + +type WTD_UICONTEXT uint + +const ( + WTD_UICONTEXT_EXECUTE WTD_UICONTEXT = 0 + WTD_UICONTEXT_INSTALL WTD_UICONTEXT = 1 +) + +type winTrustData struct { + cbStruct uint32 + pPolicyCallbackData uintptr + pSIPClientData uintptr + dwUIChoice WTD_UI + fdWRevocationChecks WTD_REVOKE_FLAGS + dwUnionChoice WTD_CHOICE + union [8]byte // This is a placeholder for the actual union data, which can be one of several types depending on dwUnionChoice + dwStateAction WTD_STATE_ACTION + hWVTStateData uintptr + pwszURLReference uintptr + dwProvFlags WTD_PROVIDER_FLAGS + dwUIContext WTD_UICONTEXT +} + +type winTrustFileInfo struct { + cbStruct uint32 + pcwszFile uintptr + hFile syscall.Handle + pgKnownSubject uintptr +} + +type winTrustCatalogInfo struct { + cbStruct uint32 + pcwszCatalogFile uintptr + pcwszMemberTag uintptr + hMemberFile syscall.Handle + pgKnownSubject uintptr +} From 96e5f13d68a70509069da723bfbc54dad3882b87 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Tue, 3 Mar 2026 19:34:29 +0000 Subject: [PATCH 06/16] nit change --- .../extensionpolicysettings_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_test.go b/pkg/extensionpolicysettings/extensionpolicysettings_test.go index fabd2b4..ee2267f 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_test.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_test.go @@ -21,8 +21,8 @@ const extensionRuntimePolicySettingsFilePath = "./testutils/runtime_policy.json" // This is a sample struct for an example extension's policy settings. Each extension will define their own struct that implements the ExtensionPolicySettings interface according to their needs. type TestPolicy struct { - RequiresSigning string `json:"requiresigning"` - AllowedScripts []string `json:"allowedscripts"` + RequiresSigning string `json:"requireSigning"` + AllowedScripts []string `json:"allowedScripts"` } func (tp TestPolicy) ValidateFormat() error { @@ -46,8 +46,8 @@ func TestLoadExtensionPolicySettings(t *testing.T) { // Test cases: // 1. Valid policy file: we should be able to load the settings without error validPolicyContent := `{ - "requiresigning": "true", - "allowedscripts": [] + "requireSigning": "true", + "allowedScripts": [] }` writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) defer cleanupFile(extensionRuntimePolicySettingsFilePath) @@ -86,8 +86,8 @@ func TestGetSettings(t *testing.T) { // Setup test parameters manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) validPolicyContent := `{ - "requiresigning": "true", - "allowedscripts": [] + "requireSigning": "true", + "allowedScripts": [] }` writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test @@ -114,8 +114,8 @@ func TestValidateAgainstAllowlist(t *testing.T) { // Some scripts are allowed validPolicyContent := fmt.Sprintf(`{ - "requiresigning": "true", - "allowedscripts": ["%s", "%s"] + "requireSigning": "true", + "allowedScripts": ["%s", "%s"] }`, script1Hash, script2Hash) writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) From 336d3e56fa20f5aae0cdcbbd29592481b06bce96 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Wed, 4 Mar 2026 00:31:36 +0000 Subject: [PATCH 07/16] adding nil check for filepath --- .../extensionpolicysettings.go | 14 ++++++++++++-- .../extensionpolicysettings_test.go | 18 +++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index 5888404..bc4acf3 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -22,14 +22,24 @@ type ExtensionPolicySettingsManager[T ExtensionPolicySettings] struct { settings *T } -func NewExtensionPolicySettingsManager[T ExtensionPolicySettings](policyFilePath string, logger logging.ILogger) *ExtensionPolicySettingsManager[T] { +func NewExtensionPolicySettingsManager[T ExtensionPolicySettings](policyFilePath string, logger logging.ILogger) (*ExtensionPolicySettingsManager[T], error) { + if policyFilePath == "" { + logger.Error("Policy file path is empty. ExtensionPolicySettingsManager may not function correctly.") + return nil, fmt.Errorf("policy file path cannot be empty") + } return &ExtensionPolicySettingsManager[T]{ settingsFilePath: policyFilePath, logger: logger, // settings is not loaded until LoadExtensionPolicySettings is called - } + }, nil } func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() error { + if (epsm == nil) || (epsm.logger == nil) { + return fmt.Errorf("invalid ExtensionPolicySettingsManager: manager or logger is nil") + } + if epsm.settingsFilePath == "" { + return fmt.Errorf("invalid ExtensionPolicySettingsManager: settings file path is empty") + } epsm.logger.Info(fmt.Sprintf("Loading extension policy settings from file: %s", epsm.settingsFilePath)) // If an extension has a default policy configuration in case the file does not exist, they should handle that logic before calling this function. diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_test.go b/pkg/extensionpolicysettings/extensionpolicysettings_test.go index ee2267f..0226802 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_test.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_test.go @@ -32,7 +32,8 @@ func (tp TestPolicy) ValidateFormat() error { func TestNewExtensionPolicySettingsManager(t *testing.T) { // Create a new ExtensionPolicySettingsManager - manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + require.NoError(t, err) require.NotNil(t, manager) require.Equal(t, extensionRuntimePolicySettingsFilePath, manager.settingsFilePath) require.Equal(t, extensionLogger, manager.logger) @@ -41,7 +42,8 @@ func TestNewExtensionPolicySettingsManager(t *testing.T) { func TestLoadExtensionPolicySettings(t *testing.T) { // Setup test parameters - manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + require.NoError(t, err) // Test cases: // 1. Valid policy file: we should be able to load the settings without error @@ -53,7 +55,7 @@ func TestLoadExtensionPolicySettings(t *testing.T) { defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Call LoadExtensionPolicySettings and check for errors - err := manager.LoadExtensionPolicySettings() + err = manager.LoadExtensionPolicySettings() require.NoError(t, err) require.NotNil(t, manager.settings) require.Equal(t, "true", manager.settings.RequiresSigning) @@ -84,7 +86,8 @@ func TestLoadExtensionPolicySettings(t *testing.T) { func TestGetSettings(t *testing.T) { // Setup test parameters - manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + require.NoError(t, err) validPolicyContent := `{ "requireSigning": "true", "allowedScripts": [] @@ -93,7 +96,7 @@ func TestGetSettings(t *testing.T) { defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test // Call LoadExtensionPolicySettings and check for errors - err := manager.LoadExtensionPolicySettings() + err = manager.LoadExtensionPolicySettings() require.NoError(t, err) require.NotNil(t, manager.settings) require.Equal(t, "true", manager.settings.RequiresSigning) @@ -105,7 +108,8 @@ func TestGetSettings(t *testing.T) { func TestValidateAgainstAllowlist(t *testing.T) { // Setup test parameters - manager := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + require.NoError(t, err) defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test script1Hash := hashHelper("./testutils/testscripts/script1.sh") @@ -120,7 +124,7 @@ func TestValidateAgainstAllowlist(t *testing.T) { writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) // Call LoadExtensionPolicySettings and check for errors - err := manager.LoadExtensionPolicySettings() + err = manager.LoadExtensionPolicySettings() require.NoError(t, err) require.NotNil(t, manager.settings) require.Equal(t, "true", manager.settings.RequiresSigning) From 160ebdf80e1309855272a278ab3bf37fc44fccb2 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Wed, 4 Mar 2026 00:35:41 +0000 Subject: [PATCH 08/16] commenting out error line --- pkg/extensionpolicysettings/extensionpolicysettings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index bc4acf3..e6a30cf 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -40,7 +40,7 @@ func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() err if epsm.settingsFilePath == "" { return fmt.Errorf("invalid ExtensionPolicySettingsManager: settings file path is empty") } - epsm.logger.Info(fmt.Sprintf("Loading extension policy settings from file: %s", epsm.settingsFilePath)) + //epsm.logger.Info(fmt.Sprintf("Loading extension policy settings from file: %s", epsm.settingsFilePath)) // If an extension has a default policy configuration in case the file does not exist, they should handle that logic before calling this function. if _, err := os.Stat(epsm.settingsFilePath); os.IsNotExist(err) { From 0cf99cda38d77133d30eb3d6be053b1b2f0d7155 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Wed, 4 Mar 2026 00:40:38 +0000 Subject: [PATCH 09/16] nit --- pkg/extensionpolicysettings/extensionpolicysettings.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index e6a30cf..0eb9496 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -71,13 +71,13 @@ func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() err } epsm.settings = settings - epsm.logger.Info("Extension policy settings loaded and validated successfully.") + //epsm.logger.Info("Extension policy settings loaded and validated successfully.") return nil } func (epsm *ExtensionPolicySettingsManager[T]) GetSettings() *T { if epsm.settings == nil { - epsm.logger.Info("Extension policy settings have not been loaded yet. Returning nil.") + //epsm.logger.Info("Extension policy settings have not been loaded yet. Returning nil.") } return epsm.settings } From 14a903b50ad134f4494330bed7c500363419a77f Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Wed, 4 Mar 2026 01:03:40 +0000 Subject: [PATCH 10/16] nit --- pkg/extensionpolicysettings/extensionpolicysettings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index 0eb9496..d7ea641 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -67,7 +67,7 @@ func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() err // Extensions themselves must decide the criteria for valid policy settings (i.e., if they can be null etc.). if err := (*settings).ValidateFormat(); err != nil { - return fmt.Errorf("extension policy settings validation failed: %w", err) + return fmt.Errorf("extension policy invalid: %w", err) } epsm.settings = settings From 17aecbaff2336a8b8921b726aaca9f7c98534734 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Wed, 4 Mar 2026 19:33:58 +0000 Subject: [PATCH 11/16] removing logging rn for testing purposes --- .../extensionpolicysettings.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index d7ea641..1ecd90d 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -24,7 +24,7 @@ type ExtensionPolicySettingsManager[T ExtensionPolicySettings] struct { func NewExtensionPolicySettingsManager[T ExtensionPolicySettings](policyFilePath string, logger logging.ILogger) (*ExtensionPolicySettingsManager[T], error) { if policyFilePath == "" { - logger.Error("Policy file path is empty. ExtensionPolicySettingsManager may not function correctly.") + //logger.Error("Policy file path is empty. ExtensionPolicySettingsManager may not function correctly.") return nil, fmt.Errorf("policy file path cannot be empty") } return &ExtensionPolicySettingsManager[T]{ @@ -98,14 +98,18 @@ func ValidateValueInAllowlist(logger logging.ILogger, value string, allowlist [] for _, allowlistValue := range allowlist { if value == allowlistValue { - logger.Info("Validation successful: item is in the allowlist.") + //logger.Info("Validation successful: item is in the allowlist.") return nil } } - logger.Info("validation failed: item is not in the allowlist.") + //logger.Info("validation failed: item is not in the allowlist.") return extensionerrors.ErrItemNotInAllowlist } +// This function is the entry point for most use cases: it takes in the filepath, reads the content, and +// determines if the content is allowlisted. If hashOpt is not HashTypeNone, it will compute the hash of the file content. +// If extensions don't want to validate a filepath but a value directly, they can call ValidateValueInAllowlist, +// which this function calls. func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowlist []string, hashOpt HashType) error { if len(allowlist) == 0 { return extensionerrors.ErrPolicyAllowlistEmpty @@ -123,12 +127,12 @@ func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowl value := string(content) // What if content is empty? Do we want to treat that as an error or just compute the hash of an empty string? For now, we'll compute the hash of an empty string, but this is something to consider based on the specific use case and security requirements. if hashOpt != HashTypeNone { - logger.Info(fmt.Sprintf("Computing hash of file %s for validation.", filePath)) + //logger.Info(fmt.Sprintf("Computing hash of file %s for validation.", filePath)) value, err := ComputeFileHash(logger, value, hashOpt) if err != nil { return fmt.Errorf("failed to compute hash for file %s for validation: %w", filePath, err) } - logger.Info(fmt.Sprintf("Computed hash value for file %s: %s", filePath, value)) + //logger.Info(fmt.Sprintf("Computed hash value for file %s: %s", filePath, value)) return ValidateValueInAllowlist(logger, value, allowlist) } @@ -137,7 +141,7 @@ func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowl // ComputeFileHash computes the hash of a file or leaves string as is. func ComputeFileHash(logger logging.ILogger, contents string, hashOpt HashType) (string, error) { - logger.Info("Computing hash for file contents") + //logger.Info("Computing hash for file contents") if contents == "" { return "", extensionerrors.ErrContentsToValidateEmpty @@ -153,6 +157,6 @@ func ComputeFileHash(logger logging.ILogger, contents string, hashOpt HashType) hashStr = hex.EncodeToString(hash[:]) } - logger.Info(fmt.Sprintf("Computed hash: %s", hashStr)) + //logger.Info(fmt.Sprintf("Computed hash: %s", hashStr)) return hashStr, nil } From a56cc80a83804a897096b6607d28990e6fa79099 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Thu, 5 Mar 2026 00:50:20 +0000 Subject: [PATCH 12/16] beginning to clean up --- pkg/extensionerrors/extensionerrors.go | 8 +-- .../extensionpolicysettings.go | 14 ++--- .../extensionpolicysettings_test.go | 62 ++++++++++++++----- .../testutils/testscripts/script4.sh | 0 .../testutils/testscripts/script5.sh | 3 + 5 files changed, 56 insertions(+), 31 deletions(-) create mode 100644 pkg/extensionpolicysettings/testutils/testscripts/script4.sh create mode 100644 pkg/extensionpolicysettings/testutils/testscripts/script5.sh diff --git a/pkg/extensionerrors/extensionerrors.go b/pkg/extensionerrors/extensionerrors.go index ed6ad0c..b946c2c 100644 --- a/pkg/extensionerrors/extensionerrors.go +++ b/pkg/extensionerrors/extensionerrors.go @@ -46,7 +46,7 @@ var ( ErrInvalidOperationName = errors.New("operation name is invalid") ErrMissingPolicyFile = errors.New("policy file is missing") - + ErrInvalidPolicyFile = errors.New("policy file is invalid") ErrEmptyPolicyFile = errors.New("policy file is empty") @@ -55,13 +55,11 @@ var ( ErrPolicyValidationFailed = errors.New("policy validation failed") - ErrPolicyAllowlistEmpty = errors.New("policy allowlist is empty") + ErrPolicyAllowlistEmpty = errors.New("File is not in allowlist because the allowlist is empty") ErrItemNotInAllowlist = errors.New("item is not in the allowlist") - ErrEmptyFilepathToValidate = errors.New("filepath to validate file cannot be empty") + ErrEmptyFilepathToValidate = errors.New("filepath of the file to validate cannot be empty") ErrFailedToReadFileToValidate = errors.New("failed to read file to validate") - - ErrContentsToValidateEmpty = errors.New("contents to validate cannot be empty") ) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index 1ecd90d..7baf969 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -119,6 +119,10 @@ func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowl return extensionerrors.ErrEmptyFilepathToValidate } + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return fmt.Errorf("file to validate does not exist: %w", err) + } + content, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("failed to read file %s for validation: %w", filePath, err) @@ -127,12 +131,10 @@ func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowl value := string(content) // What if content is empty? Do we want to treat that as an error or just compute the hash of an empty string? For now, we'll compute the hash of an empty string, but this is something to consider based on the specific use case and security requirements. if hashOpt != HashTypeNone { - //logger.Info(fmt.Sprintf("Computing hash of file %s for validation.", filePath)) value, err := ComputeFileHash(logger, value, hashOpt) if err != nil { - return fmt.Errorf("failed to compute hash for file %s for validation: %w", filePath, err) + return fmt.Errorf("error occured when hashing contents of file %s for validation: %w", filePath, err) } - //logger.Info(fmt.Sprintf("Computed hash value for file %s: %s", filePath, value)) return ValidateValueInAllowlist(logger, value, allowlist) } @@ -141,12 +143,6 @@ func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowl // ComputeFileHash computes the hash of a file or leaves string as is. func ComputeFileHash(logger logging.ILogger, contents string, hashOpt HashType) (string, error) { - //logger.Info("Computing hash for file contents") - - if contents == "" { - return "", extensionerrors.ErrContentsToValidateEmpty - } - var hashStr string switch hashOpt { case HashTypeSHA1: diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_test.go b/pkg/extensionpolicysettings/extensionpolicysettings_test.go index 0226802..195657f 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_test.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_test.go @@ -4,6 +4,7 @@ package extensionpolicysettings import ( + "crypto/sha1" "crypto/sha256" "encoding/hex" "fmt" @@ -92,7 +93,7 @@ func TestGetSettings(t *testing.T) { "requireSigning": "true", "allowedScripts": [] }` - writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) + require.NoError(t, writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent)) defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test // Call LoadExtensionPolicySettings and check for errors @@ -112,16 +113,21 @@ func TestValidateAgainstAllowlist(t *testing.T) { require.NoError(t, err) defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test - script1Hash := hashHelper("./testutils/testscripts/script1.sh") - script2Hash := hashHelper("./testutils/testscripts/script2.sh") - //script3Hash := hashHelper("./testutils/testscripts/script3.sh") + script1Hash, err := hashHelper("./testutils/testscripts/script1.sh", TestHashTypeSha256) + require.NoError(t, err) + script2Hash, err := hashHelper("./testutils/testscripts/script2.sh", TestHashTypeSha256) + require.NoError(t, err) + // Skip computing script3 hash because it will not be allowed.. + script4Hash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // pre-computed hash of the empty string + script5Hash, err := hashHelper("./testutils/testscripts/script5.sh", TestHashTypeSha1) + require.NoError(t, err) // Some scripts are allowed validPolicyContent := fmt.Sprintf(`{ "requireSigning": "true", - "allowedScripts": ["%s", "%s"] - }`, script1Hash, script2Hash) - writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent) + "allowedScripts": ["%s", "%s", "%s", "%s"] + }`, script1Hash, script2Hash, script4Hash, script5Hash) + require.NoError(t, writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent)) // Call LoadExtensionPolicySettings and check for errors err = manager.LoadExtensionPolicySettings() @@ -133,18 +139,24 @@ func TestValidateAgainstAllowlist(t *testing.T) { require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script1.sh", manager.settings.AllowedScripts, HashTypeSHA256)) require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script2.sh", manager.settings.AllowedScripts, HashTypeSHA256)) require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script3.sh", manager.settings.AllowedScripts, HashTypeSHA256), extensionerrors.ErrItemNotInAllowlist) + require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script5.sh", manager.settings.AllowedScripts, HashTypeSHA1)) + + // Empty filepath require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "", manager.settings.AllowedScripts, HashTypeSHA256), extensionerrors.ErrEmptyFilepathToValidate) + // Missing file require.Error(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/missing.sh", manager.settings.AllowedScripts, HashTypeSHA256)) - // Now, empty list. require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script1.sh", []string{}, HashTypeSHA256), extensionerrors.ErrPolicyAllowlistEmpty) + // Empty file + require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script4.sh", manager.settings.AllowedScripts, HashTypeSHA256)) + } -func writeToFile(filePath, content string) { +// Helper functions for tests + +func writeToFile(filePath, content string) error { err := os.WriteFile(filePath, []byte(content), 0644) - if err != nil { - panic(err) - } + return err } func cleanupFile(path string) { @@ -154,14 +166,30 @@ func cleanupFile(path string) { } } -func hashHelper(filePath string) string { +type TestHashType int + +const ( + TestHashTypeSha1 TestHashType = iota + TestHashTypeSha256 +) + +func hashHelper(filePath string, hashOpt TestHashType) (string, error) { contents, err := os.ReadFile(filePath) if err != nil { - panic(err) + return "", err } - hash := sha256.Sum256([]byte(contents)) - hashStr := hex.EncodeToString(hash[:]) - return hashStr + var hashStr string + switch hashOpt { + case TestHashTypeSha1: + hash := sha1.New() + hash.Write(contents) + hashStr = hex.EncodeToString(hash.Sum(nil)) + case TestHashTypeSha256: + hash := sha256.New() + hash.Write(contents) + hashStr = hex.EncodeToString(hash.Sum(nil)) + } + return hashStr, nil } diff --git a/pkg/extensionpolicysettings/testutils/testscripts/script4.sh b/pkg/extensionpolicysettings/testutils/testscripts/script4.sh new file mode 100644 index 0000000..e69de29 diff --git a/pkg/extensionpolicysettings/testutils/testscripts/script5.sh b/pkg/extensionpolicysettings/testutils/testscripts/script5.sh new file mode 100644 index 0000000..b73c937 --- /dev/null +++ b/pkg/extensionpolicysettings/testutils/testscripts/script5.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# This is a simple shell script +echo "Hello, World! I am script5.sh. I will be hashed in SHA1" \ No newline at end of file From 4828fb38d7970114fc5872966938b4e0204b2440 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Thu, 5 Mar 2026 21:43:20 +0000 Subject: [PATCH 13/16] library code for compute hash, allowlist validation, loading policy settings + UTs --- pkg/extensionerrors/extensionerrors.go | 6 +- .../extensionpolicysettings.go | 45 +++----- .../extensionpolicysettings_test.go | 41 +++---- pkg/internal/cert/cert_windows.go | 104 ------------------ 4 files changed, 43 insertions(+), 153 deletions(-) delete mode 100644 pkg/internal/cert/cert_windows.go diff --git a/pkg/extensionerrors/extensionerrors.go b/pkg/extensionerrors/extensionerrors.go index b946c2c..7465096 100644 --- a/pkg/extensionerrors/extensionerrors.go +++ b/pkg/extensionerrors/extensionerrors.go @@ -51,11 +51,15 @@ var ( ErrEmptyPolicyFile = errors.New("policy file is empty") + ErrEmptyPolicyFilePath = errors.New("the path to the policy file cannot be empty") + ErrFailedToUnmarshalPolicyFile = errors.New("failed to unmarshal policy file") + ErrPolicyNotYetLoaded = errors.New("policy settings have not yet been loaded") + ErrPolicyValidationFailed = errors.New("policy validation failed") - ErrPolicyAllowlistEmpty = errors.New("File is not in allowlist because the allowlist is empty") + ErrPolicyAllowlistEmpty = errors.New("file is not in allowlist because the allowlist is empty") ErrItemNotInAllowlist = errors.New("item is not in the allowlist") diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index 7baf969..4e7eeea 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -9,7 +9,6 @@ import ( "os" "github.com/Azure/azure-extension-platform/pkg/extensionerrors" - "github.com/Azure/azure-extension-platform/pkg/logging" ) type ExtensionPolicySettings interface { @@ -18,29 +17,25 @@ type ExtensionPolicySettings interface { type ExtensionPolicySettingsManager[T ExtensionPolicySettings] struct { settingsFilePath string - logger logging.ILogger settings *T } -func NewExtensionPolicySettingsManager[T ExtensionPolicySettings](policyFilePath string, logger logging.ILogger) (*ExtensionPolicySettingsManager[T], error) { +func NewExtensionPolicySettingsManager[T ExtensionPolicySettings](policyFilePath string) (*ExtensionPolicySettingsManager[T], error) { if policyFilePath == "" { - //logger.Error("Policy file path is empty. ExtensionPolicySettingsManager may not function correctly.") - return nil, fmt.Errorf("policy file path cannot be empty") + return nil, extensionerrors.ErrEmptyPolicyFilePath } return &ExtensionPolicySettingsManager[T]{ settingsFilePath: policyFilePath, - logger: logger, // settings is not loaded until LoadExtensionPolicySettings is called }, nil } func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() error { - if (epsm == nil) || (epsm.logger == nil) { - return fmt.Errorf("invalid ExtensionPolicySettingsManager: manager or logger is nil") + if epsm == nil { + return fmt.Errorf("invalid ExtensionPolicySettingsManager: manager is nil") } if epsm.settingsFilePath == "" { - return fmt.Errorf("invalid ExtensionPolicySettingsManager: settings file path is empty") + return extensionerrors.ErrEmptyPolicyFilePath } - //epsm.logger.Info(fmt.Sprintf("Loading extension policy settings from file: %s", epsm.settingsFilePath)) // If an extension has a default policy configuration in case the file does not exist, they should handle that logic before calling this function. if _, err := os.Stat(epsm.settingsFilePath); os.IsNotExist(err) { @@ -49,11 +44,9 @@ func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() err return fmt.Errorf("error checking extension policy settings file: %w", err) } - // Read the file content: check: what if the file is locked? What if we don't have permissions to read? - fileContent, err := os.ReadFile(epsm.settingsFilePath) if err != nil { - return fmt.Errorf("failed to read extension policy settings file: %w", err) // Should we have retry logic? + return fmt.Errorf("failed to read extension policy settings file: %w", err) // TODO: Add retry logic if appropriate. } if len(fileContent) == 0 { @@ -67,19 +60,18 @@ func (epsm *ExtensionPolicySettingsManager[T]) LoadExtensionPolicySettings() err // Extensions themselves must decide the criteria for valid policy settings (i.e., if they can be null etc.). if err := (*settings).ValidateFormat(); err != nil { - return fmt.Errorf("extension policy invalid: %w", err) + return fmt.Errorf("extension policy loaded, but invalid format: %w", err) } epsm.settings = settings - //epsm.logger.Info("Extension policy settings loaded and validated successfully.") return nil } -func (epsm *ExtensionPolicySettingsManager[T]) GetSettings() *T { +func (epsm *ExtensionPolicySettingsManager[T]) GetSettings() (*T, error) { if epsm.settings == nil { - //epsm.logger.Info("Extension policy settings have not been loaded yet. Returning nil.") + return nil, extensionerrors.ErrPolicyNotYetLoaded } - return epsm.settings + return epsm.settings, nil } // Validation Helper Functions @@ -91,18 +83,16 @@ const ( HashTypeSHA256 ) -func ValidateValueInAllowlist(logger logging.ILogger, value string, allowlist []string) error { +func ValidateValueInAllowlist(value string, allowlist []string) error { if len(allowlist) == 0 { return extensionerrors.ErrPolicyAllowlistEmpty } for _, allowlistValue := range allowlist { if value == allowlistValue { - //logger.Info("Validation successful: item is in the allowlist.") return nil } } - //logger.Info("validation failed: item is not in the allowlist.") return extensionerrors.ErrItemNotInAllowlist } @@ -110,7 +100,7 @@ func ValidateValueInAllowlist(logger logging.ILogger, value string, allowlist [] // determines if the content is allowlisted. If hashOpt is not HashTypeNone, it will compute the hash of the file content. // If extensions don't want to validate a filepath but a value directly, they can call ValidateValueInAllowlist, // which this function calls. -func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowlist []string, hashOpt HashType) error { +func ValidateFileHashInAllowlist(filePath string, allowlist []string, hashOpt HashType) error { if len(allowlist) == 0 { return extensionerrors.ErrPolicyAllowlistEmpty } @@ -128,21 +118,21 @@ func ValidateFileHashInAllowlist(logger logging.ILogger, filePath string, allowl return fmt.Errorf("failed to read file %s for validation: %w", filePath, err) } - value := string(content) // What if content is empty? Do we want to treat that as an error or just compute the hash of an empty string? For now, we'll compute the hash of an empty string, but this is something to consider based on the specific use case and security requirements. + value := string(content) if hashOpt != HashTypeNone { - value, err := ComputeFileHash(logger, value, hashOpt) + value, err := ComputeFileHash(value, hashOpt) if err != nil { return fmt.Errorf("error occured when hashing contents of file %s for validation: %w", filePath, err) } - return ValidateValueInAllowlist(logger, value, allowlist) + return ValidateValueInAllowlist(value, allowlist) } - return ValidateValueInAllowlist(logger, value, allowlist) + return ValidateValueInAllowlist(value, allowlist) } // ComputeFileHash computes the hash of a file or leaves string as is. -func ComputeFileHash(logger logging.ILogger, contents string, hashOpt HashType) (string, error) { +func ComputeFileHash(contents string, hashOpt HashType) (string, error) { var hashStr string switch hashOpt { case HashTypeSHA1: @@ -153,6 +143,5 @@ func ComputeFileHash(logger logging.ILogger, contents string, hashOpt HashType) hashStr = hex.EncodeToString(hash[:]) } - //logger.Info(fmt.Sprintf("Computed hash: %s", hashStr)) return hashStr, nil } diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_test.go b/pkg/extensionpolicysettings/extensionpolicysettings_test.go index 195657f..836a6b3 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_test.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_test.go @@ -12,15 +12,13 @@ import ( "testing" "github.com/Azure/azure-extension-platform/pkg/extensionerrors" - "github.com/Azure/azure-extension-platform/pkg/logging" "github.com/stretchr/testify/require" ) -var extensionLogger = logging.New(nil) - const extensionRuntimePolicySettingsFilePath = "./testutils/runtime_policy.json" -// This is a sample struct for an example extension's policy settings. Each extension will define their own struct that implements the ExtensionPolicySettings interface according to their needs. +// This is a sample struct for an example extension's policy settings. +// Each extension will define their own struct that implements the ExtensionPolicySettings interface according to their needs. type TestPolicy struct { RequiresSigning string `json:"requireSigning"` AllowedScripts []string `json:"allowedScripts"` @@ -33,17 +31,16 @@ func (tp TestPolicy) ValidateFormat() error { func TestNewExtensionPolicySettingsManager(t *testing.T) { // Create a new ExtensionPolicySettingsManager - manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath) require.NoError(t, err) require.NotNil(t, manager) require.Equal(t, extensionRuntimePolicySettingsFilePath, manager.settingsFilePath) - require.Equal(t, extensionLogger, manager.logger) require.Nil(t, manager.settings) // settings should not be loaded until LoadExtensionPolicySettings is called } func TestLoadExtensionPolicySettings(t *testing.T) { // Setup test parameters - manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath) require.NoError(t, err) // Test cases: @@ -87,29 +84,34 @@ func TestLoadExtensionPolicySettings(t *testing.T) { func TestGetSettings(t *testing.T) { // Setup test parameters - manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath) require.NoError(t, err) validPolicyContent := `{ "requireSigning": "true", "allowedScripts": [] }` require.NoError(t, writeToFile(extensionRuntimePolicySettingsFilePath, validPolicyContent)) - defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test + defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Call LoadExtensionPolicySettings and check for errors + _, err = manager.GetSettings() + require.ErrorIs(t, err, extensionerrors.ErrPolicyNotYetLoaded) // should return an error because settings have not been loaded yet err = manager.LoadExtensionPolicySettings() require.NoError(t, err) require.NotNil(t, manager.settings) require.Equal(t, "true", manager.settings.RequiresSigning) // Call GetSettings and check for errors - settings := manager.GetSettings() + settings, err := manager.GetSettings() + require.NoError(t, err) require.NotNil(t, settings) + require.Equal(t, "true", settings.RequiresSigning) + require.Empty(t, settings.AllowedScripts) } func TestValidateAgainstAllowlist(t *testing.T) { // Setup test parameters - manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath, extensionLogger) + manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath) require.NoError(t, err) defer cleanupFile(extensionRuntimePolicySettingsFilePath) // Clean up after test @@ -136,19 +138,19 @@ func TestValidateAgainstAllowlist(t *testing.T) { require.Equal(t, "true", manager.settings.RequiresSigning) require.NotEmpty(t, manager.settings.AllowedScripts) - require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script1.sh", manager.settings.AllowedScripts, HashTypeSHA256)) - require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script2.sh", manager.settings.AllowedScripts, HashTypeSHA256)) - require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script3.sh", manager.settings.AllowedScripts, HashTypeSHA256), extensionerrors.ErrItemNotInAllowlist) - require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script5.sh", manager.settings.AllowedScripts, HashTypeSHA1)) + require.NoError(t, ValidateFileHashInAllowlist("./testutils/testscripts/script1.sh", manager.settings.AllowedScripts, HashTypeSHA256)) + require.NoError(t, ValidateFileHashInAllowlist("./testutils/testscripts/script2.sh", manager.settings.AllowedScripts, HashTypeSHA256)) + require.ErrorIs(t, ValidateFileHashInAllowlist("./testutils/testscripts/script3.sh", manager.settings.AllowedScripts, HashTypeSHA256), extensionerrors.ErrItemNotInAllowlist) + require.NoError(t, ValidateFileHashInAllowlist("./testutils/testscripts/script5.sh", manager.settings.AllowedScripts, HashTypeSHA1)) // Empty filepath - require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "", manager.settings.AllowedScripts, HashTypeSHA256), extensionerrors.ErrEmptyFilepathToValidate) + require.ErrorIs(t, ValidateFileHashInAllowlist("", manager.settings.AllowedScripts, HashTypeSHA256), extensionerrors.ErrEmptyFilepathToValidate) // Missing file - require.Error(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/missing.sh", manager.settings.AllowedScripts, HashTypeSHA256)) + require.Error(t, ValidateFileHashInAllowlist("./testutils/testscripts/missing.sh", manager.settings.AllowedScripts, HashTypeSHA256)) // Now, empty list. - require.ErrorIs(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script1.sh", []string{}, HashTypeSHA256), extensionerrors.ErrPolicyAllowlistEmpty) + require.ErrorIs(t, ValidateFileHashInAllowlist("./testutils/testscripts/script1.sh", []string{}, HashTypeSHA256), extensionerrors.ErrPolicyAllowlistEmpty) // Empty file - require.NoError(t, ValidateFileHashInAllowlist(manager.logger, "./testutils/testscripts/script4.sh", manager.settings.AllowedScripts, HashTypeSHA256)) + require.NoError(t, ValidateFileHashInAllowlist("./testutils/testscripts/script4.sh", manager.settings.AllowedScripts, HashTypeSHA256)) } @@ -160,7 +162,6 @@ func writeToFile(filePath, content string) error { } func cleanupFile(path string) { - // Do not remove missingPolicyFilePath as it simulates a missing file if _, err := os.Stat(path); err == nil { os.Remove(path) } diff --git a/pkg/internal/cert/cert_windows.go b/pkg/internal/cert/cert_windows.go deleted file mode 100644 index 8a06a15..0000000 --- a/pkg/internal/cert/cert_windows.go +++ /dev/null @@ -1,104 +0,0 @@ -package cert - -import ( - "syscall" -) - -const ( - WINTRUST_ACTION_GENERIC_VERIFY_V2_GUID = "00AAC56B-CD44-11d0-8CC2-00C04FC295EE" -) - -var ( - Modwintrust = syscall.NewLazyDLL("wintrust.dll") - procWinVerifyTrust = Modwintrust.NewProc("WinVerifyTrust") -) - -type WTD_UI uint - -const ( - WTD_UI_ALL WTD_UI = 1 - WTD_UI_NONE WTD_UI = 2 - WTD_UI_NOBAD WTD_UI = 3 - WTD_UI_NOGOOD WTD_UI = 4 -) - -type WTD_REVOKE_FLAGS uint - -const ( - WTD_REVOKE_NONE WTD_REVOKE_FLAGS = 0x00000000 - WTD_REVOKE_WHOLECHAIN WTD_REVOKE_FLAGS = 0x00000001 -) - -type WTD_CHOICE uint - -const ( - WTD_CHOICE_FILE WTD_CHOICE = 1 - WTD_CHOICE_CATALOG WTD_CHOICE = 2 - WTD_CHOICE_BLOB WTD_CHOICE = 3 - WTD_CHOICE_SIGNER WTD_CHOICE = 4 - WTD_CHOICE_CERT WTD_CHOICE = 5 -) - -type WTD_STATE_ACTION uint - -const ( - WTD_STATEACTION_IGNORE WTD_STATE_ACTION = 0x00000000 - WTD_STATEACTION_VERIFY WTD_STATE_ACTION = 0x00000001 - WTD_STATEACTION_CLOSE WTD_STATE_ACTION = 0x00000002 - WTD_STATEACTION_AUTO_CACHE WTD_STATE_ACTION = 0x00000003 - WTD_STATEACTION_AUTO_CACHE_FLUSH WTD_STATE_ACTION = 0x00000004 -) - -type WTD_PROVIDER_FLAGS uint - -const ( - WTD_PROV_FLAGS_MASK = 0x0000FFFF - WTD_USE_IE4_TRUST_FLAG WTD_PROVIDER_FLAGS = 0x1 - WTD_NO_IE4_CHAIN_FLAG WTD_PROVIDER_FLAGS = 0x2 - WTD_NO_POLICY_USAGE_FLAG WTD_PROVIDER_FLAGS = 0x4 - WTD_REVOCATION_CHECK_NONE WTD_PROVIDER_FLAGS = 0x10 - WTD_REVOCATION_CHECK_END_CERT WTD_PROVIDER_FLAGS = 0x20 - WTD_REVOCATION_CHECK_CHAIN WTD_PROVIDER_FLAGS = 0x40 - WTD_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT WTD_PROVIDER_FLAGS = 0x80 - WTD_SAFER_FLAG WTD_PROVIDER_FLAGS = 0x100 - WTD_HASH_ONLY_FLAG WTD_PROVIDER_FLAGS = 0x200 - WTD_USE_DEFAULT_OSVER_CHECK WTD_PROVIDER_FLAGS = 0x400 - WTD_LIFETIME_SIGNING_FLAG WTD_PROVIDER_FLAGS = 0x800 -) - -type WTD_UICONTEXT uint - -const ( - WTD_UICONTEXT_EXECUTE WTD_UICONTEXT = 0 - WTD_UICONTEXT_INSTALL WTD_UICONTEXT = 1 -) - -type winTrustData struct { - cbStruct uint32 - pPolicyCallbackData uintptr - pSIPClientData uintptr - dwUIChoice WTD_UI - fdWRevocationChecks WTD_REVOKE_FLAGS - dwUnionChoice WTD_CHOICE - union [8]byte // This is a placeholder for the actual union data, which can be one of several types depending on dwUnionChoice - dwStateAction WTD_STATE_ACTION - hWVTStateData uintptr - pwszURLReference uintptr - dwProvFlags WTD_PROVIDER_FLAGS - dwUIContext WTD_UICONTEXT -} - -type winTrustFileInfo struct { - cbStruct uint32 - pcwszFile uintptr - hFile syscall.Handle - pgKnownSubject uintptr -} - -type winTrustCatalogInfo struct { - cbStruct uint32 - pcwszCatalogFile uintptr - pcwszMemberTag uintptr - hMemberFile syscall.Handle - pgKnownSubject uintptr -} From d471ed662908a55196901025a6830c7dd87c7715 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Fri, 6 Mar 2026 21:16:47 +0000 Subject: [PATCH 14/16] del later files --- .../extensionpolicysettings_linux.go | 31 ------------------- .../extensionpolicysettings_windows.go | 1 - 2 files changed, 32 deletions(-) delete mode 100644 pkg/extensionpolicysettings/extensionpolicysettings_linux.go delete mode 100644 pkg/extensionpolicysettings/extensionpolicysettings_windows.go diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_linux.go b/pkg/extensionpolicysettings/extensionpolicysettings_linux.go deleted file mode 100644 index 13f8687..0000000 --- a/pkg/extensionpolicysettings/extensionpolicysettings_linux.go +++ /dev/null @@ -1,31 +0,0 @@ -package extensionpolicysettings - -import ( - "bytes" - "fmt" - "os/exec" -) - -// declare function (obviously) -// write the passed-in signature to a file -// get the cert location (assuming cert gonna be passed in ) - -//create base command for open ssl - -// check and see if c groups are enabled; if so , scope the command using systemd just in case -// if it fails, then a) disable c groups and b)just run the comman directly. - -func ValidateFileSignature(filePath string, signature []byte, certPath string) (isValid bool, err error) { - // os.eexec - cmd := exec.Command("openssl", "cms", "-verify", "-content", filePath, "-certfile", certPath, "-signature", "-") - - cmd.Stdin = bytes.NewReader(signature) - var bOut, bErr bytes.Buffer - cmd.Stdout = &bOut - cmd.Stderr = &bErr - - if err := cmd.Run(); err != nil { - return false, fmt.Errorf("signature validation failed: error=%v stderr=%s", err, string(bErr.Bytes())) - } - return true, nil -} diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_windows.go b/pkg/extensionpolicysettings/extensionpolicysettings_windows.go deleted file mode 100644 index 8d62619..0000000 --- a/pkg/extensionpolicysettings/extensionpolicysettings_windows.go +++ /dev/null @@ -1 +0,0 @@ -package extensionpolicysettings From d83d87b4b1b8e5cccaa50071495092d29bd859f0 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Thu, 12 Mar 2026 21:15:16 +0000 Subject: [PATCH 15/16] adding UTs + case insensitive search --- .../extensionpolicysettings.go | 6 +- .../extensionpolicysettings_test.go | 78 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings.go b/pkg/extensionpolicysettings/extensionpolicysettings.go index 4e7eeea..b33982f 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/Azure/azure-extension-platform/pkg/extensionerrors" ) @@ -89,7 +90,10 @@ func ValidateValueInAllowlist(value string, allowlist []string) error { } for _, allowlistValue := range allowlist { - if value == allowlistValue { + // Although a hash value wouldn't have whitespace we trim spaces for other use cases of this function. + trimmedAllowlistValue := strings.TrimSpace(allowlistValue) + trimmedValue := strings.TrimSpace(value) + if strings.EqualFold(trimmedValue, trimmedAllowlistValue) { return nil } } diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_test.go b/pkg/extensionpolicysettings/extensionpolicysettings_test.go index 836a6b3..bc671c7 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_test.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_test.go @@ -29,6 +29,27 @@ func (tp TestPolicy) ValidateFormat() error { return nil } +type InvalidFormatPolicy struct { + Value string `json:"value"` +} + +func (p InvalidFormatPolicy) ValidateFormat() error { + return fmt.Errorf("invalid format for test") +} + +func TestNewExtensionPolicySettingsManager_EmptyPath(t *testing.T) { + manager, err := NewExtensionPolicySettingsManager[TestPolicy]("") + require.ErrorIs(t, err, extensionerrors.ErrEmptyPolicyFilePath) + require.Nil(t, manager) +} + +func TestLoadExtensionPolicySettings_NilManager(t *testing.T) { + var manager *ExtensionPolicySettingsManager[TestPolicy] + err := manager.LoadExtensionPolicySettings() + require.Error(t, err) + require.Contains(t, err.Error(), "manager is nil") +} + func TestNewExtensionPolicySettingsManager(t *testing.T) { // Create a new ExtensionPolicySettingsManager manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath) @@ -38,6 +59,25 @@ func TestNewExtensionPolicySettingsManager(t *testing.T) { require.Nil(t, manager.settings) // settings should not be loaded until LoadExtensionPolicySettings is called } +func TestLoadExtensionPolicySettings_EmptyManagerPath(t *testing.T) { // lourdes: go back and understand the difference between this test and the test above + manager := &ExtensionPolicySettingsManager[TestPolicy]{} + err := manager.LoadExtensionPolicySettings() + require.ErrorIs(t, err, extensionerrors.ErrEmptyPolicyFilePath) +} + +func TestLoadExtensionPolicySettings_ValidateFormatFailure(t *testing.T) { + manager, err := NewExtensionPolicySettingsManager[InvalidFormatPolicy](extensionRuntimePolicySettingsFilePath) + require.NoError(t, err) + + validJSONButInvalidFormat := `{"value":"anything"}` + require.NoError(t, writeToFile(extensionRuntimePolicySettingsFilePath, validJSONButInvalidFormat)) + defer cleanupFile(extensionRuntimePolicySettingsFilePath) + + err = manager.LoadExtensionPolicySettings() + require.Error(t, err) + require.Contains(t, err.Error(), "invalid format") +} + func TestLoadExtensionPolicySettings(t *testing.T) { // Setup test parameters manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath) @@ -109,6 +149,12 @@ func TestGetSettings(t *testing.T) { require.Empty(t, settings.AllowedScripts) } +func TestValidateValueInAllowlist(t *testing.T) { + require.ErrorIs(t, ValidateValueInAllowlist("x", []string{}), extensionerrors.ErrPolicyAllowlistEmpty) + require.NoError(t, ValidateValueInAllowlist("b", []string{"a", "b", "c"})) + require.ErrorIs(t, ValidateValueInAllowlist("z", []string{"a", "b", "c"}), extensionerrors.ErrItemNotInAllowlist) +} + func TestValidateAgainstAllowlist(t *testing.T) { // Setup test parameters manager, err := NewExtensionPolicySettingsManager[TestPolicy](extensionRuntimePolicySettingsFilePath) @@ -154,8 +200,38 @@ func TestValidateAgainstAllowlist(t *testing.T) { } -// Helper functions for tests +func TestValidateFileHashInAllowlist_HashTypeNone_UsesRawContent(t *testing.T) { + filePath := "./testutils/raw_content_test.txt" + content := "raw-content-123" + + require.NoError(t, writeToFile(filePath, content)) + defer cleanupFile(filePath) + + require.NoError(t, ValidateFileHashInAllowlist(filePath, []string{content}, HashTypeNone)) + require.ErrorIs(t, ValidateFileHashInAllowlist(filePath, []string{"different-content"}, HashTypeNone), extensionerrors.ErrItemNotInAllowlist) +} + +func TestComputeFileHash(t *testing.T) { + input := "abc" + + sha1Expected := sha1.Sum([]byte(input)) + sha256Expected := sha256.Sum256([]byte(input)) + gotSHA1, err := ComputeFileHash(input, HashTypeSHA1) + require.NoError(t, err) + require.Equal(t, hex.EncodeToString(sha1Expected[:]), gotSHA1) + + gotSHA256, err := ComputeFileHash(input, HashTypeSHA256) + require.NoError(t, err) + require.Equal(t, hex.EncodeToString(sha256Expected[:]), gotSHA256) + + // Current behavior: unknown hash type falls back to SHA256. + gotUnknown, err := ComputeFileHash(input, HashType(999)) + require.NoError(t, err) + require.Equal(t, hex.EncodeToString(sha256Expected[:]), gotUnknown) +} + +// Helper functions for tests func writeToFile(filePath, content string) error { err := os.WriteFile(filePath, []byte(content), 0644) return err From 25c233b48d57209f78cc985fd5223e8f2bcfded1 Mon Sep 17 00:00:00 2001 From: alsanmsft Date: Thu, 12 Mar 2026 21:19:16 +0000 Subject: [PATCH 16/16] del personal comment --- pkg/extensionpolicysettings/extensionpolicysettings_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/extensionpolicysettings/extensionpolicysettings_test.go b/pkg/extensionpolicysettings/extensionpolicysettings_test.go index bc671c7..0f556da 100644 --- a/pkg/extensionpolicysettings/extensionpolicysettings_test.go +++ b/pkg/extensionpolicysettings/extensionpolicysettings_test.go @@ -59,7 +59,7 @@ func TestNewExtensionPolicySettingsManager(t *testing.T) { require.Nil(t, manager.settings) // settings should not be loaded until LoadExtensionPolicySettings is called } -func TestLoadExtensionPolicySettings_EmptyManagerPath(t *testing.T) { // lourdes: go back and understand the difference between this test and the test above +func TestLoadExtensionPolicySettings_EmptyManagerPath(t *testing.T) { manager := &ExtensionPolicySettingsManager[TestPolicy]{} err := manager.LoadExtensionPolicySettings() require.ErrorIs(t, err, extensionerrors.ErrEmptyPolicyFilePath)