Public/Get-IntuneAnomaliesReport.ps1

<#
.SYNOPSIS
    Generates an interactive HTML report of Intune anomalies (app failures, multi-user devices, encryption, Autopilot, compliance, etc.).
    Connect first with Connect-RKGraph; this cmdlet uses the existing connection.
#>

function Get-IntuneAnomaliesReport {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DebugMode', Justification = 'Kept for compatibility')]
[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)] [switch] $SendEmail,
    [Parameter(Mandatory = $false)] [string[]] $Recipient,
    [Parameter(Mandatory = $false)] [string] $From,
    [Parameter(Mandatory = $false)] [string] $ExportPath,
    # Surface devices that were *deliberately* excluded from BitLocker / LAPS policies
    # (via exclude group or assignment filter) as Info-severity rows. Off by default
    # so the report stays focused on actual anomalies; intentional exclusions are
    # admin choices, not gaps.
    [Parameter(Mandatory = $false)] [switch] $ShowExcludedDevices,
    # Kept so existing scripts keep working; has no effect. Use -Verbose for timings.
    [Parameter(Mandatory = $false)] [switch] $DebugMode
)

$ErrorActionPreference = 'Stop'
try {
    $swReport = [System.Diagnostics.Stopwatch]::StartNew()
    $swPhase  = [System.Diagnostics.Stopwatch]::new()

    $ctx = Get-MgContext -ErrorAction SilentlyContinue
    if (-not $ctx) { throw 'Not connected to Microsoft Graph. Run Connect-RKGraph first.' }

    Write-Host 'Collecting Intune anomaly data...' -ForegroundColor Yellow
    $swPhase.Restart()
    $anomaly = Get-RKIntuneAnomaly -ShowExcludedDevices:$ShowExcludedDevices
    $swPhase.Stop(); Write-Verbose ("[Report] Get-RKIntuneAnomaly: {0:N2}s" -f $swPhase.Elapsed.TotalSeconds)
    $tenantname = $anomaly.TenantName

    $notices = [System.Collections.Generic.List[string]]::new()
    if (-not $anomaly.BitLockerKeysReadable) { $notices.Add('BitLocker recovery keys could not be read (access denied). Check that the connection has BitlockerKey.ReadBasic.All and that the account has a role that may read them, such as Global Reader. The BitLocker tab cannot tell which keys are in Entra ID.') }
    if (-not $anomaly.LapsCredentialsReadable) { $notices.Add('Windows LAPS credentials could not be read (access denied). Check that the connection has DeviceLocalCredential.ReadBasic.All and that the account has a role that may read them, such as Global Reader. The LAPS tab cannot tell which devices backed up a password.') }
    if (-not $anomaly.AppsReadable) { $notices.Add('Apps could not be read (access denied). Check that the connection has DeviceManagementApps.Read.All and that the account has an Intune role that may read apps, such as Global Reader. The Application Failures tab is empty.') }
    foreach ($w in $anomaly.Warnings) { $notices.Add("$($w.Area): $($w.Message)") }
    # A warning recorded under several areas has the same text.
    $notices = [System.Collections.Generic.List[string]]@($notices | Select-Object -Unique)
    foreach ($n in $notices) { Write-Warning $n }

    $swPhase.Restart()
    New-IntuneAnomaliesHTMLReport -TenantName $tenantname -Report_ApplicationFailureReport $anomaly.AppInstallFailures -Report_DevicesWithMultipleUsers $anomaly.MultiUser -Report_DevicesWithoutAutopilotHash $anomaly.MissingAutopilot -Report_InactiveDevices $anomaly.Inactive -Report_NoncompliantDevices $anomaly.Noncompliant -Report_OperatingSystemEditionOverview $anomaly.OperatingSystemEditions -Report_DisabledPrimaryUsers $anomaly.DisabledPrimaryUser -Report_BitLockerStatus $anomaly.BitLocker -Report_LapsStatus $anomaly.Laps -Report_DeprecatedSettings $anomaly.DeprecatedSettings -Notices $notices.ToArray() -ExportPath $ExportPath
    $swPhase.Stop(); Write-Verbose ("[Report] HTML build: {0:N2}s" -f $swPhase.Elapsed.TotalSeconds)
    $swReport.Stop(); Write-Verbose ("[Report] TOTAL Get-IntuneAnomaliesReport: {0:N2}s" -f $swReport.Elapsed.TotalSeconds)

    $emailSent = $false
    if ($SendEmail -and $Recipient) {
        $subject = "$tenantname - Intune Anomalies Report"
        $bodyHtml = "<html><body style=`"font-family: Segoe UI, Arial, sans-serif;`"><h2>Intune Anomalies Report</h2><p>Attached is the latest Intune anomalies report for <strong>$tenantname</strong>.</p><p>Open the attached HTML in a browser for the interactive dashboard.</p><p style='color:#666;'>Generated by RKSolutions - please do not reply.</p></body></html>"
        $emailSent = Send-EmailWithAttachment -Recipient $Recipient -AttachmentPath $script:ExportPath -From $From -Subject $subject -BodyHtml $bodyHtml
        if ($emailSent) { Write-Host 'INFO: Email sent successfully.' -ForegroundColor Green } else { Write-Host 'ERROR: Failed to send email.' -ForegroundColor Red }
    }
    if ($SendEmail -and $emailSent -and (Test-Path -Path $script:ExportPath)) { Remove-Item -Path $script:ExportPath -Force; Write-Host 'INFO: Temporary report file deleted.' -ForegroundColor Green }
}
catch { Write-Error "Error: $_"; throw $_ }
finally {
    # Session left connected; use Disconnect-RKGraph when done.
}
}