Public/Report/New-TBDriftReport.ps1

function New-TBDriftReport {
    <#
    .SYNOPSIS
        Generates an HTML or JSON drift report.
    .DESCRIPTION
        Collects drift data and monitor information, then generates a formatted
        report for review and compliance documentation.
    .PARAMETER OutputPath
        The file path for the report. Extension determines format (.html or .json).
    .PARAMETER MonitorId
        Optional monitor ID to scope the report.
    .PARAMETER Format
        Report format: HTML or JSON. Defaults to HTML. If OutputPath has an extension,
        that takes precedence.
    .EXAMPLE
        New-TBDriftReport -OutputPath './drift-report.html'
    .EXAMPLE
        New-TBDriftReport -OutputPath './drift-report.json' -MonitorId '00000000-...'
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [string]$MonitorId,

        [Parameter()]
        [ValidateSet('HTML', 'JSON')]
        [string]$Format = 'HTML'
    )

    # Determine format from file extension if provided
    if ($OutputPath) {
        $extension = [System.IO.Path]::GetExtension($OutputPath).ToLower()
        if ($extension -eq '.json') {
            $Format = 'JSON'
        }
        elseif ($extension -eq '.html' -or $extension -eq '.htm') {
            $Format = 'HTML'
        }
    }

    if (-not $OutputPath) {
        $dateStamp = Get-Date -Format 'yyyyMMdd-HHmmss'
        $ext = if ($Format -eq 'JSON') { '.json' } else { '.html' }
        $OutputPath = 'TBDriftReport-{0}{1}' -f $dateStamp, $ext
    }

    $driftParams = @{}
    if ($MonitorId) {
        $driftParams['MonitorId'] = $MonitorId
    }

    Write-TBLog -Message 'Collecting drift data for report'
    $drifts = @(Get-TBDrift @driftParams)
    $summary = Get-TBDriftSummary @driftParams
    $monitors = @(Get-TBMonitor)

    $reportData = [PSCustomObject]@{
        GeneratedAt    = (Get-Date).ToString('o')
        TotalDrifts    = $drifts.Count
        TotalMonitors  = $monitors.Count
        Summary        = $summary
        Drifts         = $drifts
        Monitors       = $monitors
    }

    if ($PSCmdlet.ShouldProcess($OutputPath, 'Generate drift report')) {
        $parentDir = Split-Path -Path $OutputPath -Parent
        if ($parentDir -and -not (Test-Path -Path $parentDir)) {
            $null = New-Item -Path $parentDir -ItemType Directory -Force
        }

        if ($Format -eq 'JSON') {
            $reportData | ConvertTo-Json -Depth 20 | Out-File -FilePath $OutputPath -Encoding utf8 -Force
        }
        else {
            $html = New-TBDriftReportHtml -ReportData $reportData
            $html | Out-File -FilePath $OutputPath -Encoding utf8 -Force
        }

        Write-TBLog -Message ('Report generated: {0}' -f $OutputPath)

        [PSCustomObject]@{
            OutputPath  = (Resolve-Path -Path $OutputPath).Path
            Format      = $Format
            DriftCount  = $drifts.Count
            GeneratedAt = $reportData.GeneratedAt
        }
    }
}

