Public/Get-TntIntuneDeviceComplianceReport.ps1

function Get-TntIntuneDeviceComplianceReport {
    <#
    .SYNOPSIS
        Generates Microsoft Intune device compliance analysis and reporting.

    .DESCRIPTION
        This function connects to Microsoft Graph using an app registration and generates detailed reports
        about device compliance policies, device compliance states, and policy effectiveness across the
        organization. It provides insights into compliance gaps, policy coverage, platform-specific
        compliance rates, and actionable recommendations for improving device security posture.

    .PARAMETER TenantId
        The Azure AD Tenant ID (GUID) to connect to.

    .PARAMETER ClientId
        The Application (Client) ID of the app registration created for security reporting.

    .PARAMETER ClientSecret
        The client secret for the app registration. Use this for automated scenarios.

    .PARAMETER CertificateThumbprint
        The thumbprint of the certificate to use for authentication instead of client secret.

    .PARAMETER FilterByPlatform
        Filter results by device platform. Valid values are Windows, iOS, Android, macOS.

    .PARAMETER FilterByComplianceState
        Filter results by compliance state. Valid values are Compliant, NonCompliant, InGracePeriod, ConfigManager, Error, Unknown.

    .PARAMETER ExcludeUserDetails
        Switch to skip user detail enrichment (Department, JobTitle, OfficeLocation). By default, user details
        are fetched for each device. Use this switch to improve performance when user details are not needed.

    .PARAMETER MaxDevices
        Maximum number of devices to process. Useful for large tenants. Defaults to 10000.

    .EXAMPLE
        Get-TntIntuneDeviceComplianceReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret

        Generates a comprehensive Intune device compliance report.

    .EXAMPLE
        Get-TntIntuneDeviceComplianceReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret -FilterByPlatform Windows -FilterByComplianceState NonCompliant

        Generates a report focused on non-compliant Windows devices.

    .INPUTS
        None. This function does not accept pipeline input.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        Returns a comprehensive report object containing:
        - Summary: Statistics on compliance, risk, and platforms
        - DeviceComplianceDetails: Detailed per-device analysis
        - ComplianceByRisk: Devices grouped by risk level
        - NonCompliantDevices: List of non-compliant devices
        - StaleDevicesList: List of stale devices
        - RecentEnrollments: List of recently enrolled devices

    .NOTES
        Author: Tom de Leeuw
        Website: https://systom.dev
        Module: TenantReports

        Required Permissions:
        - DeviceManagementConfiguration.Read.All (Application)
        - DeviceManagementManagedDevices.Read.All (Application)
        - Device.Read.All (Application)
        - User.Read.All (Application) (unless ExcludeUserDetails is specified)

    .LINK
        https://systom.dev
    #>


    [CmdletBinding(DefaultParameterSetName = 'ClientSecret')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [Parameter(ParameterSetName = 'Interactive')]
        [ValidateNotNullOrEmpty()]
        [Alias('Tenant')]
        [string]$TenantId,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [Parameter(ParameterSetName = 'Interactive')]
        [ValidatePattern('^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')]
        [Alias('ApplicationId')]
        [string]$ClientId,

        [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret', ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('Secret', 'ApplicationSecret')]
        [SecureString]$ClientSecret,

        [Parameter(Mandatory = $true, ParameterSetName = 'Certificate', ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('Thumbprint')]
        [string]$CertificateThumbprint,

        # Use interactive authentication (no app registration required).
        [Parameter(Mandatory = $true, ParameterSetName = 'Interactive')]
        [switch]$Interactive,

        [Parameter()]
        [ValidateSet('Windows', 'iOS', 'Android', 'macOS')]
        [string[]]$FilterByPlatform,

        [Parameter()]
        [ValidateSet('Compliant', 'NonCompliant', 'InGracePeriod', 'ConfigManager', 'Error', 'Unknown')]
        [string[]]$FilterByComplianceState,

        [Parameter()]
        [switch]$ExcludeUserDetails,

        [Parameter()]
        [ValidateRange(1, 50000)]
        [int]$MaxDevices = 10000
    )

    begin {
        # Define compliance state mappings and risk levels
        $ComplianceStateMap = @{
            'compliant'     = @{ Risk = 'Low'; Description = 'Device meets all compliance requirements' }
            'noncompliant'  = @{ Risk = 'High'; Description = 'Device fails one or more compliance requirements' }
            'inGracePeriod' = @{ Risk = 'Medium'; Description = 'Device is non-compliant but within grace period' }
            'configManager' = @{ Risk = 'Medium'; Description = 'Device managed by Configuration Manager' }
            'error'         = @{ Risk = 'High'; Description = 'Error evaluating device compliance' }
            'unknown'       = @{ Risk = 'Medium'; Description = 'Compliance state cannot be determined' }
        }

        Write-Information 'Starting Intune device compliance analysis...' -InformationAction Continue
    }

    process {
        try {
            $ConnectionParams = Get-ConnectionParameters -BoundParameters $PSBoundParameters
            $ConnectionInfo = Connect-TntGraphSession @ConnectionParams

            # Retrieve device compliance policies using Graph SDK
            Write-Verbose 'Retrieving device compliance policies...'
            try {
                $CompliancePolicies = Get-MgDeviceManagementDeviceCompliancePolicy -All -ErrorAction Stop
                Write-Verbose "Retrieved $($CompliancePolicies.Count) compliance policies"
            } catch {
                Write-Warning "Failed to retrieve compliance policies: $($_.Exception.Message)"
                $CompliancePolicies = @()
            }

            # Retrieve managed devices with compliance information using Graph SDK
            Write-Verbose "Retrieving managed devices (max $($MaxDevices))..."

            $ManagedDevices = Get-MgDeviceManagementManagedDevice -All -Top $MaxDevices -ErrorAction Stop
            Write-Verbose "Retrieved $($ManagedDevices.Count) managed devices"

            # Pre-fetch all users ONCE before loo
            $UserCache = $null
            Write-Verbose 'Pre-fetching user data for device enrichment...'
            $CacheParams = @{
                TenantId           = $TenantId
                ClientId           = $ClientId
                RequiredProperties = @('Department', 'JobTitle', 'OfficeLocation')
                FetchAll           = $true
            }
            $UserCache = Get-CachedUsers @CacheParams
            Write-Verbose "User cache ready: $($UserCache.UserCount) users (CacheHit: $($UserCache.CacheHit))"

            # Process each device for detailed compliance analysis
            $DeviceComplianceDetails = foreach ($Device in $ManagedDevices) {
                # Apply platform filter if specified
                if ($FilterByPlatform -and $Device.OperatingSystem -notin $FilterByPlatform) {
                    continue
                }

                # Apply compliance state filter if specified
                if ($FilterByComplianceState -and $Device.ComplianceState -notin $FilterByComplianceState) {
                    continue
                }

                # Determine device platform
                $DevicePlatform = switch ($Device.OperatingSystem) {
                    { $_ -match 'Windows' } { 'Windows' }
                    { $_ -match 'iOS' } { 'iOS' }
                    { $_ -match 'Android' } { 'Android' }
                    { $_ -match 'macOS' } { 'macOS' }
                    default { $Device.OperatingSystem ?? 'Unknown' }
                }

                # Calculate days since last sync
                $DaysSinceLastSync = if ($Device.LastSyncDateTime) {
                    [math]::Round(((Get-Date) - $Device.LastSyncDateTime).TotalDays, 1)
                } else {
                    999
                }

                # Calculate enrollment age
                $EnrollmentAge = if ($Device.EnrolledDateTime) {
                    [math]::Round(((Get-Date) - $Device.EnrolledDateTime).TotalDays, 0)
                } else {
                    0
                }

                # Normalize ComplianceState due to some weird issue with the SDK returning a single-element array
                $SafeComplianceState = if ($Device.ComplianceState) { 
                    ($Device.ComplianceState -join ', ').Trim()
                } else { 
                    'Unknown'
                }

                # Determine risk level based on compliance state and other factors
                $RiskLevel = $ComplianceStateMap[$SafeComplianceState].Risk
                if ($DaysSinceLastSync -gt 30) {
                    $RiskLevel = 'High'  # Override if device hasn't synced recently
                }

                # Get user information
                $UserInfo = @{
                    UserPrincipalName = $Device.UserPrincipalName ?? 'Unknown'
                    UserDisplayName   = $Device.UserDisplayName ?? 'Unknown'
                }

                if (-not $ExcludeUserDetails -and $Device.UserPrincipalName) {
                    # O(1) lookup from pre-fetched cache instead of per-device API call
                    $UserDetails = $null
                    if ($UserCache) {
                        $UserDetails = $UserCache.LookupByUPN[$Device.UserPrincipalName]
                    }

                    # Fallback to individual API call if not in cache (handles new users, transient failures)
                    if (-not $UserDetails) {
                        try {
                            $UserDetails = Get-MgUser -UserId $Device.UserPrincipalName -Property Id, DisplayName, Department, JobTitle, OfficeLocation -ErrorAction SilentlyContinue
                        } catch {
                            Write-Verbose "Could not retrieve user details for $($Device.UserPrincipalName)"
                        }
                    }

                    if ($UserDetails) {
                        $UserInfo.Department = $UserDetails.Department ?? 'Not specified'
                        $UserInfo.JobTitle = $UserDetails.JobTitle ?? 'Not specified'
                        $UserInfo.OfficeLocation = $UserDetails.OfficeLocation ?? 'Not specified'
                    }
                }

                # Create detailed device compliance entry
                [PSCustomObject]@{
                    DeviceId              = $Device.Id
                    DeviceName            = $Device.DeviceName ?? 'Unknown'
                    Platform              = $DevicePlatform
                    OperatingSystem       = $Device.OperatingSystem ?? 'Unknown'
                    OSVersion             = $Device.OSVersion ?? 'Unknown'
                    ComplianceState       = $SafeComplianceState ?? 'Unknown'
                    ComplianceDescription = $ComplianceStateMap[$SafeComplianceState].Description
                    RiskLevel             = $RiskLevel
                    DeviceType            = $Device.DeviceType ?? 'Unknown'
                    OwnerType             = ($Device.ManagedDeviceOwnerType -join ', ').Trim() ?? 'Unknown'
                    ManagementAgent       = ($Device.ManagementAgent -join ', ').Trim() ?? 'Unknown'
                    RegistrationState     = ($Device.DeviceRegistrationState -join ', ').Trim() ?? 'Unknown'
                    EnrolledDateTime      = $Device.EnrolledDateTime
                    LastSyncDateTime      = $Device.LastSyncDateTime
                    DaysSinceLastSync     = $DaysSinceLastSync
                    EnrollmentAge         = $EnrollmentAge
                    AzureADDeviceId       = $Device.AzureADDeviceId ?? 'Not available'
                    SerialNumber          = $Device.SerialNumber ?? 'Not available'
                    Model                 = $Device.Model ?? 'Unknown'
                    Manufacturer          = $Device.Manufacturer ?? 'Unknown'
                    UserPrincipalName     = $UserInfo.UserPrincipalName
                    UserDisplayName       = $UserInfo.UserDisplayName
                    UserDepartment        = $UserInfo.Department ?? 'Not retrieved'
                    UserJobTitle          = $UserInfo.JobTitle ?? 'Not retrieved'
                    UserOfficeLocation    = $UserInfo.OfficeLocation ?? 'Not retrieved'
                    IsStaleDevice         = $DaysSinceLastSync -gt 30
                    IsRecentEnrollment    = $EnrollmentAge -le 30
                    RequiresAttention     = $Device.ComplianceState -in @('NonCompliant', 'Error', 'Unknown')
                }
            }

            # Calculate overall compliance summary using single-pass accumulation
            $TotalDevicesProcessed = if ($DeviceComplianceDetails) { $DeviceComplianceDetails.Count } else { 0 }

            # Initialize counters for single-pass accumulation
            $DeviceStats = @{
                CompliantDevices          = 0
                NonCompliantDevices       = 0
                GracePeriodDevices        = 0
                ErrorDevices              = 0
                UnknownStateDevices       = 0
                HighRiskDevices           = 0
                MediumRiskDevices         = 0
                LowRiskDevices            = 0
                WindowsDevices            = 0
                iOSDevices                = 0
                AndroidDevices            = 0
                macOSDevices              = 0
                StaleDevices              = 0
                RecentEnrollments         = 0
                DevicesRequiringAttention = 0
                CorporateDevices          = 0
                PersonalDevices           = 0
            }
            $PlatformCounts = @{}

            if ($DeviceComplianceDetails) {
                foreach ($Device in $DeviceComplianceDetails) {
                    # Compliance state
                    switch ($Device.ComplianceState) {
                        'compliant' { $DeviceStats.CompliantDevices++ }
                        'noncompliant' { $DeviceStats.NonCompliantDevices++ }
                        'inGracePeriod' { $DeviceStats.GracePeriodDevices++ }
                        'error' { $DeviceStats.ErrorDevices++ }
                        'unknown' { $DeviceStats.UnknownStateDevices++ }
                    }
                    # Risk level
                    switch ($Device.RiskLevel) {
                        'High' { $DeviceStats.HighRiskDevices++ }
                        'Medium' { $DeviceStats.MediumRiskDevices++ }
                        'Low' { $DeviceStats.LowRiskDevices++ }
                    }
                    # Platform
                    switch ($Device.Platform) {
                        'Windows' { $DeviceStats.WindowsDevices++ }
                        'iOS' { $DeviceStats.iOSDevices++ }
                        'Android' { $DeviceStats.AndroidDevices++ }
                        'macOS' { $DeviceStats.macOSDevices++ }
                    }
                    # Platform counts for most common
                    if ($Device.Platform) {
                        if (-not $PlatformCounts.ContainsKey($Device.Platform)) {
                            $PlatformCounts[$Device.Platform] = 0
                        }
                        $PlatformCounts[$Device.Platform]++
                    }
                    # Health indicators
                    if ($Device.IsStaleDevice) { $DeviceStats.StaleDevices++ }
                    if ($Device.IsRecentEnrollment) { $DeviceStats.RecentEnrollments++ }
                    if ($Device.RequiresAttention) { $DeviceStats.DevicesRequiringAttention++ }
                    # Ownership
                    switch ($Device.OwnerType) {
                        'company' { $DeviceStats.CorporateDevices++ }
                        'personal' { $DeviceStats.PersonalDevices++ }
                    }
                }
            }

            # Find most common platform
            $MostCommonPlatform = 'None'
            if ($PlatformCounts.Count -gt 0) {
                $MostCommonPlatform = ($PlatformCounts.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key ?? 'Unknown'
            }

            $Summary = [PSCustomObject]@{
                TenantId                  = $TenantId
                ReportGeneratedDate       = Get-Date
                TotalDevicesAnalyzed      = $TotalDevicesProcessed
                TotalCompliancePolicies   = $CompliancePolicies.Count

                # Compliance State Distribution
                CompliantDevices          = $DeviceStats.CompliantDevices
                NonCompliantDevices       = $DeviceStats.NonCompliantDevices
                GracePeriodDevices        = $DeviceStats.GracePeriodDevices
                ErrorDevices              = $DeviceStats.ErrorDevices
                UnknownStateDevices       = $DeviceStats.UnknownStateDevices

                # Calculated Percentages
                ComplianceRate            = if ($TotalDevicesProcessed -gt 0) {
                    [math]::Round(($DeviceStats.CompliantDevices / $TotalDevicesProcessed) * 100, 1)
                } else { 0 }
                NonComplianceRate         = if ($TotalDevicesProcessed -gt 0) {
                    [math]::Round(($DeviceStats.NonCompliantDevices / $TotalDevicesProcessed) * 100, 1)
                } else { 0 }
                GracePeriodRate           = if ($TotalDevicesProcessed -gt 0) {
                    [math]::Round(($DeviceStats.GracePeriodDevices / $TotalDevicesProcessed) * 100, 1)
                } else { 0 }

                # Risk Assessment
                HighRiskDevices           = $DeviceStats.HighRiskDevices
                MediumRiskDevices         = $DeviceStats.MediumRiskDevices
                LowRiskDevices            = $DeviceStats.LowRiskDevices

                # Platform Distribution
                WindowsDevices            = $DeviceStats.WindowsDevices
                iOSDevices                = $DeviceStats.iOSDevices
                AndroidDevices            = $DeviceStats.AndroidDevices
                macOSDevices              = $DeviceStats.macOSDevices

                # Device Health Indicators
                StaleDevices              = $DeviceStats.StaleDevices
                RecentEnrollments         = $DeviceStats.RecentEnrollments
                DevicesRequiringAttention = $DeviceStats.DevicesRequiringAttention

                # Management Statistics
                CorporateDevices          = $DeviceStats.CorporateDevices
                PersonalDevices           = $DeviceStats.PersonalDevices

                # Top Issues
                MostCommonPlatform        = $MostCommonPlatform
            }

            Write-Information "Intune device compliance analysis completed - $($TotalDevicesProcessed) devices analyzed ($($Summary.ComplianceRate)% compliant)" -InformationAction Continue

            [PSCustomObject]@{
                Summary                 = $Summary
                DeviceComplianceDetails = $DeviceComplianceDetails | Sort-Object RiskLevel, ComplianceState, Platform, DeviceName
                ComplianceByRisk        = @{
                    High   = if ($DeviceComplianceDetails) { $DeviceComplianceDetails | Where-Object { $_.RiskLevel -eq 'High' } } else { @() }
                    Medium = if ($DeviceComplianceDetails) { $DeviceComplianceDetails | Where-Object { $_.RiskLevel -eq 'Medium' } } else { @() }
                    Low    = if ($DeviceComplianceDetails) { $DeviceComplianceDetails | Where-Object { $_.RiskLevel -eq 'Low' } } else { @() }
                }
                NonCompliantDevices     = if ($DeviceComplianceDetails) { $DeviceComplianceDetails | Where-Object { $_.ComplianceState -eq 'noncompliant' } | Sort-Object RiskLevel -Descending } else { @() }
                StaleDevicesList        = if ($DeviceComplianceDetails) { $DeviceComplianceDetails | Where-Object { $_.IsStaleDevice } | Sort-Object DaysSinceLastSync -Descending } else { @() }
                RecentEnrollments       = if ($DeviceComplianceDetails) { $DeviceComplianceDetails | Where-Object { $_.IsRecentEnrollment } | Sort-Object EnrolledDateTime -Descending } else { @() }
            }
        } catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.Exception]::new("Get-TntIntuneDeviceComplianceReport failed: $($_.Exception.Message)", $_.Exception),
                'GetTntIntuneDeviceComplianceReportError',
                [System.Management.Automation.ErrorCategory]::OperationStopped,
                $TenantId
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        } finally {
            if ($ConnectionInfo.ShouldDisconnect) {
                Disconnect-TntGraphSession -ConnectionState $ConnectionInfo
            }
        }
    }
}