Public/Get-MDEValidationReport.ps1

function Get-MDEValidationReport {
    <#
    .SYNOPSIS
        Generates a formatted MDE validation report.
     
    .DESCRIPTION
        Runs all MDE configuration validation tests and generates a report
        in the specified format.
     
    .PARAMETER OutputFormat
        The format of the output report. Valid values are 'Console', 'HTML', or 'Object'.
        Default is 'Console'.
     
    .PARAMETER OutputPath
        The path to save the HTML report. Only used when OutputFormat is 'HTML'.
     
    .PARAMETER IncludeOnboarding
        Include MDE onboarding status check (requires elevated privileges).
     
    .PARAMETER IncludePolicyVerification
        Include policy registry verification sub-tests. These sub-tests verify that
        settings returned by Get-MpPreference match the corresponding registry/policy
        entries based on the device's management type (Intune vs Security Settings Management).
     
    .EXAMPLE
        Get-MDEValidationReport
         
        Displays a console-formatted validation report.
     
    .EXAMPLE
        Get-MDEValidationReport -OutputFormat HTML -OutputPath "C:\Reports\MDEReport.html"
         
        Generates an HTML report and saves it to the specified path.
     
    .EXAMPLE
        Get-MDEValidationReport -OutputFormat Object
         
        Returns validation results as PowerShell objects.
     
    .EXAMPLE
        Get-MDEValidationReport -IncludePolicyVerification
         
        Displays a validation report with policy registry verification sub-tests.
     
    .OUTPUTS
        Console output, HTML file, or array of PSCustomObjects depending on OutputFormat.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Console', 'HTML', 'Object')]
        [string]$OutputFormat = 'Console',
        
        [Parameter()]
        [string]$OutputPath,
        
        [Parameter()]
        [switch]$IncludeOnboarding,
        
        [Parameter()]
        [switch]$IncludePolicyVerification
    )
    
    # Run all validation tests
    $results = Test-MDEConfiguration -IncludeOnboarding:$IncludeOnboarding -IncludePolicyVerification:$IncludePolicyVerification
    
    # Get OS information for the report header
    $osInfo = Get-MDEOperatingSystemInfo
    
    # Get management status for the report header
    $managedByStatus = Get-MDESecuritySettingsManagementStatus
    
    # Get MDE onboarding status for the report header
    $onboardingStatus = Get-MDEOnboardingStatusString
    
    switch ($OutputFormat) {
        'Object' {
            return $results
        }
        
        'Console' {
            Write-Host "`n========================================" -ForegroundColor Cyan
            Write-Host " MDE Configuration Validation Report" -ForegroundColor Cyan
            Write-Host " Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan
            Write-Host " Computer: $env:COMPUTERNAME" -ForegroundColor Cyan
            Write-Host " OS: $osInfo" -ForegroundColor Cyan
            Write-Host " Managed By: $managedByStatus" -ForegroundColor Cyan
            Write-Host " MDE Onboarding: $onboardingStatus" -ForegroundColor Cyan
            Write-Host "========================================`n" -ForegroundColor Cyan
            
            foreach ($result in $results) {
                $statusColor = switch ($result.Status) {
                    'Pass' { 'Green' }
                    'Fail' { 'Red' }
                    'Warning' { 'Yellow' }
                    'Info' { 'Cyan' }
                    'NotApplicable' { 'Gray' }
                    default { 'White' }
                }
                
                $statusSymbol = switch ($result.Status) {
                    'Pass' { '[PASS]' }
                    'Fail' { '[FAIL]' }
                    'Warning' { '[WARN]' }
                    'Info' { '[INFO]' }
                    'NotApplicable' { '[N/A]' }
                    default { '[???]' }
                }
                
                Write-Host "$statusSymbol " -ForegroundColor $statusColor -NoNewline
                Write-Host "$($result.TestName)" -ForegroundColor White
                Write-Host " $($result.Message)" -ForegroundColor Gray
                
                if ($result.Recommendation) {
                    Write-Host " Recommendation: $($result.Recommendation)" -ForegroundColor Yellow
                }
                Write-Host ""
            }
            
            # Summary
            $passCount = @($results | Where-Object { $_.Status -eq 'Pass' }).Count
            $failCount = @($results | Where-Object { $_.Status -eq 'Fail' }).Count
            $warnCount = @($results | Where-Object { $_.Status -eq 'Warning' }).Count
            $totalCount = @($results).Count
            
            Write-Host "========================================" -ForegroundColor Cyan
            Write-Host " Summary: $passCount/$totalCount Passed" -ForegroundColor $(if ($failCount -eq 0) { 'Green' } else { 'Yellow' })
            Write-Host " Passed: $passCount | Failed: $failCount | Warnings: $warnCount" -ForegroundColor Cyan
            Write-Host "========================================`n" -ForegroundColor Cyan
        }
        
        'HTML' {
            if ([string]::IsNullOrEmpty($OutputPath)) {
                $tempDir = if ($env:TEMP) { $env:TEMP } elseif ($env:TMPDIR) { $env:TMPDIR } else { '/tmp' }
                $OutputPath = Join-Path $tempDir "MDEValidationReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
            }
            
            # Resolve to absolute path to ensure Split-Path works correctly
            $OutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
            
            # Force create the output directory if it doesn't exist
            $outputDirectory = Split-Path -Path $OutputPath -Parent
            if (-not [string]::IsNullOrEmpty($outputDirectory) -and -not (Test-Path -Path $outputDirectory)) {
                try {
                    New-Item -Path $outputDirectory -ItemType Directory -Force | Out-Null
                }
                catch {
                    Write-Error "Failed to create output directory: $outputDirectory. Error: $_"
                    return
                }
            }
            
            $passCount = @($results | Where-Object { $_.Status -eq 'Pass' }).Count
            $failCount = @($results | Where-Object { $_.Status -eq 'Fail' }).Count
            $warnCount = @($results | Where-Object { $_.Status -eq 'Warning' }).Count
            $totalCount = @($results).Count
            
            $htmlContent = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MDE Configuration Validation Report</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            max-width: 1000px;
            margin: 0 auto;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            padding: 30px;
        }
        h1 {
            color: #0078d4;
            border-bottom: 2px solid #0078d4;
            padding-bottom: 10px;
        }
        .meta {
            color: #666;
            margin-bottom: 20px;
        }
        .summary {
            display: flex;
            gap: 20px;
            margin-bottom: 30px;
        }
        .summary-card {
            flex: 1;
            padding: 20px;
            border-radius: 8px;
            text-align: center;
        }
        .summary-card.pass { background-color: #dff6dd; color: #107c10; }
        .summary-card.fail { background-color: #fde7e9; color: #d13438; }
        .summary-card.warn { background-color: #fff4ce; color: #797673; }
        .summary-card h2 { margin: 0; font-size: 2em; }
        .summary-card p { margin: 5px 0 0 0; }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        th {
            background-color: #0078d4;
            color: white;
        }
        tr:hover { background-color: #f5f5f5; }
        .status {
            padding: 4px 8px;
            border-radius: 4px;
            font-weight: bold;
            font-size: 0.9em;
        }
        .status.pass { background-color: #dff6dd; color: #107c10; }
        .status.fail { background-color: #fde7e9; color: #d13438; }
        .status.warning { background-color: #fff4ce; color: #797673; }
        .status.info { background-color: #cce4f6; color: #0078d4; }
        .recommendation {
            font-size: 0.9em;
            color: #666;
            margin-top: 5px;
        }
        .expander {
            cursor: pointer;
            user-select: none;
            color: #0078d4;
            font-weight: bold;
            margin-top: 8px;
            display: inline-block;
        }
        .expander:hover {
            text-decoration: underline;
        }
        .expander::before {
            content: 'â–¶ ';
            display: inline-block;
            transition: transform 0.2s;
        }
        .expander.expanded::before {
            transform: rotate(90deg);
        }
        .details-content {
            display: none;
            margin-top: 8px;
            padding: 10px;
            background-color: #f8f8f8;
            border-left: 3px solid #0078d4;
            border-radius: 4px;
        }
        .details-content.show {
            display: block;
        }
        .details-content ul {
            margin: 0;
            padding-left: 20px;
        }
        .details-content li {
            margin: 4px 0;
        }
    </style>
    <script>
        function toggleDetails(expanderId) {
            const expander = document.getElementById('expander-' + expanderId);
            const details = document.getElementById('details-' + expanderId);
             
            if (details.classList.contains('show')) {
                details.classList.remove('show');
                expander.classList.remove('expanded');
            } else {
                details.classList.add('show');
                expander.classList.add('expanded');
            }
        }
    </script>
</head>
<body>
    <div class="container">
        <h1>MDE Configuration Validation Report</h1>
        <div class="meta">
            <p><strong>Computer:</strong> $(ConvertTo-HtmlEncodedString $env:COMPUTERNAME)</p>
            <p><strong>OS:</strong> $(ConvertTo-HtmlEncodedString $osInfo)</p>
            <p><strong>Managed By:</strong> $(ConvertTo-HtmlEncodedString $managedByStatus)</p>
            <p><strong>MDE Onboarding:</strong> $(ConvertTo-HtmlEncodedString $onboardingStatus)</p>
            <p><strong>Generated:</strong> $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p>
        </div>
         
        <div class="summary">
            <div class="summary-card pass">
                <h2>$passCount</h2>
                <p>Passed</p>
            </div>
            <div class="summary-card fail">
                <h2>$failCount</h2>
                <p>Failed</p>
            </div>
            <div class="summary-card warn">
                <h2>$warnCount</h2>
                <p>Warnings</p>
            </div>
        </div>
         
        <table>
            <tr>
                <th>Test</th>
                <th>Status</th>
                <th>Details</th>
            </tr>
"@

            
            $expanderIndex = 0
            foreach ($result in $results) {
                $statusClass = $result.Status.ToLower()
                $encodedTestName = ConvertTo-HtmlEncodedString $result.TestName
                $encodedRecommendation = ConvertTo-HtmlEncodedString $result.Recommendation
                $recommendation = if ($result.Recommendation) {
                    "<div class='recommendation'><strong>Recommendation:</strong> $encodedRecommendation</div>"
                } else { '' }
                
                # Special handling for ASR rules with expandable details
                if ($result.PSObject.Properties.Name -contains 'ASRSummary' -and 
                    $result.PSObject.Properties.Name -contains 'ASRRuleDetails' -and
                    $null -ne $result.ASRRuleDetails -and 
                    $result.ASRRuleDetails.Count -gt 0) {
                    
                    $encodedSummary = ConvertTo-HtmlEncodedString $result.ASRSummary
                    $expanderId = $expanderIndex++
                    
                    # Build the list of rules using array join for better performance
                    $rulesListItems = $result.ASRRuleDetails | ForEach-Object {
                        $encodedRule = ConvertTo-HtmlEncodedString $_
                        " <li>$encodedRule</li>"
                    }
                    $rulesList = $rulesListItems -join "`n"
                    
                    $htmlContent += @"
            <tr>
                <td>$encodedTestName</td>
                <td><span class="status $statusClass">$($result.Status.ToUpper())</span></td>
                <td>
                    $encodedSummary
                    <div class="expander" id="expander-$expanderId" onclick="toggleDetails($expanderId)">Show configured rules</div>
                    <div class="details-content" id="details-$expanderId">
                        <ul>
$rulesList
                        </ul>
                    </div>
                    $recommendation
                </td>
            </tr>
"@

                } else {
                    # Normal handling for other tests
                    $encodedMessage = ConvertTo-HtmlEncodedString $result.Message
                    $htmlContent += @"
            <tr>
                <td>$encodedTestName</td>
                <td><span class="status $statusClass">$($result.Status.ToUpper())</span></td>
                <td>$encodedMessage$recommendation</td>
            </tr>
"@

                }
            }
            
            $htmlContent += @"
        </table>
    </div>
</body>
</html>
"@

            
            $htmlContent | Out-File -FilePath $OutputPath -Encoding UTF8
            Write-Host "HTML report saved to: $OutputPath" -ForegroundColor Green
            return $OutputPath
        }
    }
}