function New-TBDriftReportHtml {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$ReportData
    )

    $styleTokens = Get-TBFluentHtmlStyleTokenSet

    $driftRows = ''
    foreach ($drift in $ReportData.Drifts) {
        $resourceType = [System.Net.WebUtility]::HtmlEncode($drift.ResourceType)
        $displayName = [System.Net.WebUtility]::HtmlEncode($drift.BaselineResourceDisplayName)
        $status = [System.Net.WebUtility]::HtmlEncode($drift.Status)
        $detected = [System.Net.WebUtility]::HtmlEncode($drift.FirstReportedDateTime)

        if ($drift.DriftedProperties) {
            foreach ($prop in $drift.DriftedProperties) {
                $propName = ''
                $desired = ''
                $current = ''

                if ($prop -is [hashtable]) {
                    $propName = $prop['propertyName']
                    $desired = "$($prop['desiredValue'])"
                    $current = "$($prop['currentValue'])"
                }
                else {
                    if ($prop.PSObject.Properties['propertyName']) { $propName = $prop.propertyName }
                    if ($prop.PSObject.Properties['desiredValue']) { $desired = "$($prop.desiredValue)" }
                    if ($prop.PSObject.Properties['currentValue']) { $current = "$($prop.currentValue)" }
                }

                $driftRows += @"
        <tr>
            <td>$resourceType</td>
            <td>$([System.Net.WebUtility]::HtmlEncode($displayName))</td>
            <td>$([System.Net.WebUtility]::HtmlEncode($propName))</td>
            <td><code>$([System.Net.WebUtility]::HtmlEncode($desired))</code></td>
            <td><code>$([System.Net.WebUtility]::HtmlEncode($current))</code></td>
            <td>$status</td>
            <td>$detected</td>
        </tr>
"@

            }
        }
        else {
            $driftRows += @"
        <tr>
            <td>$resourceType</td>
            <td>$([System.Net.WebUtility]::HtmlEncode($displayName))</td>
            <td>-</td>
            <td>-</td>
            <td>-</td>
            <td>$status</td>
            <td>$detected</td>
        </tr>
"@

        }
    }

    if (-not $driftRows) {
        $driftRows = '<tr><td colspan="7" style="text-align:center;">No drifts detected.</td></tr>'
    }

    $html = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TenantBaseline Drift Report</title>
    <style>
$styleTokens
        body { margin: 2rem; }
        h1 { color: var(--tb-accent); border-bottom: 2px solid var(--tb-accent); padding-bottom: 0.5rem; }
        h2 { color: var(--tb-text); margin-top: 2rem; }
        .summary { display: flex; gap: 1rem; flex-wrap: wrap; margin: 1rem 0; }
        .card { border-radius: 8px; padding: 1rem 1.5rem; min-width: 150px; }
        .card .label { font-size: 0.85rem; color: var(--tb-text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
        .card .value { font-size: 2rem; font-weight: 600; color: var(--tb-text); }
        .card.alert .value { color: var(--tb-danger); }
        table { width: 100%; border-collapse: collapse; margin: 1rem 0; border-radius: 8px; overflow: hidden; }
        th { background: var(--tb-accent); color: #fff; text-align: left; padding: 0.75rem 1rem; font-weight: 600; }
        td { padding: 0.5rem 1rem; border-bottom: 1px solid var(--tb-border); }
        tr:hover td { background: var(--tb-surface-muted); }
        code { background: var(--tb-surface-muted); padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
        .footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--tb-border); color: var(--tb-text-muted); font-size: 0.85rem; }
    </style>
</head>
<body>
    <h1>TenantBaseline Drift Report</h1>
    <p>Generated: $([System.Net.WebUtility]::HtmlEncode($ReportData.GeneratedAt))</p>

    <div class="summary">
        <div class="card$(if ($ReportData.TotalDrifts -gt 0) { ' alert' })">
            <div class="label">Drifts Detected</div>
            <div class="value">$($ReportData.TotalDrifts)</div>
        </div>
        <div class="card">
            <div class="label">Monitors</div>
            <div class="value">$($ReportData.TotalMonitors)</div>
        </div>
    </div>

    <h2>Drift Details</h2>
    <table>
        <thead>
            <tr>
                <th>Resource Type</th>
                <th>Resource</th>
                <th>Property</th>
                <th>Desired Value</th>
                <th>Current Value</th>
                <th>Status</th>
                <th>First Detected</th>
            </tr>
        </thead>
        <tbody>
            $driftRows
        </tbody>
    </table>

    <div class="footer">
        <p>Generated by TenantBaseline PowerShell Module</p>
    </div>
</body>
</html>
"@


    return $html
}