Private/Audit/Get-AuditPostureScore.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Get-AuditPostureScore { [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject[]]$Findings ) $severityWeights = @{ Critical = 10 High = 6 Medium = 3 Low = 1 Info = 0 } # Group findings by category $categories = $Findings | Group-Object -Property Category $categoryScores = @{} foreach ($cat in $categories) { $catFindings = @($cat.Group) $passCount = @($catFindings | Where-Object Status -eq 'PASS').Count $failCount = @($catFindings | Where-Object Status -eq 'FAIL').Count $warnCount = @($catFindings | Where-Object Status -eq 'WARN').Count $skipCount = @($catFindings | Where-Object Status -in @('SKIP', 'ERROR')).Count # Calculate deductions $deductions = 0.0 foreach ($f in $catFindings) { if ($f.Status -notin @('FAIL', 'WARN')) { continue } $weight = $severityWeights[$f.Severity] ?? 1 $multiplier = if ($f.Status -eq 'WARN') { 0.5 } else { 1.0 } $deductions += ($weight * $multiplier) } # Max possible deductions for normalization $maxPossible = 0.0 foreach ($f in $catFindings) { if ($f.Status -in @('SKIP', 'ERROR')) { continue } $maxPossible += ($severityWeights[$f.Severity] ?? 1) } # If maxPossible is 0, every finding in this category was SKIP or ERROR # (or the category had zero findings at all because the collector failed # upstream). In that case we have no real data to score against — do NOT # treat the category as a perfect 100, which would silently inflate the # overall posture score whenever a collector quietly fails. $evaluated = $maxPossible -gt 0 $catScore = if ($evaluated) { [Math]::Max(0, [Math]::Round(100 * (1 - ($deductions / $maxPossible)), 0)) } else { 0 } $categoryScores[$cat.Name] = @{ Score = [int]$catScore Evaluated = $evaluated Pass = $passCount Fail = $failCount Warn = $warnCount Skip = $skipCount Total = $catFindings.Count } } # Overall score: weighted average of category scores. Categories where nothing # was actually evaluated are excluded from the average — otherwise an all-skip # category would either inflate (old behavior, 100) or deflate (new per-cat # default, 0) the overall number and mislead the user. $totalWeight = 0.0 $weightedSum = 0.0 foreach ($cat in $categoryScores.GetEnumerator()) { if (-not $cat.Value.Evaluated) { continue } $catFindings = @($Findings | Where-Object { $_.Category -eq $cat.Key -and $_.Status -notin @('SKIP', 'ERROR') }) $catWeight = 0.0 foreach ($f in $catFindings) { $catWeight += ($severityWeights[$f.Severity] ?? 1) } $totalWeight += $catWeight $weightedSum += ($cat.Value.Score * $catWeight) } $overallScore = if ($totalWeight -gt 0) { [int][Math]::Round($weightedSum / $totalWeight, 0) } else { 0 } return @{ OverallScore = $overallScore CategoryScores = $categoryScores } } |