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/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..e1bf460 100644 --- a/README.md +++ b/README.md @@ -314,17 +314,108 @@ 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. + +## 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)"` | + +The most obvious logging method to overwrite will be `Audit`. In your +[configuration file](#configuration) you will need to overwrite the script +block. + +### 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. -- [ ] 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