Private/Export/Export-HierarchicalCsv.ps1
|
function Export-HierarchicalCsv { <# .SYNOPSIS Exports a hierarchical view of initiatives and their member policies. .DESCRIPTION Creates a two-level hierarchical CSV where each initiative is followed by its member policies. Includes comparison status, versions, effects, and lifecycle info. This provides a comprehensive view of all assignments with their policy details in a single denormalized file. ⚡ OPTIMIZED VERSION - Uses hashtable indexes for O(1) lookups instead of Where-Object. .PARAMETER Summary Array of summary objects from comparisons. .PARAMETER Details Array of detail objects (Missing, Extra, VersionMismatch). .PARAMETER Baseline Array of baseline policy objects. .PARAMETER InitiativesFolder Path to folder containing Initiative-{Name}-Policies.csv files. .PARAMETER PolicyDefinitionCache Hashtable cache for policy definitions. .PARAMETER OutputPath Full path to the output CSV file. .EXAMPLE Export-HierarchicalCsv -Summary $summary ` -Details $details ` -Baseline $baseline ` -InitiativesFolder $initiativesFolder ` -PolicyDefinitionCache $script:policyDefinitionCache ` -OutputPath "C:\Reports\Hierarchical_Initiatives_Policies.csv" Exports hierarchical view to specified CSV file. .OUTPUTS None. Writes CSV file to disk with initiative and policy rows. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object[]]$Summary, [Parameter(Mandatory)] [object[]]$Details, [Parameter(Mandatory)] [object[]]$Baseline, [Parameter(Mandatory)] [string]$InitiativesFolder, [Parameter(Mandatory)] [hashtable]$PolicyDefinitionCache, [Parameter(Mandatory)] [string]$OutputPath ) Write-Verbose "Building hierarchical CSV..." # ========== ✅ OPTIMISATION: Créer des index AVANT la boucle ========== Write-Verbose " Building Details and Baseline indexes for fast lookups..." # ✅ Index des Details par "AssignmentName|PolicyDefinitionId" $detailsIndex = @{} foreach ($detail in $Details) { $key = "$($detail.AssignmentName)|$($detail.PolicyDefinitionId)" if (-not $detailsIndex.ContainsKey($key)) { $detailsIndex[$key] = $detail } } Write-Verbose " ├─ Details index: $($detailsIndex.Count) entries" # ✅ Index du Baseline par PolicyDefinitionId $baselineIndex = @{} foreach ($bp in $Baseline) { if (-not $baselineIndex.ContainsKey($bp.PolicyDefinitionId)) { $baselineIndex[$bp.PolicyDefinitionId] = $bp } } Write-Verbose " └─ Baseline index: $($baselineIndex.Count) entries" # ✅ Cache des CSV déjà lus (évite Import-Csv répétés) $csvCache = @{} $hierarchicalRows = @() # Process initiatives only (exclude individual policies) $summaryInitiatives = $Summary | Where-Object { -not $_.IsIndividualPolicy } Write-Verbose " Processing $($summaryInitiatives.Count) initiatives..." $progressCounter = 0 foreach ($summaryRow in $summaryInitiatives) { $progressCounter++ # Progress every 10 initiatives if ($progressCounter % 10 -eq 0 -or $progressCounter -eq $summaryInitiatives.Count) { Write-Progress -Activity "Generating Hierarchical CSV" ` -Status "Processing initiative $progressCounter of $($summaryInitiatives.Count)" ` -PercentComplete (($progressCounter / $summaryInitiatives.Count) * 100) } # Add initiative row $hierarchicalRows += [PSCustomObject]@{ Type = "Initiative" InitiativeName = $summaryRow.AssignmentDisplayName InitiativeScope = $summaryRow.AssignmentScope InitiativeId = $summaryRow.InitiativeDefinitionId TotalPolicies = $summaryRow.TotalPolicies_Assignment Matched = $summaryRow.InCommon Missing = $summaryRow.TotalPolicies_Assignment - $summaryRow.InCommon - $summaryRow.ExtraInAssignment_Count Extra = $summaryRow.ExtraInAssignment_Count VersionMismatches = $summaryRow.VersionDiffs_Count PolicyName = "" PolicyId = "" Effect = "" BaselineSource = "" Version = "" Status = "" DeployedVersion = "" Lifecycle = "" Category = "Uncategorized" } # Add policy rows $initiativeCsvPath = Join-Path $InitiativesFolder "Initiative-$($summaryRow.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 ($policy in $initiativePolicies) { # ✅ OPTIMISÉ: Lookup O(1) au lieu de Where-Object O(n) $detailKey = "$($summaryRow.AssignmentName)|$($policy.PolicyDefinitionId)" $policyDetail = $detailsIndex[$detailKey] $baselinePolicy = $baselineIndex[$policy.PolicyDefinitionId] # Status WITH icons (will be properly encoded in UTF-8 with BOM) $status = if ($policyDetail) { switch ($policyDetail.DifferenceType) { 'MissingFromAssignment' { '❌ Missing' } 'ExtraInAssignment' { 'ℹ️ Extra' } 'VersionMismatch' { '⚠️ Version Mismatch' } default { '✅ Match' } } } else { '✅ Match' } # Resolve effect $effect = "N/A" if ($policy.Effect -and $policy.Effect -ne "N/A") { $effect = $policy.Effect } elseif ($baselinePolicy -and $baselinePolicy.Effect -and $baselinePolicy.Effect -ne "N/A") { $effect = $baselinePolicy.Effect } else { # Fallback: try to load from cache try { $pol = Get-PolicyDefinitionCached -PolicyDefinitionId $policy.PolicyDefinitionId ` -Cache $PolicyDefinitionCache $effect = Resolve-PolicyEffect -Ref $null ` -PolicyDefinition $pol ` -PolicyDisplayName $pol.DisplayName ` -PolicyDefinitionId $pol.Id } catch { Write-Debug "Cannot load policy to resolve effect: $($policy.PolicyDefinitionId)" $effect = "N/A" } } $hierarchicalRows += [PSCustomObject]@{ Type = "Policy" InitiativeName = $summaryRow.AssignmentDisplayName InitiativeScope = "" InitiativeId = "" TotalPolicies = "" Matched = "" Missing = "" Extra = "" VersionMismatches = "" PolicyName = $policy.PolicyDisplayName PolicyId = $policy.PolicyDefinitionId Effect = $effect BaselineSource = if ($policyDetail) { $policyDetail.BaselineSources } elseif ($baselinePolicy) { $baselinePolicy.BaselineSources } else { "Custom" } Version = if ($policyDetail) { $policyDetail.BaselineVersion } elseif ($baselinePolicy) { $baselinePolicy.Version } else { $policy.Version } Status = $status DeployedVersion = $policy.Version Lifecycle = if ($policy.PolicyDisplayName -match '\[?Deprecated\]?|\(Deprecated\)') { 'Deprecated' } elseif ($policy.PolicyDisplayName -match '\[?Preview\]?|\(Preview\)') { 'Preview' } else { '' } Category = if ($policy.Category -and $policy.Category.Trim() -ne '') { $policy.Category } elseif ($baselinePolicy -and $baselinePolicy.Category -and $baselinePolicy.Category.Trim() -ne '') { $baselinePolicy.Category } else { "Uncategorized" } } } } else { Write-Warning "Initiative CSV not found: $initiativeCsvPath" } } Write-Progress -Activity "Generating Hierarchical CSV" -Completed # ✅ Nettoyer le cache CSV pour libérer la mémoire $csvCache.Clear() try { # Force UTF-8 with BOM for proper Unicode icon rendering # PowerShell 7+: Use utf8BOM, PowerShell 5.1: UTF8 already includes BOM if ($PSVersionTable.PSVersion.Major -ge 7) { $hierarchicalRows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8BOM } else { # PowerShell 5.1: UTF8 = UTF-8 with BOM by default $hierarchicalRows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 } Write-Host ("✅ Hierarchical CSV exported: {0}" -f (Split-Path $OutputPath -Leaf)) -ForegroundColor Green Write-Host (" ├─ Total rows: {0}" -f $hierarchicalRows.Count) -ForegroundColor DarkCyan Write-Host (" ├─ Initiatives: {0}" -f ($hierarchicalRows | Where-Object { $_.Type -eq 'Initiative' }).Count) -ForegroundColor DarkCyan Write-Host (" └─ Policies: {0}" -f ($hierarchicalRows | Where-Object { $_.Type -eq 'Policy' }).Count) -ForegroundColor DarkCyan } catch { Write-Warning "Failed to export hierarchical CSV: $($_.Exception.Message)" } } |