Public/Get-ZeroTrustScore.ps1
|
function Get-ZeroTrustScore { <# .SYNOPSIS Compute a Zero Trust posture score per CISA ZTMM pillar from audit findings — and disclose how much coverage backs each score. .DESCRIPTION Rolls Guerrilla findings up by their ZeroTrustPillar / ZeroTrustWeight into a weighted posture score per pillar: score% = 100 * Σ(weight × credit) / Σ(weight) over ASSESSED checks credit: PASS = 1.0, WARN = 0.5, FAIL = 0.0 Only checks that produced a real verdict (PASS/FAIL/WARN) count toward a score; Not-Assessed (SKIP) checks are excluded from both numerator and denominator, so an uncollected control never quietly inflates or deflates the number. Every pillar also reports CoverageConfidence — Solid / Moderate / Directional — based on how many checks define that pillar. A pillar computed from few checks (e.g. Data) is flagged Directional so its score is read as indicative, not authoritative. The score that discloses its own density is the whole point: it cannot 200 confidently over a thin basis without saying so. .PARAMETER Finding Guerrilla.AuditFinding objects (pipeline). Typically the .Findings of an Invoke-Campaign / Invoke-Infiltration / Invoke-Reconnaissance / Invoke-Fortification result, e.g. (Invoke-Infiltration ...).Findings | Get-ZeroTrustScore .PARAMETER DirectionalThreshold Pillars with fewer than this many checks are marked Directional (default 25). .PARAMETER SolidThreshold Pillars with at least this many checks are marked Solid (default 60); between the two thresholds is Moderate. .EXAMPLE (Invoke-Infiltration -TenantId $t).Findings | Get-ZeroTrustScore | Format-Table .EXAMPLE $r.Findings | Get-ZeroTrustScore | Where-Object CoverageConfidence -ne 'Directional' #> [CmdletBinding()] [OutputType('Guerrilla.ZeroTrustPillarScore')] param( [Parameter(ValueFromPipeline)] [object[]]$Finding, [int]$DirectionalThreshold = 25, [int]$SolidThreshold = 60 ) begin { $all = [System.Collections.Generic.List[object]]::new() $credit = @{ PASS = 1.0; WARN = 0.5; FAIL = 0.0 } } process { foreach ($f in $Finding) { if ($null -ne $f) { $all.Add($f) } } } end { $withPillar = @($all | Where-Object { $_.ZeroTrustPillar }) foreach ($g in ($withPillar | Group-Object ZeroTrustPillar | Sort-Object Name)) { $items = @($g.Group) $assessed = @($items | Where-Object { $_.Status -in 'PASS', 'FAIL', 'WARN' }) $maxW = [double](@($assessed | Measure-Object ZeroTrustWeight -Sum).Sum) $earnedW = 0.0 foreach ($i in $assessed) { $earnedW += ([double]$i.ZeroTrustWeight) * ([double]($credit["$($i.Status)"] ?? 0)) } $score = if ($maxW -gt 0) { [int][math]::Round(100 * $earnedW / $maxW, 0) } else { $null } $conf = if ($items.Count -lt $DirectionalThreshold) { 'Directional' } elseif ($items.Count -lt $SolidThreshold) { 'Moderate' } else { 'Solid' } [PSCustomObject]@{ PSTypeName = 'Guerrilla.ZeroTrustPillarScore' Pillar = $g.Name ScorePercent = $score CoverageConfidence = $conf AssessedChecks = $assessed.Count TotalChecks = $items.Count MaxWeight = [int]$maxW EarnedWeight = [math]::Round($earnedW, 1) } } } } |