Private/Core/Get-GuerrillaScoreCalculation.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-GuerrillaScoreCalculation { <# .SYNOPSIS Computes the composite Guerrilla Security Score (0-100). .DESCRIPTION Calculates a weighted composite score from four components: - Posture (40%): Audit posture scores from AD + Cloud findings - Threats (30%): Inverse normalized threat count, weighted by severity - Coverage (15%): Percentage of theaters actively monitored - Trend (15%): Score delta from previous scan (improving = bonus) .PARAMETER AuditFindings Array of audit finding objects (from Fortification/Reconnaissance theaters). .PARAMETER ScanResults Array of scan result objects from all theaters. .PARAMETER PreviousScore Previous Guerrilla Score for trend calculation. If not provided, trend is neutral. .PARAMETER Profile Baseline profile hashtable with component weights. Uses default weights if not provided. #> [CmdletBinding()] param( [PSCustomObject[]]$AuditFindings, [PSCustomObject[]]$ScanResults, [double]$PreviousScore = -1, [hashtable]$Profile ) # Component weights from profile or defaults $weights = if ($Profile.guerrillaScore.componentWeights) { $Profile.guerrillaScore.componentWeights } else { @{ posture = 0.40; threats = 0.30; coverage = 0.15; trend = 0.15 } } # --- Component 1: Posture Score (0-100) --- $postureScore = 100 if ($AuditFindings -and $AuditFindings.Count -gt 0) { $postureResult = Get-AuditPostureScore -Findings $AuditFindings $postureScore = [Math]::Max(0, [Math]::Min(100, $postureResult.OverallScore)) } # --- Component 2: Threat Score (0-100, inverse normalized) --- $threatScore = 100 $threatSeverityWeights = @{ 'CRITICAL' = 20 'HIGH' = 10 'MEDIUM' = 4 'LOW' = 1 } $maxThreatBudget = 200 # Weighted threat count that maps to score 0 if ($ScanResults -and $ScanResults.Count -gt 0) { $weightedThreatCount = 0.0 foreach ($result in $ScanResults) { $weightedThreatCount += ($result.CriticalCount ?? 0) * $threatSeverityWeights['CRITICAL'] $weightedThreatCount += ($result.HighCount ?? 0) * $threatSeverityWeights['HIGH'] $weightedThreatCount += ($result.MediumCount ?? 0) * $threatSeverityWeights['MEDIUM'] $weightedThreatCount += ($result.LowCount ?? 0) * $threatSeverityWeights['LOW'] } $threatScore = [Math]::Max(0, [Math]::Round(100 * (1 - ($weightedThreatCount / $maxThreatBudget)), 0)) } # --- Component 3: Coverage Score (0-100) --- $allTheaters = @('Fortification', 'Reconnaissance', 'Surveillance', 'Watchtower') $activeTheaters = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) if ($AuditFindings -and $AuditFindings.Count -gt 0) { # Fortification = AD checks, Reconnaissance = Cloud checks $hasAD = @($AuditFindings | Where-Object { $_.CheckId -match '^AD' }).Count -gt 0 $hasCloud = @($AuditFindings | Where-Object { $_.CheckId -match '^(AUTH|ADMIN|EMAIL|COLLAB|DRIVE|OAUTH|DEVICE|LOG|EID|M365|AZIAM|INTUNE)' }).Count -gt 0 if ($hasAD) { $activeTheaters.Add('Fortification') | Out-Null } if ($hasCloud) { $activeTheaters.Add('Reconnaissance') | Out-Null } } if ($ScanResults -and $ScanResults.Count -gt 0) { foreach ($result in $ScanResults) { $theater = $result.Theater ?? $result.PSObject.TypeNames[0] if ($theater -match 'Surveillance') { $activeTheaters.Add('Surveillance') | Out-Null } if ($theater -match 'Watchtower') { $activeTheaters.Add('Watchtower') | Out-Null } if ($theater -match 'Wiretap') { $activeTheaters.Add('Wiretap') | Out-Null } } } $coverageScore = if ($allTheaters.Count -gt 0) { [int][Math]::Round(100 * ($activeTheaters.Count / $allTheaters.Count), 0) } else { 0 } # --- Component 4: Trend Score (0-100) --- $trendScore = 50 # Neutral default if ($PreviousScore -ge 0) { $currentRaw = [Math]::Round( ($postureScore * $weights.posture + $threatScore * $weights.threats + $coverageScore * $weights.coverage) / ($weights.posture + $weights.threats + $weights.coverage), 0 ) $delta = $currentRaw - $PreviousScore # Map delta to 0-100: +20 or more = 100, -20 or less = 0, linear between $trendScore = [Math]::Max(0, [Math]::Min(100, [int][Math]::Round(50 + ($delta * 2.5), 0))) } # --- Composite Score --- $compositeScore = [int][Math]::Round( ($postureScore * $weights.posture) + ($threatScore * $weights.threats) + ($coverageScore * $weights.coverage) + ($trendScore * $weights.trend), 0 ) $compositeScore = [Math]::Max(0, [Math]::Min(100, $compositeScore)) $label = Get-GuerrillaScoreLabel -Score $compositeScore return [PSCustomObject]@{ PSTypeName = 'PSGuerrilla.GuerrillaScore' Score = $compositeScore Label = $label.Label LabelColor = $label.Color Components = [PSCustomObject]@{ Posture = [PSCustomObject]@{ Score = $postureScore; Weight = $weights.posture; Weighted = [int][Math]::Round($postureScore * $weights.posture, 0) } Threats = [PSCustomObject]@{ Score = $threatScore; Weight = $weights.threats; Weighted = [int][Math]::Round($threatScore * $weights.threats, 0) } Coverage = [PSCustomObject]@{ Score = $coverageScore; Weight = $weights.coverage; Weighted = [int][Math]::Round($coverageScore * $weights.coverage, 0) } Trend = [PSCustomObject]@{ Score = $trendScore; Weight = $weights.trend; Weighted = [int][Math]::Round($trendScore * $weights.trend, 0) } } ActiveTheaters = @($activeTheaters) PreviousScore = if ($PreviousScore -ge 0) { $PreviousScore } else { $null } Timestamp = [datetime]::UtcNow } } |