Private/Comparison/Calculate-ComplianceScores.ps1
|
function Calculate-ComplianceScores { <# .SYNOPSIS Calculates compliance scores for ALZ, MCSB, and overall baseline coverage. .DESCRIPTION Analyzes which baseline policies are actually deployed by examining all assignments and their expanded policies. Computes separate scores for: - ALZ baseline coverage - MCSB baseline coverage - Global (combined) compliance score This function reads initiative CSV files to determine which policies are deployed. ⚡ OPTIMIZED VERSION - Uses normalized indexes for O(1) lookups instead of O(n) loops. .PARAMETER Baseline Array of baseline policy objects with BaselineSources property. .PARAMETER Summary Array of summary objects from assignments (with IsIndividualPolicy flag). .PARAMETER InitiativesFolder Path to folder containing Initiative-{Name}-Policies.csv files. .PARAMETER PolicyDefinitionCache Hashtable cache for policy definitions (for individual policies). .EXAMPLE $scores = Calculate-ComplianceScores -Baseline $baseline ` -Summary $summary ` -InitiativesFolder $initiativesFolder ` -PolicyDefinitionCache $script:policyDefinitionCache Returns PSCustomObject with AlzScore, McsbScore, GlobalScore, and detailed metrics. .OUTPUTS PSCustomObject with properties: - AlzScore: Percentage of ALZ baseline deployed (0-100) - McsbScore: Percentage of MCSB baseline deployed (0-100) - GlobalScore: Percentage of total baseline deployed (0-100) - AlzMetrics: Hashtable with BaselineCount, Deployed, Missing - McsbMetrics: Hashtable with BaselineCount, Deployed, Missing - GlobalMetrics: Hashtable with TotalBaseline, TotalDeployed, ComplianceLevel #> [CmdletBinding()] param( [Parameter(Mandatory)] [object[]]$Baseline, [Parameter()] [AllowEmptyCollection()] [object[]]$Summary = @(), [Parameter(Mandatory)] [string]$InitiativesFolder, [Parameter(Mandatory)] [hashtable]$PolicyDefinitionCache ) Write-Host "📊 Computing baseline coverage scores..." -ForegroundColor Cyan # ========== ✅ OPTIMISATION: Build Baseline Indexes for O(1) lookups ========== Write-Verbose "Building baseline policy indexes..." $alzPoliciesIndex = @{} $mcsbPoliciesIndex = @{} # ✅ NOUVEAU: Créer des index normalisés UNE SEULE FOIS $alzNormalizedIndex = @{} $mcsbNormalizedIndex = @{} foreach ($policy in $Baseline) { if ($policy.BaselineSources -like "*ALZ*") { $alzPoliciesIndex[$policy.PolicyDefinitionId] = $policy # Ajouter à l'index normalisé $normName = Normalize-PolicyName $policy.PolicyDisplayName if ($normName -and -not $alzNormalizedIndex.ContainsKey($normName)) { $alzNormalizedIndex[$normName] = $policy.PolicyDefinitionId } } if ($policy.BaselineSources -like "*MCSB*") { $mcsbPoliciesIndex[$policy.PolicyDefinitionId] = $policy # Ajouter à l'index normalisé $normName = Normalize-PolicyName $policy.PolicyDisplayName if ($normName -and -not $mcsbNormalizedIndex.ContainsKey($normName)) { $mcsbNormalizedIndex[$normName] = $policy.PolicyDefinitionId } } } Write-Verbose " ALZ index: $($alzPoliciesIndex.Count) policies, $($alzNormalizedIndex.Count) normalized names" Write-Verbose " MCSB index: $($mcsbPoliciesIndex.Count) policies, $($mcsbNormalizedIndex.Count) normalized names" # ========== ALZ Score ========== Write-Host " ├─ Analyzing ALZ baseline coverage..." -ForegroundColor DarkCyan $alzBaselineCount = $alzPoliciesIndex.Count $alzMatchedPoliciesSet = [System.Collections.Generic.HashSet[string]]::new() # ✅ OPTIMISATION: Cache des CSV déjà lus $csvCache = @{} foreach ($sumItem in $Summary) { $initiativeCsvPath = Join-Path $InitiativesFolder "Initiative-$($sumItem.AssignmentName)-Policies.csv" if (Test-Path $initiativeCsvPath) { # ✅ OPTIMISATION: Lire CSV une seule fois et le mettre en cache if (-not $csvCache.ContainsKey($initiativeCsvPath)) { $csvCache[$initiativeCsvPath] = Import-Csv $initiativeCsvPath Write-Debug "Cached CSV: $initiativeCsvPath" } $initiativePolicies = $csvCache[$initiativeCsvPath] foreach ($pol in $initiativePolicies) { # ✅ Match by ID first (O(1) lookup) if ($alzPoliciesIndex.ContainsKey($pol.PolicyDefinitionId)) { [void]$alzMatchedPoliciesSet.Add($pol.PolicyDefinitionId) Write-Debug "ALZ match by ID: $($pol.PolicyDefinitionId)" } # ✅ OPTIMISÉ: Utiliser l'index normalisé (O(1) au lieu de O(n)) else { $normName = Normalize-PolicyName $pol.PolicyDisplayName if ($normName -and $alzNormalizedIndex.ContainsKey($normName)) { # Utiliser l'ID du baseline pour éviter les duplicatas $baselineId = $alzNormalizedIndex[$normName] [void]$alzMatchedPoliciesSet.Add($baselineId) Write-Debug "ALZ match by name: $normName -> $baselineId" } } } } else { Write-Debug "Initiative CSV not found: $initiativeCsvPath" } } $alzDeployed = $alzMatchedPoliciesSet.Count $alzMissing = $alzBaselineCount - $alzDeployed if ($alzBaselineCount -gt 0) { $alzScore = [math]::Round(($alzDeployed / $alzBaselineCount) * 100, 1) } else { $alzScore = 0 } Write-Host (" │ ├─ ALZ Baseline: {0} policies" -f $alzBaselineCount) -ForegroundColor DarkGray Write-Host (" │ ├─ Deployed: {0}" -f $alzDeployed) -ForegroundColor DarkGray Write-Host (" │ ├─ Missing: {0}" -f $alzMissing) -ForegroundColor DarkGray Write-Host (" │ └─ ALZ Score: {0}%" -f $alzScore) -ForegroundColor $(if ($alzScore -ge 50) { "Green" } else { "Yellow" }) # ========== MCSB Score ========== Write-Host " ├─ Analyzing MCSB baseline coverage..." -ForegroundColor DarkCyan $mcsbBaselineCount = $mcsbPoliciesIndex.Count $mcsbMatchedPoliciesSet = [System.Collections.Generic.HashSet[string]]::new() foreach ($sumItem in $Summary) { $initiativeCsvPath = Join-Path $InitiativesFolder "Initiative-$($sumItem.AssignmentName)-Policies.csv" if (Test-Path $initiativeCsvPath) { # ✅ Réutiliser le cache CSV if (-not $csvCache.ContainsKey($initiativeCsvPath)) { $csvCache[$initiativeCsvPath] = Import-Csv $initiativeCsvPath } $initiativePolicies = $csvCache[$initiativeCsvPath] foreach ($pol in $initiativePolicies) { # ✅ Match by ID first (O(1) lookup) if ($mcsbPoliciesIndex.ContainsKey($pol.PolicyDefinitionId)) { [void]$mcsbMatchedPoliciesSet.Add($pol.PolicyDefinitionId) Write-Debug "MCSB match by ID: $($pol.PolicyDefinitionId)" } # ✅ OPTIMISÉ: Utiliser l'index normalisé (O(1)) else { $normName = Normalize-PolicyName $pol.PolicyDisplayName if ($normName -and $mcsbNormalizedIndex.ContainsKey($normName)) { $baselineId = $mcsbNormalizedIndex[$normName] [void]$mcsbMatchedPoliciesSet.Add($baselineId) Write-Debug "MCSB match by name: $normName -> $baselineId" } } } } else { Write-Debug "Initiative CSV not found: $initiativeCsvPath" } } $mcsbDeployed = $mcsbMatchedPoliciesSet.Count $mcsbMissing = $mcsbBaselineCount - $mcsbDeployed if ($mcsbBaselineCount -gt 0) { $mcsbScore = [math]::Round(($mcsbDeployed / $mcsbBaselineCount) * 100, 1) } else { $mcsbScore = 0 } Write-Host (" │ ├─ MCSB Baseline: {0} policies" -f $mcsbBaselineCount) -ForegroundColor DarkGray Write-Host (" │ ├─ Deployed: {0}" -f $mcsbDeployed) -ForegroundColor DarkGray Write-Host (" │ ├─ Missing: {0}" -f $mcsbMissing) -ForegroundColor DarkGray Write-Host (" │ └─ MCSB Score: {0}%" -f $mcsbScore) -ForegroundColor $(if ($mcsbScore -ge 75) { "Green" } elseif ($mcsbScore -ge 50) { "Yellow" } else { "Red" }) Write-Host " └─ Baseline coverage analysis complete" -ForegroundColor Green Write-Host "" # ========== Global Score ========== Write-Host "📊 Computing global compliance score..." -ForegroundColor Cyan $totalBaselinePolicies = $alzBaselineCount + $mcsbBaselineCount $totalDeployedPolicies = $alzDeployed + $mcsbDeployed if ($totalBaselinePolicies -gt 0) { $globalScore = [math]::Round(($totalDeployedPolicies / $totalBaselinePolicies) * 100, 1) } else { $globalScore = 0 } # Determine compliance level $complianceLevel = if ($globalScore -ge 90) { "Excellent" } elseif ($globalScore -ge 75) { "Bon" } elseif ($globalScore -ge 50) { "Moyen" } else { "Faible" } $scoreColor = if ($globalScore -ge 90) { "Green" } elseif ($globalScore -ge 75) { "Cyan" } elseif ($globalScore -ge 50) { "Yellow" } else { "Red" } Write-Host "" Write-Host "╔═══════════════════════════════════╗" -ForegroundColor Cyan Write-Host " 🎯 GLOBAL COMPLIANCE SCORE: $globalScore%" -ForegroundColor $scoreColor Write-Host " Compliance Level: $complianceLevel" -ForegroundColor $scoreColor Write-Host "╚═══════════════════════════════════╝" -ForegroundColor Cyan Write-Host "" # ✅ Nettoyer le cache CSV pour libérer la mémoire $csvCache.Clear() # Return structured result return [PSCustomObject]@{ AlzScore = $alzScore McsbScore = $mcsbScore GlobalScore = $globalScore AlzMetrics = @{ BaselineCount = $alzBaselineCount Deployed = $alzDeployed Missing = $alzMissing } McsbMetrics = @{ BaselineCount = $mcsbBaselineCount Deployed = $mcsbDeployed Missing = $mcsbMissing } GlobalMetrics = @{ TotalBaseline = $totalBaselinePolicies TotalDeployed = $totalDeployedPolicies ComplianceLevel = $complianceLevel } } } |