From 3b1a531c481d9858d5a9721e15bb28e59d85c113 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 29 Jun 2025 15:44:55 -0700 Subject: [PATCH 1/3] =?UTF-8?q?feat(configuration):=20=E2=9C=A8=20enhance?= =?UTF-8?q?=20logging=20configuration=20and=20update=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added detailed logging configuration options in `Configuration.psd1`. * Updated `Import-GatekeeperConfig.ps1` to utilize cached configuration and parse logging settings. * Enhanced `Test-FeatureFlag.ps1` to execute logging scripts based on rule outcomes. * Expanded README with configuration details and logging behavior explanations. --- Gatekeeper/Configuration.psd1 | 40 ++++++++++- Gatekeeper/Public/Get-PropertySet.ps1 | 17 +++++ Gatekeeper/Public/Import-GatekeeperConfig.ps1 | 35 ++++++++- Gatekeeper/Public/New-Condition.ps1 | 1 - Gatekeeper/Public/Test-FeatureFlag.ps1 | 9 ++- README.md | 71 ++++++++++++++++--- 6 files changed, 154 insertions(+), 19 deletions(-) diff --git a/Gatekeeper/Configuration.psd1 b/Gatekeeper/Configuration.psd1 index 3a2c84a..32ef366 100644 --- a/Gatekeeper/Configuration.psd1 +++ b/Gatekeeper/Configuration.psd1 @@ -1,8 +1,42 @@ +# Configuration settings for the Gatekeeper module @{ - # Configuration settings for the Gatekeeper module - ModuleName = 'Gatekeeper' - ModuleVersion = '0.1.0' + # This is the version of the configuration, this will + # allow safe upgrades in the future. + # It is not the version of the module itself. + Version = '0.1.0' FilePaths = @{ Schemas = "$PSScriptRoot\Schemas" } + Logging = @{ + Allow = @{ + # We leave this disabled by default to avoid cluttering the console + Enabled = $false + Script = { + param($Rule) + Write-Host "✅ Rule [$($Rule.Name)] matched and is allowed." + } + } + Deny = @{ + # We leave this disabled by default to avoid cluttering the console + Enabled = $false + Script = { + param($Rule) + Write-Host "⛔ Rule [$($Rule.Name)] matched and is denied." + } + } + Warning = @{ + Enabled = $true + Script = { + param($Rule) + Write-Warning "⚠️ Rule [$($Rule.Name)] matched." + } + } + Audit = @{ + Enabled = $true + Script = { + param($Rule) + Write-Host "Audit: $($Rule.Name)" + } + } + } } diff --git a/Gatekeeper/Public/Get-PropertySet.ps1 b/Gatekeeper/Public/Get-PropertySet.ps1 index f3db89d..8a44c29 100644 --- a/Gatekeeper/Public/Get-PropertySet.ps1 +++ b/Gatekeeper/Public/Get-PropertySet.ps1 @@ -1,4 +1,21 @@ function Get-PropertySet { + <# + .SYNOPSIS + Retrieve property sets from the Gatekeeper configuration. + + .DESCRIPTION + This function retrieves property sets from the Gatekeeper configuration. + It can return all property sets or a specific one by name. Property sets are + stored in a cache to avoid multiple reads from disk, improving performance. + + .PARAMETER Name + The name of the property set to retrieve. + If not specified, all property sets will be returned. + .EXAMPLE + $propertySet = Get-PropertySet -Name 'MyPropertySet' + + This retrieves the property set with the name 'MyPropertySet'. + #> param ( [Parameter()] [string] diff --git a/Gatekeeper/Public/Import-GatekeeperConfig.ps1 b/Gatekeeper/Public/Import-GatekeeperConfig.ps1 index 809d38f..0cc53de 100644 --- a/Gatekeeper/Public/Import-GatekeeperConfig.ps1 +++ b/Gatekeeper/Public/Import-GatekeeperConfig.ps1 @@ -16,10 +16,43 @@ function Import-GatekeeperConfig { } } process { - $script:GatekeeperConfiguration = Import-Configuration + if ($script:GatekeeperConfiguration) { + Write-Verbose "Using cached Gatekeeper configuration." + } else { + Write-Verbose "Loading Gatekeeper configuration from disk." + $script:GatekeeperConfiguration = Import-Configuration + } + # Check if the configuration was imported successfully if (-not $script:GatekeeperConfiguration) { throw "Failed to import Gatekeeper configuration." } + + #region Parse the logging configuration + if ($script:GatekeeperConfiguration.Logging) { + Write-Verbose "Parsing logging configuration." + $script:GatekeeperLogging = @{} + foreach ($level in $script:GatekeeperConfiguration.Logging.Keys) { + if (-not $script:GatekeeperConfiguration.Logging[$level].Enabled) { + Write-Verbose "Logging level '$level' is disabled, skipping." + continue + } + # Handle if the script is file or script block + if ($script:GatekeeperConfiguration.Logging[$level].Script -is [string]) { + $scriptPath = $script:GatekeeperConfiguration.Logging[$level].Script + if (-not (Test-Path -Path $scriptPath)) { + throw "Logging script file not found: $scriptPath" + } + Write-Verbose "Loading logging script from file: $scriptPath" + $script:GatekeeperLogging[$level] = [scriptblock]::Create((Get-Content -Path $scriptPath -Raw)) + } elseif ($script:GatekeeperConfiguration.Logging[$level].Script -is [scriptblock]) { + Write-Verbose "Using inline script block for logging level: $level" + $script:GatekeeperLogging[$level] = $script:GatekeeperConfiguration.Logging[$level].Script + } else { + Write-Warning "No valid script found for logging level: $level" + } + } + } + #endregion Parse the logging configuration } end { Write-Verbose "Gatekeeper configuration imported successfully." diff --git a/Gatekeeper/Public/New-Condition.ps1 b/Gatekeeper/Public/New-Condition.ps1 index 301128c..c5f0842 100644 --- a/Gatekeeper/Public/New-Condition.ps1 +++ b/Gatekeeper/Public/New-Condition.ps1 @@ -41,7 +41,6 @@ function New-Condition { Write-Verbose "Initializing new condition for property '$Property' with operator '$Operator' and value '$Value'." # Test if Property is a known property - # TODO: Configure Get-PropertySet to return a list of all properties if ($property -notin (Get-PropertySet).Properties.Keys) { Write-Warning "Property '$Property' is not defined in any property set." } diff --git a/Gatekeeper/Public/Test-FeatureFlag.ps1 b/Gatekeeper/Public/Test-FeatureFlag.ps1 index 1fda845..6b0c1af 100644 --- a/Gatekeeper/Public/Test-FeatureFlag.ps1 +++ b/Gatekeeper/Public/Test-FeatureFlag.ps1 @@ -38,6 +38,7 @@ begin { $finalResult = $False + $config = Import-GatekeeperConfig } process { @@ -51,22 +52,24 @@ Condition = $rule.Conditions } if (Test-Condition @testConditionSplat) { - Write-Verbose "✅ Rule [$($rule.Name)] matched. Effect: $($rule.Effect)" -ForegroundColor Green + Write-Verbose "✅ Rule [$($rule.Name)] matched. Effect: $($rule.Effect)" # Check effect switch ($rule.Effect) { 'Allow' { + . $script:GatekeeperLogging['Allow'] -Rule $rule $finalResult = $true break } 'Deny' { + . $script:GatekeeperLogging['Deny'] -Rule $rule $finalResult = $false break } 'Audit' { - # TODO: Implement auditing function + . $script:GatekeeperLogging['Audit'] -Rule $rule } 'Warn' { - Write-Warning "⚠️ Rule [$($rule.Name)] matched." + . $script:GatekeeperLogging['Warning'] -Rule $rule } default { throw 'Unknown effect' diff --git a/README.md b/README.md index 80a66cd..d33a079 100644 --- a/README.md +++ b/README.md @@ -314,17 +314,66 @@ property. > effect is not an approval. This protects against accidentally opening a > feature when your default is warn or audit. -## To Do +## Configuration + +| Key | Description | Default | +|------------|-----------------------------------------------------------------------------------------------------| -| +| `Version` | The version of the configuration file, used for safe upgrades. | `0.1.0` | +| `FilePaths` | An object containing paths to important folders, such as Schemas. | Objects | +| `FilePaths.Schemas` | The path to the the Schemas on disk. | `Schemas` in Module directory | +| `FilePaths.FeatureFlags` | The path to the the FeatureFlags on disk. | `$null`. ^[1] | +| `FilePaths.PropertySet` | The path to the the PropertySet's on disk. | `$null`. ^[1] | +| `Logging` | An object defining logging behaviors for different rule outcomes (Allow, Deny, Warning, Audit). | Object with `Allow`, `Deny`, `Warning`, and `Audit` defined. | +| `Logging.Allow` | Logging settings for allowed rules, including whether logging is enabled and the script to execute. | See the [Logging](#logging) table | +| `Logging.Deny` | Logging settings for denied rules, including whether logging is enabled and the script to execute. | See the [Logging](#logging) table | +| `Logging.Warning` | Logging settings for warning rules, including whether logging is enabled and the script to execute. | See the [Logging](#logging) table | +| `Logging.Audit` | Logging settings for audit rules, including whether logging is enabled and the script to execute. | See the [Logging](#logging) table | + +[^1]: These folders are evaluated during a run and the configuration is saved to +disk. These default to the same folder as the machine wide configuration. + +### Loading Precedent + +Configuration begins by loading the +[Configuration.psd1](config) from the module. +Then it loads the machine-wide settings (e.g. `$Env:ProgramData` or +`/etc/xdg/`). Then it imports the users' enterprise roaming settings (e.g. from +`$Env:AppData` (the roaming path) or `~/.local/share/`). Finally it imports the +users' local settings (from `$Env:LocalAppData` or `~/.config/`). + +> [!NOTE] +> All the logic of placing configuration is thanks to the Configuration module. + +# TODO finish this read me + +## Logging + +Logging is defined in the configuration file and can be a a path on disk or a +scriptblock. Either should accept a `$Rule` parameter (but don't necessarily +need to use it). + +The default configuration has the Allow and Deny logging rule set to disable to +avoid cluttering the screen. The default warning will `Write-Warning` to let you +know the rule would have passed. + +| Logging Level | Enabled | Default | +|---------------|----------|--------------------------------------------------------------| +| Allow | Disabled | `Write-Host "✅ Rule [$($Rule.Name)] matched and is allowed"` | +| Deny | Disabled | `Write-Host "⛔ Rule [$($Rule.Name)] matched and is denied."` | +| Warning | Enabled | `Write-Warning "⚠️ Rule [$($Rule.Name)] matched."` | +| Audit | Enabled | `Write-Host "Audit: $($Rule.Name)"` | + +# TODO add example of overwriting logging + +The most obvious logging method to overwrite will be `Audit`. In your +[configuration file](#configuration) you will need to overwrite the script +block. + +## ToDo + +These are items that may or may not be setup. -- [ ] Function to create PropertySet -- [ ] Class for FeatureFlag - [ ] Evaluate performance -- [ ] Handle fetching/caching feature flags -- [ ] Script level variables for defining where to get/set json files. -- [ ] Function to create property in PropertySet -- [ ] Ability to create PropertySet in memory and then ability to save to disk. -- [ ] Define auditing method that users can overwrite -- [ ] Publish schemas somewhere consistent with some type of versioning -- [ ] CRUD for creating condition -- [ ] TUI? - [ ] Support for evaluating remote device + +[config]: blob/main/Gatekeeper/Configuration.psd1 \ No newline at end of file From daefac79961a6a243234b4e8bd442a6ded8d1a54 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 29 Jun 2025 16:05:50 -0700 Subject: [PATCH 2/3] =?UTF-8?q?feat(logging):=20=E2=9C=A8=20enhance=20audi?= =?UTF-8?q?ting=20functionality=20in=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added detailed instructions for changing the auditing function in `Configuration.psd1`. - Provided examples for both inline script and external script usage for logging. - Updated README to improve clarity on logging configuration. --- .markdownlint.json | 4 ++++ README.md | 60 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..9111dfd --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": false, + "MD053": false +} diff --git a/README.md b/README.md index d33a079..405f51d 100644 --- a/README.md +++ b/README.md @@ -329,8 +329,8 @@ property. | `Logging.Warning` | Logging settings for warning rules, including whether logging is enabled and the script to execute. | See the [Logging](#logging) table | | `Logging.Audit` | Logging settings for audit rules, including whether logging is enabled and the script to execute. | See the [Logging](#logging) table | -[^1]: These folders are evaluated during a run and the configuration is saved to -disk. These default to the same folder as the machine wide configuration. +[^1]: These folders are evaluated during a run and the configuration is saved + to disk. These default to the same folder as the machine wide configuration. ### Loading Precedent @@ -344,8 +344,6 @@ users' local settings (from `$Env:LocalAppData` or `~/.config/`). > [!NOTE] > All the logic of placing configuration is thanks to the Configuration module. -# TODO finish this read me - ## Logging Logging is defined in the configuration file and can be a a path on disk or a @@ -363,17 +361,61 @@ know the rule would have passed. | Warning | Enabled | `Write-Warning "⚠️ Rule [$($Rule.Name)] matched."` | | Audit | Enabled | `Write-Host "Audit: $($Rule.Name)"` | -# TODO add example of overwriting logging - The most obvious logging method to overwrite will be `Audit`. In your [configuration file](#configuration) you will need to overwrite the script block. -## ToDo +### Changing Auditing Function + +To change your auditing function you need to update your `Configuration.psd1` to +contain something like the following: + +```powershell +Logging = @{ + ... # Your other logging functions (if any) + Audit = @{ + # Ensure it's enabled + Enabled = $true + Script = { + param($Rule) + $line = "✅ Rule [$($Rule.Name)] matched and is allowed." + $line | Out-File C:\Contoso\Logs\Gatekeeper.log -Append + } + } +} +``` + +Here is an example if you prefer to use a script from disk. + +Let's say you have a script called `C:\Contoso\Logging.ps1`. That script writes +to a log file. The script could look something like : + +```powershell +param($Rule) +$line = "✅ Rule [$($Rule.Name)] matched and is allowed." +$line | Out-File C:\Contoso\Logs\Gatekeeper.log -Append +``` + +Then you would update your configuration to look like: + +```powershell +Logging = @{ + ... # Your other logging functions (if any) + Audit = @{ + # Ensure it's enabled + Enabled = $true + Script = 'C:\Contoso\Logging.ps1' + } +} +``` + +> [!IMPORTANT] +> If you supply a string, it must be a valid path to a script file +> that accepts the `$Rule` parameter. + +# ToDo List These are items that may or may not be setup. - [ ] Evaluate performance - [ ] Support for evaluating remote device - -[config]: blob/main/Gatekeeper/Configuration.psd1 \ No newline at end of file From 98241de9750f1bd0ac0f5b8323fa8d06fca5d83b Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 29 Jun 2025 16:08:34 -0700 Subject: [PATCH 3/3] Fix footnote --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 405f51d..e1bf460 100644 --- a/README.md +++ b/README.md @@ -321,8 +321,8 @@ property. | `Version` | The version of the configuration file, used for safe upgrades. | `0.1.0` | | `FilePaths` | An object containing paths to important folders, such as Schemas. | Objects | | `FilePaths.Schemas` | The path to the the Schemas on disk. | `Schemas` in Module directory | -| `FilePaths.FeatureFlags` | The path to the the FeatureFlags on disk. | `$null`. ^[1] | -| `FilePaths.PropertySet` | The path to the the PropertySet's on disk. | `$null`. ^[1] | +| `FilePaths.FeatureFlags` | The path to the the FeatureFlags on disk. | `$null`. [^1] | +| `FilePaths.PropertySet` | The path to the the PropertySet's on disk. | `$null`.[^1] | | `Logging` | An object defining logging behaviors for different rule outcomes (Allow, Deny, Warning, Audit). | Object with `Allow`, `Deny`, `Warning`, and `Audit` defined. | | `Logging.Allow` | Logging settings for allowed rules, including whether logging is enabled and the script to execute. | See the [Logging](#logging) table | | `Logging.Deny` | Logging settings for denied rules, including whether logging is enabled and the script to execute. | See the [Logging](#logging) table |