Scoring/Get-EscPostureScore.ps1
|
function Get-EscPostureScore { <# .SYNOPSIS Aggregates scored ESC Findings into an overall Posture Score (0-100) summary. .DESCRIPTION Implements the authoritative Posture Score model: StatusWeight : Vulnerable=1.0 Potential=0.5 ManualReview=0.2 NotVulnerable=0.0 Error=0.0 (Error excluded from the sum) PostureScore = 100 - [math]::Min(100, sum(RiskScore_i * StatusWeight_i)) The result is rounded to one decimal via [math]::Round(x, 1) - the same rounding used per-finding - so higher scores mean a better security posture. Letter grade: A(90-100) B(80-89) C(70-79) D(60-69) F(<60). Findings SHOULD already carry RiskScore (run Get-EscRiskScore first); any finding lacking RiskScore is treated as 0. The summary also reports the Status counts, a BySeverity breakdown (counted only among Vulnerable and Potential findings), and up to 5 TopFindings (highest RiskScore, Vulnerable only). The function is deterministic and pure - no clock is read; the caller stamps the generation time and may pass it (or any context) via -GeneratedContext, which is echoed back untouched. .PARAMETER Finding The scored Finding [pscustomobject]s. .PARAMETER GeneratedContext Optional caller-supplied context object (e.g. Domain/Forest/GeneratedAt); passed through verbatim onto the summary's GeneratedContext property. .OUTPUTS [pscustomobject] summary: PostureScore, Grade, Total, Vulnerable, Potential, ManualReview, NotVulnerable, Error, BySeverity (hashtable), TopFindings, GeneratedContext. .EXAMPLE $summary = Get-EscPostureScore -Finding ($findings | Get-EscRiskScore) # Score every finding, then roll them up into one posture summary. .EXAMPLE $scored = Get-EscRiskScore -Finding $findings $summary = Get-EscPostureScore -Finding $scored -GeneratedContext $ctx.Meta "{0} / 100 -> {1}" -f $summary.PostureScore, $summary.Grade #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [pscustomobject[]] $Finding, [Parameter(Mandatory = $false)] [object] $GeneratedContext ) $statusWeight = @{ 'Vulnerable' = 1.0 'Potential' = 0.5 'ManualReview' = 0.2 'NotVulnerable' = 0.0 'Error' = 0.0 } $counts = @{ 'Vulnerable' = 0 'Potential' = 0 'ManualReview' = 0 'NotVulnerable' = 0 'Error' = 0 } $bySeverity = @{ 'Critical' = 0 'High' = 0 'Medium' = 0 'Low' = 0 'Info' = 0 } $total = 0 $sum = 0.0 foreach ($f in $Finding) { if ($null -eq $f) { continue } $total++ $status = [string] $f.Status if ($counts.ContainsKey($status)) { $counts[$status] = $counts[$status] + 1 } $risk = 0.0 if ($f.PSObject.Properties.Match('RiskScore').Count -gt 0 -and $null -ne $f.RiskScore) { $risk = [double] $f.RiskScore } if ($statusWeight.ContainsKey($status)) { $sum += ($risk * $statusWeight[$status]) } if ($status -eq 'Vulnerable' -or $status -eq 'Potential') { $sev = [string] $f.Severity if ($bySeverity.ContainsKey($sev)) { $bySeverity[$sev] = $bySeverity[$sev] + 1 } } } $penalty = [math]::Min(100.0, $sum) $additive = [math]::Round((100.0 - $penalty), 1) $ceiling = 100.0 $worstVuln = 'None' $vulnSevs = @() foreach ($f in $Finding) { if ($null -ne $f -and [string]$f.Status -eq 'Vulnerable') { $vulnSevs += [string]$f.Severity } } if ($vulnSevs -contains 'Critical') { $ceiling = 39.0; $worstVuln = 'Critical' } elseif ($vulnSevs -contains 'High') { $ceiling = 69.0; $worstVuln = 'High' } elseif ($vulnSevs -contains 'Medium') { $ceiling = 79.0; $worstVuln = 'Medium' } elseif ($vulnSevs -contains 'Low') { $ceiling = 89.0; $worstVuln = 'Low' } $posture = [math]::Round([math]::Min([double] $additive, $ceiling), 1) $capped = ($posture -lt $additive) if ($posture -ge 90) { $grade = 'A' } elseif ($posture -ge 80) { $grade = 'B' } elseif ($posture -ge 70) { $grade = 'C' } elseif ($posture -ge 60) { $grade = 'D' } else { $grade = 'F' } $topFindings = @( $Finding | Where-Object { $null -ne $_ -and [string]$_.Status -eq 'Vulnerable' } | Sort-Object -Property @{ Expression = { [double] $_.RiskScore } } -Descending | Select-Object -First 5 ) [pscustomobject]@{ PostureScore = $posture Grade = $grade AdditiveScore = $additive Capped = $capped WorstVulnerableSeverity = $worstVuln Total = $total Vulnerable = $counts['Vulnerable'] Potential = $counts['Potential'] ManualReview = $counts['ManualReview'] NotVulnerable = $counts['NotVulnerable'] Error = $counts['Error'] BySeverity = $bySeverity TopFindings = $topFindings GeneratedContext = $GeneratedContext } } |