internal/Audit/Merge-GuerrillaDeviation.ps1
|
# Guerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/Guerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution # Apply a validated deviation overlay to a finding set. # # Deviations are expressed as flags/notes on each finding's Details, NEVER as a # new Status value: the run-record verdict mapper refuses unknown statuses, and # any consumer that hasn't learned about deviations must keep treating an # accepted FAIL as a FAIL (conservative — a deviation can never manufacture a # false PASS). Only the posture scorer opts in to reading these flags. # # Guardrails baked in: # * accept-risk / suppress apply ONLY to a real FAIL/WARN. A SKIP ("Not # Assessed") is never turned into an accepted or hidden finding — absence of # evidence can't be waived into compliance. # * an expired deviation does not apply; instead the finding gets a visible # note that its exception lapsed, so nothing silently reverts. # * suppressed findings stay in the set flagged Details.Suppressed — the report # counts them in a dedicated section rather than dropping them silently. # # Returns { Findings = <same objects, mutated>; Summary = <counts> }. function Merge-GuerrillaDeviation { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [PSCustomObject[]]$Findings, [Parameter(Mandatory)] [AllowEmptyCollection()] [hashtable[]]$Deviations, [datetime]$ReferenceTime = [datetime]::UtcNow ) $summary = [ordered]@{ Accepted = 0 Suppressed = 0 Annotated = 0 ExpectFailSatisfied = 0 ExpectFailDrifted = 0 Expired = 0 Unmatched = 0 } if (-not $Deviations -or $Deviations.Count -eq 0) { return [pscustomobject]@{ Findings = @($Findings); Summary = [pscustomobject]$summary } } # Index deviations by checkId (case-insensitive), preserving order. $byCheck = @{} foreach ($d in $Deviations) { $k = $d.CheckId.ToUpperInvariant() if (-not $byCheck.ContainsKey($k)) { $byCheck[$k] = [System.Collections.Generic.List[hashtable]]::new() } $byCheck[$k].Add($d) } $matched = [System.Collections.Generic.HashSet[object]]::new() foreach ($f in $Findings) { $k = "$($f.CheckId)".ToUpperInvariant() if (-not $byCheck.ContainsKey($k)) { continue } foreach ($d in $byCheck[$k]) { # Scope narrowing: if the deviation names an OU, only apply to that OU. if ($d.OrgUnitPath -and "$($f.OrgUnitPath)" -ne $d.OrgUnitPath) { continue } [void]$matched.Add($d) if ($d.Expires -and $ReferenceTime -gt $d.Expires) { $summary.Expired++ Add-GuerrillaDeviationNote -Finding $f -Note ("A '$($d.Type)' deviation for this finding EXPIRED on {0} and no longer applies. Re-review or renew it." -f $d.Expires.ToString('yyyy-MM-dd')) continue } switch ($d.Type) { 'annotate' { Add-GuerrillaDeviationNote -Finding $f -Note $d.Justification $summary.Annotated++ } 'suppress' { if ($f.Status -in @('FAIL', 'WARN')) { Set-GuerrillaDeviationFlag -Finding $f -Flag 'Suppressed' -Deviation $d $summary.Suppressed++ } } 'accept-risk' { if ($f.Status -in @('FAIL', 'WARN')) { Set-GuerrillaDeviationFlag -Finding $f -Flag 'Accepted' -Deviation $d $summary.Accepted++ } } 'expect-fail' { if ($f.Status -eq 'FAIL') { Set-GuerrillaDeviationFlag -Finding $f -Flag 'Accepted' -Deviation $d Add-GuerrillaDeviationNote -Finding $f -Note "Expected-fail: this is a known, accepted failure. $($d.Justification)" $summary.ExpectFailSatisfied++ } elseif ($f.Status -eq 'PASS') { Add-GuerrillaDeviationNote -Finding $f -Note "CONFIG DRIFT: an 'expect-fail' deviation is registered for this check but it now PASSes. The underlying condition changed — confirm the deviation is still needed." $summary.ExpectFailDrifted++ } } } } } $summary.Unmatched = @($Deviations | Where-Object { -not $matched.Contains($_) }).Count return [pscustomobject]@{ Findings = @($Findings); Summary = [pscustomobject]$summary } } # True when a finding has been waived out of the posture math (accepted or # suppressed). The scorer uses this; everything else keeps seeing the raw Status. function Test-GuerrillaDeviated { [CmdletBinding()] param([Parameter(Mandatory)] $Finding) $det = $Finding.Details if ($det -isnot [System.Collections.IDictionary]) { return $false } return [bool]($det['Suppressed'] -or $det['Accepted']) } function Set-GuerrillaDeviationFlag { [CmdletBinding()] param( [Parameter(Mandatory)] $Finding, [Parameter(Mandatory)] [string]$Flag, [Parameter(Mandatory)] [hashtable]$Deviation ) if ($Finding.Details -isnot [System.Collections.IDictionary]) { $Finding | Add-Member -NotePropertyName Details -NotePropertyValue @{} -Force } $Finding.Details[$Flag] = $true $Finding.Details['Deviation'] = @{ Type = $Deviation.Type Justification = $Deviation.Justification ApprovedBy = $Deviation.ApprovedBy Expires = if ($Deviation.Expires) { $Deviation.Expires.ToString('yyyy-MM-dd') } else { $null } OriginalStatus = $Finding.Status } } function Add-GuerrillaDeviationNote { [CmdletBinding()] param( [Parameter(Mandatory)] $Finding, [Parameter(Mandatory)] [string]$Note ) if ($Finding.Details -isnot [System.Collections.IDictionary]) { $Finding | Add-Member -NotePropertyName Details -NotePropertyValue @{} -Force } $existing = $Finding.Details['DeviationNote'] $Finding.Details['DeviationNote'] = if ($existing) { "$existing`n$Note" } else { $Note } } |