Private/New-HtmlDashboard.ps1
|
function New-HtmlDashboard { <# .SYNOPSIS Generates an HTML dashboard report from permission audit results. .DESCRIPTION Internal function that compiles all audit section results into a single-file HTML dashboard with a dark theme and teal accent color. The report is self-contained with inline CSS and requires no external dependencies. .PARAMETER Summary The audit summary object with counts and metadata. .PARAMETER DirectUserResults Results from Get-DirectUserACEs. .PARAMETER BrokenInheritResults Results from Get-BrokenInheritance. .PARAMETER NestedGroupResults Results from Get-NestedGroupReport. .PARAMETER ShareResults Results from Get-SharePermissionReport. .PARAMETER PathsAudited The original paths that were audited. .PARAMETER ReportFile Full path where the HTML file will be written. #> [CmdletBinding()] param( [Parameter(Mandatory)] $Summary, [Parameter(Mandatory)] $DirectUserResults, [Parameter(Mandatory)] $BrokenInheritResults, [Parameter(Mandatory)] $NestedGroupResults, [Parameter(Mandatory)] $ShareResults, [Parameter(Mandatory)] [string[]]$PathsAudited, [Parameter(Mandatory)] [string]$ReportFile ) $accentColor = '#39d353' $accentDark = '#2da843' $bgDark = '#0d1117' $bgCard = '#161b22' $bgTable = '#1c2129' $borderColor = '#30363d' $textPrimary = '#e6edf3' $textSecondary = '#8b949e' $dangerColor = '#f85149' $warningColor = '#d29922' $successColor = '#39d353' function ConvertTo-HtmlTable { param( [Parameter(Mandatory)] $Data, [string]$Id = 'table' ) if (-not $Data -or $Data.Count -eq 0) { return '<p class="no-data">No items found.</p>' } $properties = $Data[0].PSObject.Properties.Name $sb = [System.Text.StringBuilder]::new() [void]$sb.Append("<table id=`"$Id`"><thead><tr>") foreach ($prop in $properties) { [void]$sb.Append("<th>$([System.Web.HttpUtility]::HtmlEncode($prop))</th>") } [void]$sb.Append('</tr></thead><tbody>') foreach ($row in $Data) { $finding = '' if ($row.PSObject.Properties['Finding']) { $finding = $row.Finding } $rowClass = if ($finding -and $finding -ne 'OK') { ' class="finding"' } else { '' } [void]$sb.Append("<tr$rowClass>") foreach ($prop in $properties) { $value = [System.Web.HttpUtility]::HtmlEncode($row.$prop) $cellClass = '' if ($prop -eq 'Finding') { if ($value -ne 'OK' -and $value) { $cellClass = ' class="cell-danger"' } else { $cellClass = ' class="cell-ok"' } } [void]$sb.Append("<td$cellClass>$value</td>") } [void]$sb.Append('</tr>') } [void]$sb.Append('</tbody></table>') $sb.ToString() } # Count findings per section $directFindings = @($DirectUserResults | Where-Object { $_.Finding -eq 'DIRECT USER ACE' }).Count $brokenFindings = @($BrokenInheritResults | Where-Object { $_.Finding -like 'INHERITANCE DISABLED*' }).Count $nestingFindings = @($NestedGroupResults | Where-Object { $_.Finding -like 'EXCESSIVE NESTING*' }).Count $shareFindings = @($ShareResults | Where-Object { $_.Finding -ne 'OK' }).Count $pathsList = ($PathsAudited | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" $directTable = ConvertTo-HtmlTable -Data $DirectUserResults -Id 'directUserTable' $brokenTable = ConvertTo-HtmlTable -Data $BrokenInheritResults -Id 'brokenInheritTable' $nestingTable = ConvertTo-HtmlTable -Data $NestedGroupResults -Id 'nestingTable' $shareTable = ConvertTo-HtmlTable -Data $ShareResults -Id 'shareTable' $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>NTFS Permission Audit Report - $($Summary.AuditDate)</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: $bgDark; color: $textPrimary; line-height: 1.6; padding: 2rem; } .container { max-width: 1400px; margin: 0 auto; } header { border-bottom: 2px solid $accentColor; padding-bottom: 1.5rem; margin-bottom: 2rem; } header h1 { font-size: 1.8rem; font-weight: 600; color: $accentColor; } header .subtitle { color: $textSecondary; font-size: 0.95rem; margin-top: 0.25rem; } .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; } .summary-card { background: $bgCard; border: 1px solid $borderColor; border-radius: 8px; padding: 1.25rem; text-align: center; } .summary-card .value { font-size: 2rem; font-weight: 700; color: $accentColor; } .summary-card .value.danger { color: $dangerColor; } .summary-card .value.warning { color: $warningColor; } .summary-card .value.success { color: $successColor; } .summary-card .label { color: $textSecondary; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.25rem; } .section { background: $bgCard; border: 1px solid $borderColor; border-radius: 8px; margin-bottom: 1.5rem; overflow: hidden; } .section-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid $borderColor; cursor: pointer; user-select: none; } .section-header:hover { background: rgba(57, 211, 83, 0.05); } .section-header h2 { font-size: 1.15rem; font-weight: 600; } .badge { display: inline-block; padding: 0.2rem 0.65rem; border-radius: 12px; font-size: 0.8rem; font-weight: 600; } .badge-danger { background: rgba(248, 81, 73, 0.2); color: $dangerColor; } .badge-warning { background: rgba(210, 153, 34, 0.2); color: $warningColor; } .badge-success { background: rgba(57, 211, 83, 0.2); color: $successColor; } .section-body { padding: 1rem 1.5rem; overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } th { background: $bgTable; color: $textSecondary; font-weight: 600; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid $borderColor; position: sticky; top: 0; } td { padding: 0.6rem 1rem; border-bottom: 1px solid $borderColor; color: $textPrimary; } tr:hover td { background: rgba(57, 211, 83, 0.03); } tr.finding td { background: rgba(248, 81, 73, 0.05); } .cell-danger { color: $dangerColor; font-weight: 600; } .cell-ok { color: $successColor; } .no-data { color: $textSecondary; font-style: italic; padding: 1rem 0; } .paths-list { list-style: none; padding: 0; } .paths-list li { padding: 0.3rem 0; color: $textSecondary; font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; font-size: 0.85rem; } .paths-list li::before { content: '\25B8'; color: $accentColor; margin-right: 0.5rem; } footer { text-align: center; color: $textSecondary; font-size: 0.8rem; margin-top: 2rem; padding-top: 1rem; border-top: 1px solid $borderColor; } @media (max-width: 768px) { body { padding: 1rem; } .summary-grid { grid-template-columns: repeat(2, 1fr); } td, th { padding: 0.4rem 0.5rem; font-size: 0.75rem; } } </style> </head> <body> <div class="container"> <header> <h1>NTFS Permission Audit Report</h1> <div class="subtitle">Generated $($Summary.AuditDate) | Duration: $($Summary.Duration) | Paths scanned: $($Summary.PathsScanned)</div> </header> <!-- Summary Cards --> <div class="summary-grid"> <div class="summary-card"> <div class="value$(if ([int]$Summary.TotalFindings -gt 0) { ' danger' } else { ' success' })">$($Summary.TotalFindings)</div> <div class="label">Total Findings</div> </div> <div class="summary-card"> <div class="value$(if ([int]$directFindings -gt 0) { ' danger' } else { ' success' })">$directFindings</div> <div class="label">Direct User ACEs</div> </div> <div class="summary-card"> <div class="value$(if ([int]$brokenFindings -gt 0) { ' warning' } else { ' success' })">$brokenFindings</div> <div class="label">Broken Inheritance</div> </div> <div class="summary-card"> <div class="value$(if ([int]$nestingFindings -gt 0) { ' warning' } else { ' success' })">$nestingFindings</div> <div class="label">Excessive Nesting</div> </div> <div class="summary-card"> <div class="value$(if ([int]$shareFindings -gt 0) { ' danger' } else { ' success' })">$shareFindings</div> <div class="label">Share Issues</div> </div> </div> <!-- Paths Audited --> <div class="section"> <div class="section-header"> <h2>Paths Audited</h2> <span class="badge badge-success">$($PathsAudited.Count) paths</span> </div> <div class="section-body"> <ul class="paths-list"> $pathsList </ul> </div> </div> <!-- Section 1: Direct User ACEs --> <div class="section"> <div class="section-header"> <h2>Direct User ACEs</h2> <span class="badge $(if ($directFindings -gt 0) { 'badge-danger' } else { 'badge-success' })">$directFindings findings</span> </div> <div class="section-body"> <p style="color: $textSecondary; margin-bottom: 1rem; font-size: 0.9rem;"> ACL entries assigned directly to user accounts instead of security groups. Direct user ACEs bypass group-based access management and are a compliance risk. </p> $directTable </div> </div> <!-- Section 2: Broken Inheritance --> <div class="section"> <div class="section-header"> <h2>Broken Inheritance</h2> <span class="badge $(if ($brokenFindings -gt 0) { 'badge-warning' } else { 'badge-success' })">$brokenFindings findings</span> </div> <div class="section-body"> <p style="color: $textSecondary; margin-bottom: 1rem; font-size: 0.9rem;"> Folders where NTFS permission inheritance has been disabled. Each instance should be documented and justified. Undocumented broken inheritance is a common vector for permission drift. </p> $brokenTable </div> </div> <!-- Section 3: Nested Group Depth --> <div class="section"> <div class="section-header"> <h2>Nested Group Depth</h2> <span class="badge $(if ($nestingFindings -gt 0) { 'badge-warning' } else { 'badge-success' })">$nestingFindings findings</span> </div> <div class="section-body"> <p style="color: $textSecondary; margin-bottom: 1rem; font-size: 0.9rem;"> Security groups nested beyond the acceptable depth threshold. Deeply nested groups make effective access unpredictable and permission troubleshooting nearly impossible. </p> $nestingTable </div> </div> <!-- Section 4: Share Permissions --> <div class="section"> <div class="section-header"> <h2>Share-Level Permissions</h2> <span class="badge $(if ($shareFindings -gt 0) { 'badge-danger' } else { 'badge-success' })">$shareFindings findings</span> </div> <div class="section-body"> <p style="color: $textSecondary; margin-bottom: 1rem; font-size: 0.9rem;"> SMB share-level permissions (separate from NTFS). Windows applies the most restrictive of the two layers, but auditors expect both to follow least-privilege. </p> $shareTable </div> </div> <footer> NTFS Permission Audit Report — Generated by NTFS-PermissionAudit Module — $($Summary.AuditDate) </footer> </div> </body> </html> "@ # Ensure output directory exists $outputDir = Split-Path -Path $ReportFile -Parent if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir -Force | Out-Null } $html | Out-File -FilePath $ReportFile -Encoding UTF8 -Force Write-Verbose "HTML dashboard written to: $ReportFile" } |