While the Microsoft Intune Admin Center offers built-in operational reports for compliance, device configuration, and update rings, enterprise IT executives, security auditors, and asset managers quickly run into UI limitations. Native Intune blades paginate slowly, restrict multi-tenant aggregation, and make cross-referencing installed software versions with hardware lifecycle metrics cumbersome.
To unlock deep endpoint analytics, organizations must extract raw telemetry directly from the Microsoft Graph API and feed it into a structured business intelligence model. This architectural guide walks through building an automated, resilient PowerShell pipeline that extracts hardware inventory, battery health, storage capacity, and detected application catalogs into PowerBI with zero manual overhead.
Understanding Key Graph API Telemetry Endpoints
To extract full fidelity hardware and software datasets, our automation interacts with two distinct Microsoft Graph Beta endpoints:
/deviceManagement/managedDevices
Contains core hardware attributes: Serial number, manufacturer, model, total storage space, free storage space, TPM specification version, battery health, Wi-Fi MAC address, enrolled user UPN, and OS build version.
/deviceManagement/detectedApps
Contains discovered software titles on corporate-owned devices: Application display name, publisher, version string, size, and the device count running that specific build.
Production PowerShell Telemetry Harvester Script
The following script authenticates using certificate-based authentication against an Entra ID App Registration, handles @odata.nextLink pagination automatically, implements exponential backoff on HTTP 429 throttling, and outputs clean CSV exports ready for PowerBI ingestion:
# ==============================================================================
# Automated Intune Telemetry Harvester for PowerBI
# ==============================================================================
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$ClientId,
[Parameter(Mandatory = $true)]
[string]$CertThumbprint,
[Parameter(Mandatory = $false)]
[string]$OutputDir = "$PSScriptRoot\Export"
)
if (-not (Test-Path $OutputDir)) { New-Item -Path $OutputDir -ItemType Directory -Force | Out-Null }
Write-Host ">>> [1/3] Authenticating to Microsoft Graph via Certificate..." -ForegroundColor Cyan
Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertThumbprint -NoWelcome
# 1. Harvest Managed Devices Hardware Telemetry
Write-Host ">>> [2/3] Querying /deviceManagement/managedDevices with pagination..." -ForegroundColor Cyan
$deviceUri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$select=id,deviceName,serialNumber,manufacturer,model,operatingSystem,osVersion,totalStorageSpaceInBytes,freeStorageSpaceInBytes,userPrincipalName,complianceState,lastSyncDateTime,enrolledDateTime,chassisType,hardwareInformation"
$allDevices = [System.Collections.Generic.List[PSObject]]::new()
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $deviceUri
if ($response.value) {
foreach ($d in $response.value) {
$totalGB = [math]::Round(($d.totalStorageSpaceInBytes / 1GB), 2)
$freeGB = [math]::Round(($d.freeStorageSpaceInBytes / 1GB), 2)
$usedPct = if ($totalGB -gt 0) { [math]::Round((($totalGB - $freeGB) / $totalGB) * 100, 1) } else { 0 }
$allDevices.Add([PSCustomObject]@{
DeviceId = $d.id
DeviceName = $d.deviceName
SerialNumber = $d.serialNumber
Manufacturer = $d.manufacturer
Model = $d.model
OS = $d.operatingSystem
OSVersion = $d.osVersion
TotalStorageGB = $totalGB
FreeStorageGB = $freeGB
StorageUsedPct = $usedPct
UserPrincipalName = $d.userPrincipalName
ComplianceState = $d.complianceState
LastSyncDateTime = $d.lastSyncDateTime
EnrolledDateTime = $d.enrolledDateTime
TPMVersion = $d.hardwareInformation.tpmSpecificationVersion
BatteryHealthPct = $d.hardwareInformation.batteryHealthPercentage
})
}
}
$deviceUri = $response.'@odata.nextLink'
} while ($deviceUri)
$devicesCsv = "$OutputDir\Intune_Devices_Inventory.csv"
$allDevices | Export-Csv -Path $devicesCsv -NoTypeInformation -Encoding UTF8
Write-Host "Exported $($allDevices.Count) devices to $devicesCsv" -ForegroundColor Green
# 2. Harvest Detected Applications Catalog
Write-Host ">>> [3/3] Querying /deviceManagement/detectedApps..." -ForegroundColor Cyan
$appUri = "https://graph.microsoft.com/beta/deviceManagement/detectedApps?`$select=id,displayName,version,publisher,sizeInByte,deviceCount"
$allApps = [System.Collections.Generic.List[PSObject]]::new()
do {
$appResponse = Invoke-MgGraphRequest -Method GET -Uri $appUri
if ($appResponse.value) {
foreach ($a in $appResponse.value) {
$allApps.Add([PSCustomObject]@{
AppId = $a.id
DisplayName = $a.displayName
Version = $a.version
Publisher = $a.publisher
SizeMB = [math]::Round(($a.sizeInByte / 1MB), 2)
DeviceCount = $a.deviceCount
})
}
}
$appUri = $appResponse.'@odata.nextLink'
} while ($appUri)
$appsCsv = "$OutputDir\Intune_Detected_Apps.csv"
$allApps | Export-Csv -Path $appsCsv -NoTypeInformation -Encoding UTF8
Write-Host "Exported $($allApps.Count) application records to $appsCsv" -ForegroundColor Green
Designing the PowerBI Data Model (Star Schema)
To avoid slow visual rendering and circular dependencies in PowerBI, configure your relationship model as a pure Star Schema:
| Table Name | Type | Primary Key / Foreign Key | Key Measures & Visual Analytics |
|---|---|---|---|
Dim_Devices |
Dimension | DeviceId (PK) |
Fleet Breakdown by Model, TPM 2.0 Compliance, Low Disk Space (<15% free). |
Dim_Applications |
Dimension | AppId (PK) |
Vulnerable Software Titles, Shadow IT Discovery, Outdated Browsers. |
Dim_Date |
Dimension | DateKey (PK) |
Enrollment velocity, Inactive devices (>30 days without sync). |
Fact_DeviceAppBridge |
Fact | DeviceId (FK), AppId (FK) |
Total app deployment penetration, End-of-Life software exposure. |
Summary & Implementation Checklist for Engineers
- App Registration: Register an application in Microsoft Entra ID with
DeviceManagementManagedDevices.Read.AllandDeviceManagementApps.Read.Allpermissions. - Deploy Automation: Host the extraction script in an Azure Automation Account or scheduled GitHub Actions workflow with certificate credentials.
- Configure Staging Lake: Push daily CSV or Parquet outputs to an Azure Storage Blob container with 90-day lifecycle retention.
- Connect PowerBI: In PowerBI Desktop, use the Azure Blob Storage connector and set the Star Schema relationships.
- Publish & Schedule Refresh: Publish the dashboard to the PowerBI Service and configure automated scheduled refresh twice daily.