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 &mdash; Generated by NTFS-PermissionAudit Module &mdash; $($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"
}