Private/Get-IslExportReport.ps1

function Get-IslExportReport {
    <#
    .SYNOPSIS
        Runs one Intune report export job and returns its rows.
 
    .DESCRIPTION
        The per-app status endpoints (installSummary, deviceStatuses, userStatuses) are gone from
        Graph (Findings.md, "Install status via Graph"); the export jobs under
        deviceManagement/reports are what works. This posts one job, polls it every five seconds
        until it completes, downloads the zipped CSV it produces and returns the rows as objects.
        AppInstallStatusAggregate gives every app's install counts in one job of about twenty
        seconds; DeviceInstallStatusByApp needs an ApplicationId filter and one job per app.
 
    .PARAMETER ReportName
        The report to export, e.g. AppInstallStatusAggregate.
 
    .PARAMETER Filter
        The report's filter expression, when it needs one.
 
    .PARAMETER Select
        The columns to export; all of them when omitted.
 
    .PARAMETER TimeoutSeconds
        How long to wait for the job. Default 300.
 
    .EXAMPLE
        Get-IslExportReport -ReportName AppInstallStatusAggregate
 
        One row per app with InstalledDeviceCount, FailedDeviceCount and the other counts.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [string]$ReportName,

        [string]$Filter,

        [string[]]$Select,

        [int]$TimeoutSeconds = 300
    )

    $body = @{ reportName = $ReportName; format = 'csv' }
    if ($Filter) { $body.filter = $Filter }
    if ($Select) { $body.select = $Select }
    $jobs = '/beta/deviceManagement/reports/exportJobs'
    $job = Invoke-IslGraphRequest -Uri $jobs -Method POST -Body $body
    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    while ("$($job.status)" -in 'notStarted', 'inProgress') {
        if ((Get-Date) -gt $deadline) {
            throw "The $ReportName export job did not complete within $TimeoutSeconds seconds"
        }
        Start-Sleep -Seconds 5
        $job = Invoke-IslGraphRequest -Uri "$jobs/$($job.id)"
    }
    if ("$($job.status)" -ne 'completed' -or -not $job.url) {
        throw "The $ReportName export job ended with status '$($job.status)'"
    }

    $zipName = "IntuneScriptLab-$([guid]::NewGuid().ToString('N')).zip"
    $zip = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $zipName
    try {
        Invoke-WebRequest -Uri $job.url -OutFile $zip -UseBasicParsing
        Add-Type -AssemblyName System.IO.Compression.FileSystem
        $archive = [System.IO.Compression.ZipFile]::OpenRead($zip)
        try {
            $entry = $archive.Entries | Where-Object Name -like '*.csv' | Select-Object -First 1
            if (-not $entry) { throw "The $ReportName export contains no CSV file" }
            $reader = [System.IO.StreamReader]::new($entry.Open())
            try { $csv = $reader.ReadToEnd() } finally { $reader.Dispose() }
        }
        finally { $archive.Dispose() }
        @($csv | ConvertFrom-Csv)
    }
    finally {
        Remove-Item -LiteralPath $zip -Force -ErrorAction SilentlyContinue
    }
}