Public/Get-IntuneAppInventory.ps1

function Get-IntuneAppInventory {
    <#
    .SYNOPSIS
        Audits Intune managed applications, install status, and assignment health.
    .DESCRIPTION
        Enumerates all mobile apps managed through Intune, checks install success/failure
        counts, identifies apps with high failure rates, unassigned apps, and apps with
        outdated or missing versions.
    .PARAMETER FailureThresholdPercent
        Percentage of failed installs before flagging an app. Defaults to 10.
    .PARAMETER IncludeStore
        Include store apps (iOS App Store, Google Play, Microsoft Store). Excluded by default.
    .EXAMPLE
        Get-IntuneAppInventory -FailureThresholdPercent 5
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateRange(1, 100)]
        [int]$FailureThresholdPercent = 10,

        [Parameter()]
        [switch]$IncludeStore
    )

    begin {
        Test-GraphConnection
        $results = [System.Collections.Generic.List[PSObject]]::new()
    }

    process {
        $apps = Get-MgDeviceAppManagementMobileApp -All -Property @(
            'id', 'displayName', '@odata.type', 'publisher',
            'createdDateTime', 'lastModifiedDateTime'
        )

        if (-not $IncludeStore) {
            $apps = $apps | Where-Object {
                $_.'@odata.type' -notmatch 'iosStoreApp|androidStoreApp|microsoftStoreForBusinessApp'
            }
        }

        foreach ($app in $apps) {
            $findings = @()
            $appType = ($app.AdditionalProperties.'@odata.type' ?? $app.'@odata.type') -replace '#microsoft.graph.', ''

            # Get assignments
            try {
                $assignments = Get-MgDeviceAppManagementMobileAppAssignment -MobileAppId $app.Id -ErrorAction Stop
                $assignmentCount = ($assignments | Measure-Object).Count
            }
            catch {
                $assignments = @()
                $assignmentCount = 0
            }

            if ($assignmentCount -eq 0) {
                $findings += 'UNASSIGNED'
            }

            # Get device install status summary
            try {
                $statusSummary = Invoke-MgGraphRequest -Method GET `
                    -Uri "https://graph.microsoft.com/v1.0/deviceAppManagement/mobileApps/$($app.Id)/deviceStatuses" `
                    -ErrorAction Stop

                $statuses = $statusSummary.value
                $totalInstalls = ($statuses | Measure-Object).Count
                $failures = ($statuses | Where-Object { $_.installState -eq 'failed' } | Measure-Object).Count
                $pending = ($statuses | Where-Object { $_.installState -eq 'notInstalled' } | Measure-Object).Count

                if ($totalInstalls -gt 0) {
                    $failureRate = [math]::Round(($failures / $totalInstalls) * 100, 1)
                    if ($failureRate -ge $FailureThresholdPercent) {
                        $findings += "HIGH FAILURE RATE ($failureRate% - $failures of $totalInstalls)"
                    }
                }
            }
            catch {
                $totalInstalls = 0
                $failures = 0
                $pending = 0
                $failureRate = 0
            }

            # Check for stale apps (not modified in 180+ days and has failures)
            if ($app.LastModifiedDateTime) {
                $daysSinceUpdate = [math]::Round(((Get-Date) - $app.LastModifiedDateTime).TotalDays)
                if ($daysSinceUpdate -gt 180 -and $failures -gt 0) {
                    $findings += "STALE APP ($daysSinceUpdate days since update)"
                }
            }

            $results.Add([PSCustomObject]@{
                AppName         = $app.DisplayName
                AppType         = $appType
                Publisher       = $app.Publisher
                Assignments     = $assignmentCount
                TotalDevices    = $totalInstalls
                Installed       = $totalInstalls - $failures - $pending
                Failed          = $failures
                Pending         = $pending
                FailureRate     = "$failureRate%"
                LastModified    = $app.LastModifiedDateTime
                Finding         = if ($findings.Count -gt 0) { $findings -join ' | ' } else { 'OK' }
            })
        }
    }

    end {
        $flagged = $results | Where-Object { $_.Finding -ne 'OK' }
        Write-Host " Apps reviewed: $($results.Count) | Findings: $($flagged.Count)" -ForegroundColor Gray
        $results | Sort-Object Finding, AppName
    }
}