Private/Export/Export-HtmlReport.ps1
|
function Export-HtmlReport { <# .SYNOPSIS Generates an HTML report of the policy compliance analysis. .DESCRIPTION Creates a comprehensive HTML report with: - Global compliance score with visual gauge and gradient background - Score cards for ALZ and MCSB with progress bars - Metrics pills (Matched, Missing, Version Mismatch, Extra) - Summary tables for custom initiatives, MCSB initiatives, and individual policies - Baseline coverage details (ALZ and MCSB) - Interactive filtering and search with DataTables - Responsive design with modern UI - Lifecycle badges (Deprecated, Preview) This version matches the rich UI from the legacy script. .PARAMETER Summary Array of summary objects from comparisons. .PARAMETER Baseline Array of baseline policy objects. .PARAMETER Scores Scores object from Calculate-ComplianceScores with AlzScore, McsbScore, GlobalScore. .PARAMETER Metrics Additional metrics (assignment counts, details, etc.). .PARAMETER AssignedById Hashtable of assigned policies by ID for status calculation. .PARAMETER AssignedByNormName Hashtable of assigned policies by normalized name for status calculation. .PARAMETER AssignedEffects Hashtable of policy effects for display. .PARAMETER MatchByNameOnly Whether name-only matching was used. .PARAMETER OutputPath Full path to the output HTML file. .PARAMETER LogoPath Optional path to logo image file. .PARAMETER ProjectName Name of the project for branding. .PARAMETER ProjectVersion Version number for display. .PARAMETER Scope Scope of the analysis (MG ID or Subscription ID). .EXAMPLE Export-HtmlReport -Summary $summary ` -Baseline $baseline ` -Scores $scores ` -Metrics $metrics ` -AssignedById $assignedById ` -AssignedByNormName $assignedByNormName ` -AssignedEffects $assignedEffects ` -MatchByNameOnly $true ` -OutputPath "C:\Reports\Report.html" ` -ProjectName "AzurePolicyWatch" ` -ProjectVersion "1.1.0" ` -Scope "/providers/Microsoft.Management/managementGroups/MyMG" Generates HTML report at specified path. .OUTPUTS None. Writes HTML file to disk. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object[]]$Summary, [Parameter(Mandatory)] [object[]]$Baseline, [Parameter()] [object]$Scores = $null, [Parameter()] [hashtable]$Metrics = @{}, [Parameter()] [hashtable]$AssignedById = @{}, [Parameter()] [hashtable]$AssignedByNormName = @{}, [Parameter()] [hashtable]$AssignedEffects = @{}, [Parameter()] [bool]$MatchByNameOnly = $false, [Parameter(Mandatory)] [string]$OutputPath, [Parameter()] [string]$LogoPath = "", [Parameter()] [string]$ProjectName = "AzurePolicyWatch", [Parameter()] [string]$ProjectVersion = "1.1.0", [Parameter()] [string]$Scope = "", [Parameter()] [bool]$ExcludeExtraPolicies = $false ) Write-Host "📄 Generating HTML report..." -ForegroundColor Cyan Write-Verbose "AssignedById count: $($AssignedById.Count)" Write-Verbose "AssignedByNormName count: $($AssignedByNormName.Count)" Write-Verbose "AssignedEffects count: $($AssignedEffects.Count)" if ($null -eq $Scores) { $Scores = [PSCustomObject]@{ AlzScore = 0 McsbScore = 0 GlobalScore = 0 } Write-Warning "Scores not provided, using defaults (0%)" } $totalMatched = if ($Metrics.GlobalMetrics -and $Metrics.GlobalMetrics['TotalDeployed']) { $Metrics.GlobalMetrics['TotalDeployed'] } else { 0 } $alzMissingVal = if ($Metrics.AlzMetrics -and $Metrics.AlzMetrics['Missing']) { $Metrics.AlzMetrics['Missing'] } else { 0 } $mcsbMissingVal = if ($Metrics.McsbMetrics -and $Metrics.McsbMetrics['Missing']) { $Metrics.McsbMetrics['Missing'] } else { 0 } $totalMissing = $alzMissingVal + $mcsbMissingVal $totalExtra = if ($ExcludeExtraPolicies) { 0 } elseif ($Metrics.GlobalMetrics -and $Metrics.GlobalMetrics['TotalExtra']) { $Metrics.GlobalMetrics['TotalExtra'] } else { ($Summary | Where-Object { $_.AssignmentStatus -eq 'Extra' }).Count } $extraFilteredOut = $ExcludeExtraPolicies $totalVersionMismatch = 0 $alzBaselineCount = if ($Metrics.AlzMetrics -and $Metrics.AlzMetrics['BaselineCount']) { $Metrics.AlzMetrics['BaselineCount'] } else { 0 } $alzDeployed = if ($Metrics.AlzMetrics -and $Metrics.AlzMetrics['Deployed']) { $Metrics.AlzMetrics['Deployed'] } else { 0 } $alzMissing = $alzMissingVal $mcsbBaselineCount = if ($Metrics.McsbMetrics -and $Metrics.McsbMetrics['BaselineCount']) { $Metrics.McsbMetrics['BaselineCount'] } else { 0 } $mcsbDeployed = if ($Metrics.McsbMetrics -and $Metrics.McsbMetrics['Deployed']) { $Metrics.McsbMetrics['Deployed'] } else { 0 } $mcsbMissing = $mcsbMissingVal $totalBaseline = if ($Metrics.GlobalMetrics -and $Metrics.GlobalMetrics['TotalBaseline']) { $Metrics.GlobalMetrics['TotalBaseline'] } else { ($alzBaselineCount + $mcsbBaselineCount) } $summaryInitiatives = @($Summary | Where-Object { -not $_.IsIndividualPolicy }) $summaryIndividualPolicies = @($Summary | Where-Object { $_.IsIndividualPolicy }) $summaryCustom = @($summaryInitiatives | Where-Object { -not $_.IsMCSB }) $summaryMcsb = @($summaryInitiatives | Where-Object { $_.IsMCSB }) $assignmentStatusCache = @{} foreach ($policy in $Baseline) { $isAssigned = Test-PolicyAssigned -PolicyDisplayName $policy.PolicyDisplayName ` -PolicyDefinitionId $policy.PolicyDefinitionId ` -AssignedById $AssignedById ` -AssignedByNormName $AssignedByNormName ` -MatchByNameOnly $MatchByNameOnly $assignmentStatusCache[$policy.PolicyDefinitionId] = if ($isAssigned) { "Matched" } else { "Missing" } } $baselineMcsbView = $Baseline | Where-Object { $_.BaselineSources -like "*MCSB*" } | Select-Object PolicyDisplayName, PolicyDefinitionId, Version, PolicyType, BaselineSources, @{Name="AssignmentStatus"; Expression={ $assignmentStatusCache[$_.PolicyDefinitionId] }}, @{Name="Effect"; Expression={ $effect = $null if ($AssignedEffects.ContainsKey($_.PolicyDefinitionId)) { $effect = $AssignedEffects[$_.PolicyDefinitionId] } if (-not $effect) { $nk = Normalize-PolicyName $_.PolicyDisplayName if ($nk -and $AssignedEffects.ContainsKey($nk)) { $effect = $AssignedEffects[$nk] } } if ($effect -and $effect -match "\[parameters\('([^']+)'\)\]") { $effect = "Parameterized" } if ($effect) { $effect } else { "-" } }} $baselineAlzView = $Baseline | Where-Object { $_.BaselineSources -like "*ALZ*" } | Select-Object PolicyDisplayName, PolicyDefinitionId, Version, PolicyType, BaselineSources, @{Name="AssignmentStatus"; Expression={ if (Test-PolicyAssigned -PolicyDisplayName $_.PolicyDisplayName ` -PolicyDefinitionId $_.PolicyDefinitionId ` -AssignedById $AssignedById ` -AssignedByNormName $AssignedByNormName ` -MatchByNameOnly $MatchByNameOnly) { "Matched" } else { "Missing" } }}, @{Name="Effect"; Expression={ $effect = $null if ($AssignedEffects.ContainsKey($_.PolicyDefinitionId)) { $effect = $AssignedEffects[$_.PolicyDefinitionId] } if (-not $effect) { $nk = Normalize-PolicyName $_.PolicyDisplayName if ($nk -and $AssignedEffects.ContainsKey($nk)) { $effect = $AssignedEffects[$nk] } } if ($effect -and $effect -match "\[parameters\('([^']+)'\)\]") { $effect = "Parameterized" } if ($effect) { $effect } else { "-" } }} $scoreColor = if ($Scores.GlobalScore -ge 90) { "#10b981" } # Green elseif ($Scores.GlobalScore -ge 75) { "#06b6d4" } # Cyan elseif ($Scores.GlobalScore -ge 50) { "#f59e0b" } # Yellow/Orange else { "#ef4444" } # Red $complianceLevel = if ($Scores.GlobalScore -ge 90) { "Excellent" } elseif ($Scores.GlobalScore -ge 75) { "Good" } elseif ($Scores.GlobalScore -ge 50) { "Average" } else { "Poor" } # Encode logo as base64 if provided $logoBase64 = "" $logoDataUri = "" # ✅ Initialiser pour éviter erreur si pas de logo if ($LogoPath -and (Test-Path $LogoPath)) { try { Write-Verbose "Encoding logo from: $LogoPath" $logoBytes = [System.IO.File]::ReadAllBytes($LogoPath) $logoBase64 = [Convert]::ToBase64String($logoBytes) $logoExt = [System.IO.Path]::GetExtension($LogoPath).TrimStart('.') $logoDataUri = "data:image/$logoExt;base64,$logoBase64" Write-Verbose "✅ Logo encoded successfully (size: $($logoBytes.Length) bytes)" } catch { Write-Warning "Failed to encode logo: $($_.Exception.Message)" $logoDataUri = "" } } else { if ($LogoPath) { Write-Warning "Logo file not found: $LogoPath" } else { Write-Verbose "No logo provided, using default icon (🔍) in hero section" } } # Build HTML tables $columnsToHide = @('IsMCSB', 'IsExtra', 'IsIndividualPolicy') $sumCustomHtml = if ($summaryCustom.Count -gt 0) { $filtered = $summaryCustom | Select-Object * -ExcludeProperty $columnsToHide Build-HtmlContent -Data $filtered -TableId "tblSummaryCustom" } else { "<p>No custom initiatives found.</p>" } $sumMcsbHtml = if ($summaryMcsb.Count -gt 0) { $filtered = $summaryMcsb | Select-Object * -ExcludeProperty $columnsToHide Build-HtmlContent -Data $filtered -TableId "tblSummaryMcsb" } else { "<p>No MCSB initiatives found.</p>" } $sumIndividualHtml = if ($summaryIndividualPolicies.Count -gt 0) { $filtered = $summaryIndividualPolicies | Select-Object * -ExcludeProperty $columnsToHide Build-HtmlContent -Data $filtered -TableId "tblIndividual" } else { "<p>No individual policies found.</p>" } $mcsbHtml = if ($baselineMcsbView.Count -gt 0) { Build-HtmlContent -Data $baselineMcsbView -TableId "tblMCSB" } else { "<p>No MCSB baseline policies.</p>" } $alzHtml = if ($baselineAlzView.Count -gt 0) { Build-HtmlContent -Data $baselineAlzView -TableId "tblALZ" } else { "<p>No ALZ baseline policies.</p>" } # Apply table CSS class for styling $sumCustomHtml = $sumCustomHtml -replace '<table>', '<table class="data-table">' $sumCustomHtml = $sumCustomHtml -replace '<td>(-\d+)</td>', '<td style="color: #d32f2f; font-weight: bold; background-color: #ffebee;">$1</td>' $sumMcsbHtml = $sumMcsbHtml -replace '<table>', '<table class="data-table">' $sumMcsbHtml = $sumMcsbHtml -replace '<td>(-\d+)</td>', '<td style="color: #d32f2f; font-weight: bold; background-color: #ffebee;">$1</td>' if ($sumIndividualHtml -ne "<p>No individual policies found.</p>") { $sumIndividualHtml = $sumIndividualHtml -replace '<table>', '<table class="data-table">' } $mcsbHtml = $mcsbHtml -replace '<table>', '<table class="data-table">' $alzHtml = $alzHtml -replace '<table>', '<table class="data-table">' # Build comprehensive HTML document with rich UI $style = @" <style> /* ========== GLOBAL STYLES ========== */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; background: #f5f7fa; /* ✅ CHANGEMENT: Fond gris clair au lieu de violet */ padding: 20px; min-height: 100vh; } .container { max-width: 1800px; /* ✅ CHANGEMENT: Élargi pour éviter scroll horizontal */ margin: 0 auto; } /* ========== HEADER / BRAND ========== */ .brand { background: white; padding: 24px 32px; border-radius: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; border: 1px solid #e0e4e8; } .brand-logo { height: 50px; width: auto; } .brand-text { display: flex; flex-direction: column; } .brand-title { font-size: 32px; font-weight: 800; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .brand-sub { color: #666; font-size: 14px; margin-top: 4px; font-weight: 500; } .version-badge { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 10px 24px; border-radius: 30px; font-size: 14px; font-weight: 700; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); letter-spacing: 0.5px; } /* ========== HERO SCORE SECTION ========== */ .hero-score { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 50px; /* ✅ CHANGEMENT: Réduit pour tenir sur une page */ border-radius: 16px; text-align: center; margin-bottom: 24px; box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3); position: relative; overflow: hidden; } .hero-score::before { content: ''; position: absolute; top: -50%; right: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); animation: pulse 20s ease-in-out infinite; } /* ========== LOGO ANIMÉ EN ARRIÈRE-PLAN ========== */ @keyframes floatLogo { 0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.15; } 50% { transform: translate(-50%, -50%) scale(1.05); /* ✅ Moins de grossissement */ opacity: 0.18; /* ✅ Moins de variation d'opacité */ } } /* Animation pulse pour l'effet radial (déjà présent) */ @keyframes pulse { 0%, 100% { transform: scale(1) rotate(0deg); opacity: 0.3; } 50% { transform: scale(1.1) rotate(180deg); opacity: 0.5; } } /* Logo par défaut (icône) avec animation */ .hero-score::after { content: '🔍'; position: absolute; font-size: 200px; top: 50%; left: 50%; z-index: 0; color: white; opacity: 0.08; transform: translate(-50%, -50%); animation: floatLogo 40s ease-in-out infinite; pointer-events: none; } /* Override pour logo image fourni */ .hero-score[data-has-logo="true"]::after { content: ''; font-size: 0; /* Cache l'icône texte */ background-image: var(--logo-url); background-size: contain; background-repeat: no-repeat; background-position: center; width: 550px; height: 550px; opacity: 0.18; filter: blur(0px); /* L'animation floatLogo est héritée automatiquement */ } .score-value { font-size: 72px; /* ✅ CHANGEMENT: Réduit pour compacité */ font-weight: 900; margin-bottom: 16px; text-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); position: relative; z-index: 1; letter-spacing: -2px; } .score-label { font-size: 18px; /* ✅ CHANGEMENT: Réduit */ font-weight: 700; text-transform: uppercase; letter-spacing: 3px; margin-bottom: 16px; opacity: 0.95; position: relative; z-index: 1; } .score-badge { background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(10px); padding: 10px 24px; border-radius: 25px; display: inline-block; margin-top: 12px; font-weight: 700; font-size: 16px; position: relative; z-index: 1; border: 2px solid rgba(255, 255, 255, 0.3); } .progress-bar-container { background: rgba(255, 255, 255, 0.2); border-radius: 10px; height: 20px; overflow: hidden; margin-top: 20px; box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2); position: relative; z-index: 1; } .progress-bar { background: linear-gradient(90deg, #ffffff 0%, #f0f0f0 100%); height: 100%; border-radius: 10px; transition: width 2s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 2px 8px rgba(255, 255, 255, 0.5); } .progress-text { text-align: center; margin-top: 10px; font-size: 14px; opacity: 0.95; position: relative; z-index: 1; font-weight: 600; } /* ========== METRICS PILLS ========== */ .metrics-pills { display: flex; justify-content: center; gap: 14px; flex-wrap: wrap; margin-top: 20px; position: relative; z-index: 1; } .pill { background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px); padding: 10px 20px; border-radius: 20px; font-size: 14px; font-weight: 600; border: 1px solid rgba(255, 255, 255, 0.3); transition: all 0.3s ease; } .pill:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } .pill .num { font-size: 18px; font-weight: 900; margin-right: 6px; } .pill.matched .num { color: #4caf50; } .pill.missing .num { color: #f44336; } .pill.version .num { color: #ff9800; } .pill.extra .num { color: #2196f3; } /* ========== SCORE CARDS ========== */ .score-cards { display: grid; grid-template-columns: repeat(2, 1fr); /* ✅ CHANGEMENT: 2 colonnes fixes */ gap: 20px; margin-bottom: 24px; } .score-card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border-top: 4px solid; transition: all 0.3s ease; border: 1px solid #e0e4e8; } .score-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); } .score-card.alz { border-top-color: #0078D4; } .score-card.mcsb { border-top-color: #4caf50; } .card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; font-size: 12px; color: #666; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; } .card-score { font-size: 48px; font-weight: 900; margin-bottom: 10px; letter-spacing: -1px; } .score-card.alz .card-score { color: #0078D4; } .score-card.mcsb .card-score { color: #4caf50; } .card-subtitle { font-size: 12px; color: #999; font-weight: 600; } .card-progress { background: #f0f0f0; border-radius: 10px; height: 8px; overflow: hidden; margin: 14px 0; } .card-progress-bar { height: 100%; border-radius: 10px; transition: width 1.5s cubic-bezier(0.4, 0, 0.2, 1); } .card-progress-bar.alz { background: linear-gradient(90deg, #0078D4 0%, #4da6ff 100%); } .card-progress-bar.mcsb { background: linear-gradient(90deg, #4caf50 0%, #81c784 100%); } .card-stats { display: flex; justify-content: space-between; font-size: 12px; color: #666; font-weight: 600; } /* ========== FILTERS ========== */ #filters { background: white; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); display: flex; gap: 14px; align-items: center; flex-wrap: wrap; border: 1px solid #e0e4e8; } #globalSearch { flex: 1; min-width: 280px; padding: 10px 16px; border: 2px solid #e0e4e8; border-radius: 10px; font-size: 14px; font-weight: 500; transition: all 0.3s ease; } #globalSearch:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } #statusFilter { padding: 10px 16px; border: 2px solid #e0e4e8; border-radius: 10px; font-size: 14px; font-weight: 500; background: white; cursor: pointer; transition: all 0.3s ease; } #statusFilter:hover { border-color: #667eea; } label { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #666; font-weight: 600; cursor: pointer; } label:hover { color: #667eea; } label input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; } #resetFilters { padding: 10px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 700; cursor: pointer; transition: all 0.3s ease; } #resetFilters:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); } /* ========== TABLES ========== */ .section { overflow-x: auto; background: white; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #e0e4e8; } .section h2 { color: #212529; font-size: 20px; margin-bottom: 20px; font-weight: 800; } .section p { color: #666; font-size: 14px; line-height: 1.6; } table { width: 100%; border-collapse: collapse; font-size: 13px; /* ✅ CHANGEMENT: Réduit pour compacité */ } th, td { padding: 12px 14px; /* ✅ CHANGEMENT: Padding réduit */ text-align: left; } th { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 700; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; position: sticky; top: 0; z-index: 10; } /* ✅ ZEBRA STRIPING */ tr:nth-child(even) { background-color: #f8f9fa; } tr:nth-child(odd) { background-color: #ffffff; } tr:hover { background-color: #e3f2fd !important; transition: background-color 0.2s ease; } tr.matched td { background-color: #e8f5e9 !important; border-left: 4px solid #4caf50; } tr.matched td:last-child { border-right: 4px solid #4caf50; } tr.extra td { background-color: #e3f2fd !important; border-left: 4px solid #2196f3; } tr.extra td:last-child { border-right: 4px solid #2196f3; } tr.missing td { background-color: #ffebee !important; border-left: 4px solid #f44336; } tr.missing td:last-child { border-right: 4px solid #f44336; } .data-table td { padding: 12px 14px; border-bottom: 1px solid #e0e4e8; font-size: 13px; color: #333; font-weight: 500; } .data-table tr:last-child td { border-bottom: none; } /* ========== LIFECYCLE BADGES - COULEURS DIFFÉRENTES ========== */ /* ✅ CHANGEMENT: Couleurs très distinctes pour Deprecated et Preview */ tr.deprecated { background-color: #fff3e0 !important; /* Orange très clair */ } tr.preview { background-color: #e8eaf6 !important; /* Indigo très clair - DIFFÉRENT */ } tr.deprecated td { border-left: 4px solid #ff6f00; /* Orange foncé */ } tr.preview td { border-left: 4px solid #3f51b5; /* Indigo - DIFFÉRENT */ } .badge-deprecated { background: linear-gradient(135deg, #ff6f00 0%, #ff8f00 100%); /* Orange */ color: white; padding: 5px 12px; border-radius: 14px; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; display: inline-block; box-shadow: 0 2px 6px rgba(255, 111, 0, 0.4); } .badge-preview { background: linear-gradient(135deg, #3f51b5 0%, #5c6bc0 100%); /* Indigo - DIFFÉRENT */ color: white; padding: 5px 12px; border-radius: 14px; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; display: inline-block; box-shadow: 0 2px 6px rgba(63, 81, 181, 0.4); } .badge-deprecated::before { content: '⚠️ '; } .badge-preview::before { content: '🧪 '; } /* ✅ CHANGEMENT: Icône différente */ /* ========== TABS ========== */ .tabs { background: white; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); overflow: hidden; border: 1px solid #e0e4e8; } .tab-header { display: flex; border-bottom: 2px solid #e0e4e8; background: #f8f9fa; } .tab-btn { flex: 1; padding: 16px 28px; background: transparent; border: none; cursor: pointer; font-size: 15px; font-weight: 700; color: #666; transition: all 0.3s ease; position: relative; } .tab-btn:hover { background: rgba(102, 126, 234, 0.05); color: #667eea; } .tab-btn.active { color: #667eea; background: white; } .tab-btn.active::after { content: ''; position: absolute; bottom: -2px; left: 0; right: 0; height: 3px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .tab-content { display: none; padding: 0; } .tab-content.active { display: block; } /* ========== DETAILS/SUMMARY ========== */ details { margin-bottom: 20px; } details summary { cursor: pointer; padding: 16px 20px; background: #f8f9fa; border-radius: 10px; font-weight: 700; font-size: 15px; transition: all 0.3s ease; border: 2px solid #e0e4e8; } details summary:hover { background: #e9ecef; border-color: #667eea; color: #667eea; } details[open] { border: 2px solid #667eea; border-radius: 10px; padding: 10px; } details[open] summary { border-bottom: 2px solid #e0e4e8; margin-bottom: 14px; border-radius: 10px 10px 0 0; } /* ========== FOOTER ========== */ .footer { background: white; border-radius: 12px; padding: 20px; text-align: center; color: #666; font-size: 13px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #e0e4e8; font-weight: 500; } .footer p { margin: 6px 0; } /* ========== RESPONSIVE DESIGN ========== */ @media (max-width: 1400px) { .container { max-width: 100%; } } @media (max-width: 768px) { body { padding: 12px; } .brand { flex-direction: column; gap: 14px; text-align: center; } .hero-score { padding: 32px 20px; } .score-value { font-size: 56px; } .score-cards { grid-template-columns: 1fr; } #filters { flex-direction: column; align-items: stretch; } #globalSearch, #statusFilter, #resetFilters { width: 100%; } table { font-size: 12px; } th, td { padding: 10px 12px; } } /* ========== PRINT STYLES (Éviter coupures de page) ========== */ @media print { body { background: white; } .section { page-break-inside: avoid; } table { page-break-inside: auto; } tr { page-break-inside: avoid; page-break-after: auto; } } </style> "@ $htmlContent = @" <!DOCTYPE html> <html lang="fr"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>$ProjectName - Policy Compliance Report</title> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css"> $style </head> <body> <div class="container"> <!-- Brand Header --> <div class="brand"> <div style="display: flex; align-items: center; gap: 20px;"> $(if ($logoDataUri) { "<img src='$logoDataUri' alt='Logo' class='brand-logo'>" } else { "" }) <div class="brand-text"> <div class="brand-title">$ProjectName</div> <div class="brand-sub">Azure Policy Compliance Analysis</div> </div> </div> <span class="version-badge">v$ProjectVersion</span> </div> <!-- Hero Score --> <div class="hero-score" $(if ($logoDataUri) { "data-has-logo='true' style='--logo-url: url($logoDataUri);'" } else { "" })> <div class="score-value">$([math]::Round($Scores.GlobalScore, 1))%</div> <div class="score-label">Compliance Score</div> <div class="score-badge">$complianceLevel</div> <div class="progress-bar-container"> <div class="progress-bar" style="width: $($Scores.GlobalScore)%;"></div> </div> <div class="progress-text">$totalMatched / $totalBaseline policies</div> <div class="metrics-pills"> <div class="pill matched"><span class="num">$totalMatched</span> Match</div> <div class="pill missing"><span class="num">$totalMissing</span> Missing</div> <div class="pill version"><span class="num">$totalVersionMismatch</span> Version Mismatch</div> $(if ($extraFilteredOut) { '<div class="pill extra" style="opacity: 0.5;"><span class="num">-</span> Extra (filtered)</div>' } else { "<div class='pill extra'><span class='num'>$totalExtra</span> Extra</div>" }) </div> </div> <!-- Score Cards --> <div class="score-cards"> <div class="score-card alz"> <div class="card-header"> <span>🏛️ AZURE LANDING ZONES</span> </div> <div class="card-score">$([math]::Round($Scores.AlzScore, 1))%</div> <div class="card-subtitle">$alzDeployed / $alzBaselineCount policies</div> <div class="card-progress"> <div class="card-progress-bar alz" style="width: $($Scores.AlzScore)%;"></div> </div> <div class="card-stats"> <span>✅ Matched: $alzDeployed</span> <span>❌ Missing: $alzMissing</span> </div> </div> <div class="score-card mcsb"> <div class="card-header"> <span>🔒 MICROSOFT CLOUD SECURITY BENCHMARK</span> </div> <div class="card-score">$([math]::Round($Scores.McsbScore, 1))%</div> <div class="card-subtitle">$mcsbDeployed / $mcsbBaselineCount policies</div> <div class="card-progress"> <div class="card-progress-bar mcsb" style="width: $($Scores.McsbScore)%;"></div> </div> <div class="card-stats"> <span>✅ Matched: $mcsbDeployed</span> <span>❌ Missing: $mcsbMissing</span> </div> </div> </div> <!-- Filters --> <div id="filters"> <input type="text" id="globalSearch" placeholder="Search (name / id / text)..."> <select id="statusFilter"> <option value="Tous">All</option> <option value="Matched">Matched</option> <option value="Missing">Missing</option> <option value="Extra">Extra</option> </select> <label> <input type="checkbox" id="chkDeprecated"> Hide Deprecated </label> <label> <input type="checkbox" id="chkPreview"> Hide Preview </label> <button id="resetFilters">Reset</button> </div> <!-- Tabs Container --> <div class="tabs"> <!-- Tab Header --> <div class="tab-header"> <button class="tab-btn active" data-tab="environment">🏢 My Environment</button> <button class="tab-btn" data-tab="recommendations">📋 Recommendations</button> </div> <!-- Tab Content: Mon environnement --> <div class="tab-content active" id="environment"> <!-- Custom Initiatives Section --> <details class="custom-initiatives" open> <summary>📊 Custom Initiatives - Click to collapse</summary> <div class="section"> $sumCustomHtml </div> </details> <!-- Individual Policies Section --> <details class="individual-policies" open> <summary>📌 Individual Policies - Click to collapse</summary> <div class="section"> $sumIndividualHtml </div> </details> </div> <!-- Tab Content: Recommandations --> <div class="tab-content" id="recommendations"> <!-- MCSB Deployment Section --> <details open> <summary>🔒 MCSB Deployment - Click to expand</summary> <div class="section"> <h2>🔒 MCSB Initiatives Summary</h2> $sumMcsbHtml <h2 style="margin-top: 30px;">🎯 MCSB Baseline Details</h2> $mcsbHtml </div> </details> <!-- ALZ Baseline Section --> <details open> <summary>🚀 ALZ Baseline - Click to expand</summary> <div class="section"> <h2>🚀 ALZ Baseline Details</h2> $alzHtml </div> </details> </div> </div> <!-- Footer --> <div class="footer"> <p>Generated on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Scope: $Scope</p> <p>$ProjectName v$ProjectVersion | Azure Policy Compliance Analysis</p> </div> </div> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script> <script> `$(document).ready(function(){ // Find status column index dynamically by header name function findColumnIndex(table, columnName) { var colIndex = -1; table.find('thead tr th, tr:first th, tr:first td').each(function(idx) { var headerText = `$(this).text().trim().toUpperCase(); if (headerText === columnName.toUpperCase()) { colIndex = idx; return false; } }); return colIndex; } // Apply coloring and classes to tables var tableIds = ['tblSummaryCustom', 'tblSummaryMcsb', 'tblIndividual', 'tblMCSB', 'tblALZ']; tableIds.forEach(function(tableId){ var tbl = `$('#' + tableId); if(tbl.length){ // Find AssignmentStatus column dynamically var statusColIndex = findColumnIndex(tbl, 'ASSIGNMENTSTATUS'); if (statusColIndex === -1) { console.log(tableId + ': AssignmentStatus column not found, trying index 5'); statusColIndex = 5; } console.log(tableId + ': Using status column index ' + statusColIndex); var rowCount = 0; var matchedCount = 0; var extraCount = 0; var missingCount = 0; // Iterate all data rows tbl.find('tr').each(function(idx){ var row = `$(this); var cells = row.find('td'); // Skip header rows if(cells.length === 0) return; rowCount++; var statusCell = cells.eq(statusColIndex); var status = statusCell.text().trim(); console.log('Row ' + idx + ': status = "' + status + '"'); // Apply row classes based on status if(status === 'Matched'){ row.addClass('matched'); matchedCount++; } else if(status === 'Extra'){ row.addClass('extra'); extraCount++; } else if(status === 'Missing'){ row.addClass('missing'); missingCount++; } // Check for Deprecated/Preview in all cells cells.each(function(){ var txt = `$(this).text(); if(txt.includes('Deprecated')){ row.addClass('deprecated'); `$(this).html(txt.replace('Deprecated','<span class="badge-deprecated">Deprecated</span>')); } if(txt.includes('Preview')){ row.addClass('preview'); `$(this).html(txt.replace('Preview','<span class="badge-preview">Preview</span>')); } }); }); console.log(tableId + ': ' + rowCount + ' rows - ' + matchedCount + ' matched, ' + extraCount + ' extra, ' + missingCount + ' missing'); } }); // Global filters function applyFiltersAllTables(){ var searchVal = `$('#globalSearch').val().toLowerCase(); var statusVal = `$('#statusFilter').val(); var hideDeprecated = `$('#chkDeprecated').is(':checked'); var hidePreview = `$('#chkPreview').is(':checked'); console.log('Filter applied: status=' + statusVal); `$('table tr').each(function(){ var row = `$(this); // Skip header rows if (row.find('th').length > 0) return; if (row.find('td').length === 0) return; var show = true; // Text search if(searchVal){ var rowText = row.text().toLowerCase(); if(rowText.indexOf(searchVal) === -1) show = false; } // Status filter if(show && statusVal !== 'Tous'){ var hasClass = row.hasClass(statusVal.toLowerCase()); console.log('Row classes: ' + row.attr('class') + ', looking for: ' + statusVal.toLowerCase() + ', hasClass: ' + hasClass); if(!hasClass) show = false; } // Deprecated filter if(show && hideDeprecated && row.hasClass('deprecated')) show = false; // Preview filter if(show && hidePreview && row.hasClass('preview')) show = false; row.toggle(show); }); } `$('#globalSearch').on('keyup', applyFiltersAllTables); `$('#statusFilter').on('change', applyFiltersAllTables); `$('#chkDeprecated, #chkPreview').on('change', applyFiltersAllTables); `$('#resetFilters').on('click', function(){ `$('#globalSearch').val(''); `$('#statusFilter').val('Tous'); `$('#chkDeprecated, #chkPreview').prop('checked', false); applyFiltersAllTables(); }); // Tab switching `$('.tab-btn').on('click', function(){ var targetTab = `$(this).data('tab'); `$('.tab-btn').removeClass('active'); `$(this).addClass('active'); `$('.tab-content').removeClass('active'); `$('#' + targetTab).addClass('active'); }); }); </script> </script> </body> </html> "@ try { $htmlContent | Out-File -FilePath $OutputPath -Encoding UTF8 Write-Host (" ✅ HTML report generated: {0}" -f (Split-Path $OutputPath -Leaf)) -ForegroundColor Green } catch { Write-Warning "Failed to export HTML report: $($_.Exception.Message)" } } |