Private/ConvertTo-DriftGroupedHtmlReport.ps1

function ConvertTo-DriftGroupedHtmlReport {
    <#
    .SYNOPSIS
        Converts Management Group / Subscription drift data into a grouped HTML report.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Title,

        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$DriftResult,

        [System.Collections.IDictionary]$ManagementGroupNames = @{}, 
        [System.Collections.IDictionary]$SubscriptionNames = @{},

        [string]$GeneratedBy = 'PowerShell.IGA.AccessGuard'
    )

    $generated_on = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $style = @'
<style>
    body { font-family: Segoe UI, Arial, sans-serif; margin: 20px; color: #222; }
    h1 { margin-bottom: 0; }
    .subtitle { color: #666; margin-top: 4px; }
    h2 { border-bottom: 2px solid #444; padding-bottom: 4px; margin-top: 30px; }
    details { margin-bottom: 8px; border: 1px solid #ccc; border-radius: 4px; padding: 6px 10px; }
    summary { font-weight: bold; cursor: pointer; }
    table { border-collapse: collapse; width: 100%; margin-top: 8px; }
    th, td { border: 1px solid #ddd; padding: 6px 8px; font-size: 13px; text-align: left; }
    th { background-color: #f2f2f2; }
    td.added { background-color: #e8f5e9; color: #1b5e20; font-weight: 600; }
    td.absent { background-color: #ffebee; color: #b71c1c; font-weight: 600; }
    td.removed { background-color: #ffebee; color: #b71c1c; font-weight: 600; }
</style>
'@


    function ConvertTo-DriftScopeTable {
        param([array]$Entries)

        if (-not $Entries -or $Entries.Count -eq 0) { return '<p><em>No changes.</em></p>' }

        $columns = 'Status', 'SuggestedAction', 'Scope', 'DisplayName', 'SignInName', 'ObjectId', 'ObjectType', 'RoleDefinitionName'
        $sb = New-Object System.Text.StringBuilder
        [void]$sb.Append('<table><thead><tr>')
        foreach ($column in $columns) { [void]$sb.Append("<th>$([System.Net.WebUtility]::HtmlEncode($column))</th>") }
        [void]$sb.Append('</tr></thead><tbody>')

        foreach ($entry in $Entries) {
            $assignment = $entry.Assignment
            $status_class = switch ($entry.Status) {
                'Added' { ' class="added"'; break }
                'Absent' { ' class="absent"'; break }
                'Removed' { ' class="removed"'; break }
                default { '' }
            }
            [void]$sb.Append('<tr>')
            foreach ($column in $columns) {
                $value = if ($column -eq 'Status') { $entry.Status } elseif ($column -eq 'SuggestedAction') { $entry.SuggestedAction } else { $assignment.$column }
                $cell_class = if ($column -eq 'Status') { $status_class } else { '' }
                [void]$sb.Append("<td$cell_class>$([System.Net.WebUtility]::HtmlEncode([string]$value))</td>")
            }
            [void]$sb.Append('</tr>')
        }

        [void]$sb.Append('</tbody></table>')
        return $sb.ToString()
    }

    function ConvertTo-DriftGroupSectionHtml {
        param([string]$SectionTitle, [System.Collections.IDictionary]$Data, [System.Collections.IDictionary]$Names)

        $sb = New-Object System.Text.StringBuilder
        [void]$sb.Append("<h2>$([System.Net.WebUtility]::HtmlEncode($SectionTitle))</h2>")
        if (-not $Data -or $Data.Keys.Count -eq 0) {
            [void]$sb.Append('<p><em>No changes.</em></p>')
            return $sb.ToString()
        }

        foreach ($id in $Data.Keys) {
            $entries = @($Data[$id])
            $label = if ($Names -and $Names.Contains($id) -and $Names[$id]) { "$id ($($Names[$id]))" } else { $id }
            [void]$sb.Append("<details><summary>$([System.Net.WebUtility]::HtmlEncode($label)) ($($entries.Count))</summary>")
            [void]$sb.Append((ConvertTo-DriftScopeTable -Entries $entries))
            [void]$sb.Append('</details>')
        }

        return $sb.ToString()
    }

    $body = "<h1>$([System.Net.WebUtility]::HtmlEncode($Title))</h1>"
    $body += "<p class='subtitle'>Generated by $([System.Net.WebUtility]::HtmlEncode($GeneratedBy)) on $generated_on</p>"
    $body += ConvertTo-DriftGroupSectionHtml -SectionTitle 'Management Groups' -Data $DriftResult.ManagementGroup -Names $ManagementGroupNames
    $body += ConvertTo-DriftGroupSectionHtml -SectionTitle 'Subscriptions' -Data $DriftResult.Subscription -Names $SubscriptionNames

    return "<html><head><meta charset='utf-8'><title>$([System.Net.WebUtility]::HtmlEncode($Title))</title>$style</head><body>$body</body></html>"
}