Scoring/Get-EscRiskScore.ps1
|
function Get-EscRiskScore { <# .SYNOPSIS Fills the RiskScore field on ESC Finding objects (read-only, deterministic). .DESCRIPTION Computes the per-finding RiskScore from the following deterministic model: SeverityBase : Critical=9.0 High=7.0 Medium=5.0 Low=3.0 Info=1.0 ExploitMult : High=1.0 Medium=0.8 Low=0.55 Theoretical=0.35 RiskScore = [math]::Round([math]::Min(10.0, SeverityBase * ExploitMult), 1) Special Status handling: - Status = NotVulnerable -> RiskScore = 0 (formula not applied) - Status = Error -> RiskScore = 0 (excluded from posture) - Status = ManualReview -> formula IS applied (score kept); it is down-weighted (0.2) in the posture roll-up, not zeroed here. Only NotVulnerable maps to 0; the ManualReview worked example yields 0.4. - Severity is never modified for any status. - Missing / unknown Exploitability -> defaults to the Medium multiplier (0.8) - Missing / unknown Severity -> defaults to the Info base (1.0) The function updates only the RiskScore field in place; every other Finding field is left exactly as received. It is idempotent - running it twice yields the same RiskScore - and pure (no clock, no external state). .PARAMETER Finding One or more Finding [pscustomobject]s. Accepts pipeline input or the -Finding parameter. .OUTPUTS [pscustomobject] The same Finding objects with RiskScore populated. .EXAMPLE $scored = $findings | Get-EscRiskScore # Fills RiskScore on every finding coming out of the analyzers. .EXAMPLE Get-EscRiskScore -Finding $findings | Where-Object RiskScore -ge 7.0 # Score a batch and keep only the high-impact findings. #> [CmdletBinding()] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [pscustomobject[]] $Finding ) begin { $severityBase = @{ 'Critical' = 9.0 'High' = 7.0 'Medium' = 5.0 'Low' = 3.0 'Info' = 1.0 } $exploitMultiplier = @{ 'High' = 1.0 'Medium' = 0.8 'Low' = 0.55 'Theoretical' = 0.35 } $defaultMultiplier = 0.8 $defaultBase = 1.0 } process { foreach ($f in $Finding) { if ($null -eq $f) { continue } $status = [string] $f.Status if ($status -eq 'NotVulnerable' -or $status -eq 'Error') { $score = 0.0 } else { $sev = [string] $f.Severity if (-not [string]::IsNullOrEmpty($sev) -and $severityBase.ContainsKey($sev)) { $base = [double] $severityBase[$sev] } else { $base = $defaultBase } $exp = [string] $f.Exploitability if (-not [string]::IsNullOrEmpty($exp) -and $exploitMultiplier.ContainsKey($exp)) { $mult = [double] $exploitMultiplier[$exp] } else { $mult = $defaultMultiplier } $score = [math]::Round([math]::Min(10.0, ($base * $mult)), 1) } if ($f.PSObject.Properties.Match('RiskScore').Count -gt 0) { $f.RiskScore = $score } else { Add-Member -InputObject $f -MemberType NoteProperty -Name 'RiskScore' -Value $score -Force } $f } } } |