← Back to articles PowerShell

Automating Intune Hardware & Managed App Inventory Telemetry Extraction to PowerBI via Microsoft Graph API

Automating Intune Hardware & Managed App Inventory Telemetry Extraction to PowerBI via Microsoft Graph API
Executive Snapshot & Pipeline Context
• Target Architecture: Automated Microsoft Graph Telemetry Harvester → Azure Blob Storage Lake → PowerBI Star Schema
• Authentication Model: Entra ID App Registration with Certificate Credentials (Least Privilege, Secretless)
• Graph API Scopes: DeviceManagementManagedDevices.Read.All, DeviceManagementApps.Read.All
• Core Telemetry Dimensions: Hardware Lifecycle (TPM 2.0, Battery Wear %, Storage Capacity) & Software Inventory (Detected Apps + Device Bridge)
• Business Value: Eliminates native Intune portal pagination bottlenecks, provides multi-year historical trend analysis, and surfaces actionable executive KPIs for fleet replacement cycles and security posture.

Endpoint telemetry extraction is a foundational capability for proactive enterprise modern management that allows IT engineering teams to monitor fleet health, security baselines, and hardware life cycles beyond the built-in boundaries of the Microsoft Intune console. While the Microsoft Intune Admin Center provides operational reports for device configuration, endpoint compliance baselines (see our guide on proactive admin auditing with PowerShell to detect unauthorized local admins), and Windows Update rings, enterprise IT architects, security auditors, and asset lifecycle managers rapidly hit severe administrative bottlenecks. Native Intune console blades paginate slowly, enforce rigid 10,000-row export caps, restrict multi-tenant correlation, and make cross-referencing installed desktop application versions with physical hardware wear metrics extraordinarily difficult.

To overcome these portal limitations and unlock executive-grade business intelligence, organizations must extract raw endpoint telemetry directly from the Microsoft Graph API and feed it into a structured, relational reporting model. This engineering blueprint provides the complete, production-hardened PowerShell automation pipeline to harvest hardware inventory, battery degradation metrics, storage capacity, and detected application catalogs into PowerBI—featuring automated pagination, exponential backoff for HTTP 429 throttling, and a resilient Star Schema data model.

Operational Comparison: Native Intune Portal vs. Graph-Powered PowerBI Pipeline

Operational Capability Native Intune Admin Center Automated Graph API + PowerBI Pipeline
Data Retention & Trends Transient; only represents current state snapshot Multi-year historical trend lake (track battery degradation & fleet expansion over time)
Export & Query Limits Hard limits at 10,000 rows; frequent UI timeouts Unlimited scale via automated @odata.nextLink pagination & batch processing
Hardware & App Correlation Separated into disconnected blades; manual VLOOKUP required Relational Star Schema linking hardware models, users, and vulnerable software in one click
Custom KPI Calculations Restricted to built-in Microsoft summary cards Custom DAX Measures (e.g., Fleet Battery Health < 75%, Storage Pressure > 90%)
API Throttling Handling Manual page refresh on browser session disconnects Automated Exponential Backoff honoring HTTP 429 Retry-After headers
Multi-Tenant Aggregation Requires tenant switching in separate browser sessions Unified multi-tenant dashboard with row-level security (RLS) per subsidiary
☁️ Microsoft Graph API /managedDevices /detectedApps ⚡ PowerShell Pipeline Pagination & Backoff Certificate Auth 💾 Data Lake / Blob Daily Snapshots (CSV/JSON) Historical Trend Store 📊 PowerBI Reports Executive Dashboards Automated Refresh FIGURE 1: INTUNE TELEMETRY TO POWERBI ARCHITECTURE
Figure 1: Automated Microsoft Graph Telemetry Harvester & PowerBI Data Modeling Pipeline.

Deep Dive: Core Microsoft Graph Telemetry Endpoints

Building an enterprise-grade reporting pipeline requires querying three foundational Graph resources. While the Microsoft Graph v1.0 endpoint exposes core inventory records, deep hardware telemetry (such as battery wear percentage and TPM specification levels) resides in the beta schema.

1. GET /deviceManagement/managedDevices (Hardware & State Telemetry) Exposes full device identity and system metadata. Key attributes include:
  • id: Unique Intune Device GUID (Primary Key for device dimensions).
  • deviceName, serialNumber, manufacturer, model: Core hardware asset tracking.
  • operatingSystem, osVersion: OS build tracking (crucial for Windows 11 23H2/24H2 readiness).
  • totalStorageSpaceInBytes, freeStorageSpaceInBytes: Storage capacity tracking.
  • hardwareInformation: Nested container containing tpmSpecificationVersion and batteryHealthPercentage (supported on compatible Windows endpoints with Endpoint Analytics enabled).
  • lastSyncDateTime, complianceState, userPrincipalName: Operational health and ownership context. For endpoints reporting stale check-ins, learn how to configure an Intune device sync force refresh to accelerate telemetry synchronization.
2. GET /deviceManagement/detectedApps (Application Master Catalog) Maintains an aggregated software catalog of all installed software discovered on enrolled corporate workstations. Key attributes include:
  • id: Unique Application GUID (Primary Key for software dimension).
  • displayName, publisher, version: Identifies software titles, major revisions, and vendors.
  • sizeInByte: Estimated installation footprint on disk.
  • deviceCount: Summary count of corporate devices reporting this specific build.
3. GET /deviceManagement/managedDevices('{id}')/detectedApps (The Relational Bridge) This endpoint provides the indispensable relational mapping linking specific devices to their installed software catalog. Without extracting this sub-resource, reporting models can only display aggregate software counts rather than answering critical security questions such as: "Which exact workstations are running an unpatched, vulnerable build of 7-Zip or Chrome?"

Production-Grade PowerShell Telemetry Harvester Script

The PowerShell script below implements enterprise-grade resilience patterns: certificate authentication via an Entra ID App Registration, automated @odata.nextLink pagination across large multi-thousand device fleets, safe null-handling for mobile and virtual endpoints, true exponential backoff with HTTP 429 Retry-After parsing, and export of the complete Star Schema tables (including the Device-to-App bridge):

<#
.SYNOPSIS
    Production-Grade Intune Telemetry Harvester for PowerBI Analytics.
.DESCRIPTION
    Extracts hardware inventory, detected applications, and device-to-app 
    associations from Microsoft Graph API with automatic pagination, exponential
    backoff on HTTP 429 throttling, and outputs Star Schema CSVs 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",

    [Parameter(Mandatory = $false)]
    [switch]$IncludeAppBridge
)

# Ensure output directory structure exists
if (-not (Test-Path $OutputDir)) {
    New-Item -Path $OutputDir -ItemType Directory -Force | Out-Null
}

# --- Resilient Graph API Invocation with HTTP 429 & 503 Exponential Backoff ---
function Invoke-ResilientGraphRequest {
    param(
        [string]$Uri,
        [int]$MaxRetries = 5
    )
    $retryCount = 0
    while ($retryCount -lt $MaxRetries) {
        try {
            return (Invoke-MgGraphRequest -Method GET -Uri $Uri -ErrorAction Stop)
        } catch {
            $statusCode = 0
            if ($_.Exception.Response) {
                $statusCode = [int]$_.Exception.Response.StatusCode
            }

            if ($statusCode -eq 429 -or $statusCode -eq 503) {
                $retryAfter = $_.Exception.Response.Headers['Retry-After']
                $waitSec = if ($retryAfter) { [int]$retryAfter } else { [math]::Pow(2, $retryCount) * 2 }
                Write-Warning "[Rate Limited: HTTP $statusCode] Pausing for $waitSec seconds (Attempt $($retryCount + 1)/$MaxRetries)..."
                Start-Sleep -Seconds $waitSec
                $retryCount++
            } else {
                throw $_
            }
        }
    }
    throw "Fatal: Max Graph API retries exceeded for URI: $Uri"
}

Write-Host ">>> [1/4] Authenticating to Microsoft Graph via Certificate..." -ForegroundColor Cyan
try {
    Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertThumbprint -NoWelcome -ErrorAction Stop
} catch {
    Write-Error "Authentication Failed: Ensure the certificate is installed in Cert:\CurrentUser\My or Cert:\LocalMachine\My."
    exit 1
}

# ==============================================================================
# 1. Harvest Dim_Devices (Hardware & Configuration Telemetry)
# ==============================================================================
Write-Host ">>> [2/4] 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-ResilientGraphRequest -Uri $deviceUri
    if ($response.value) {
        foreach ($d in $response.value) {
            $totalGB = if ($d.totalStorageSpaceInBytes) { [math]::Round(($d.totalStorageSpaceInBytes / 1GB), 2) } else { 0 }
            $freeGB  = if ($d.freeStorageSpaceInBytes) { [math]::Round(($d.freeStorageSpaceInBytes / 1GB), 2) } else { 0 }
            $usedPct = if ($totalGB -gt 0) { [math]::Round((($totalGB - $freeGB) / $totalGB) * 100, 1) } else { 0 }

            # Safe navigation for hardwareInformation (protects against null errors on mobile & VMs)
            $hw = $d.hardwareInformation
            $tpm = if ($hw) { $hw.tpmSpecificationVersion } else { $null }
            $battery = if ($hw) { $hw.batteryHealthPercentage } else { $null }

            $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        = $tpm
                BatteryHealthPct  = $battery
            })
        }
    }
    $deviceUri = $response.'@odata.nextLink'
} while ($deviceUri)

$devicesCsv = "$OutputDir\Dim_Devices.csv"
$allDevices | Export-Csv -Path $devicesCsv -NoTypeInformation -Encoding UTF8
Write-Host "[+] Exported $($allDevices.Count) endpoint records to $devicesCsv" -ForegroundColor Green

# ==============================================================================
# 2. Harvest Dim_Applications (Detected Software Catalog)
# ==============================================================================
Write-Host ">>> [3/4] Querying /deviceManagement/detectedApps with pagination..." -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-ResilientGraphRequest -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      = if ($a.sizeInByte) { [math]::Round(($a.sizeInByte / 1MB), 2) } else { 0 }
                DeviceCount = $a.deviceCount
            })
        }
    }
    $appUri = $appResponse.'@odata.nextLink'
} while ($appUri)

$appsCsv = "$OutputDir\Dim_Applications.csv"
$allApps | Export-Csv -Path $appsCsv -NoTypeInformation -Encoding UTF8
Write-Host "[+] Exported $($allApps.Count) application titles to $appsCsv" -ForegroundColor Green

# ==============================================================================
# 3. Harvest Fact_DeviceAppBridge (Relational Association)
# ==============================================================================
if ($IncludeAppBridge) {
    Write-Host ">>> [4/4] Extracting Fact_DeviceAppBridge (Device-to-App mappings)..." -ForegroundColor Cyan
    $bridgeData = [System.Collections.Generic.List[PSObject]]::new()
    $processed = 0

    foreach ($dev in $allDevices) {
        $processed++
        if ($processed % 25 -eq 0) {
            Write-Host "   Processing associations: $processed of $($allDevices.Count) devices..." -ForegroundColor Gray
        }

        $devAppUri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$($dev.DeviceId)')/detectedApps?`$select=id"
        try {
            $devApps = Invoke-ResilientGraphRequest -Uri $devAppUri
            if ($devApps.value) {
                foreach ($da in $devApps.value) {
                    $bridgeData.Add([PSCustomObject]@{
                        DeviceId = $dev.DeviceId
                        AppId    = $da.id
                    })
                }
            }
        } catch {
            Write-Warning "Skipped application inventory for device $($dev.DeviceName): $($_.Exception.Message)"
        }
    }

    $bridgeCsv = "$OutputDir\Fact_DeviceAppBridge.csv"
    $bridgeData | Export-Csv -Path $bridgeCsv -NoTypeInformation -Encoding UTF8
    Write-Host "[+] Exported $($bridgeData.Count) associations to $bridgeCsv" -ForegroundColor Green
}

Disconnect-MgGraph | Out-Null
Write-Host ">>> Telemetry Harvester Completed Successfully." -ForegroundColor Green

Designing the PowerBI Star Schema Relationship Model

In analytical business intelligence, flattening hardware properties and software catalogs into a single denormalized spreadsheet results in massive data duplication, high memory consumption, and slow visual rendering. A pure Star Schema maintains clean separation between entity dimensions and transactional facts:

Table Name Schema Role Primary Key / Foreign Key Relationship & Cardinality Analytical Focus
Dim_Devices Dimension DeviceId (PK) One-to-Many (1:*) to Fact Bridge Fleet model distribution, Windows 11 TPM 2.0 readiness, disk space pressure, battery wear analysis.
Dim_Applications Dimension AppId (PK) One-to-Many (1:*) to Fact Bridge Software portfolio governance, unapproved vendor discovery, legacy browser & runtime tracking.
Fact_DeviceAppBridge Bridge Fact DeviceId (FK), AppId (FK) Many-to-One (*:1) to Dimensions Cross-filtering engine: identifies exact devices running vulnerable builds or unapproved software packages.
Dim_Date Dimension DateKey (PK) One-to-Many (1:*) to Device Enrollment Enrollment cohort velocity, stale inactive devices (>30 days without sync), hardware replacement forecasting.

Essential DAX Measures for Enterprise Executive Dashboards

Once your Star Schema is loaded into PowerBI Desktop, implement the following DAX measures to power executive KPI cards, fleet health gauges, and security risk matrices:

// 1. Total Managed Fleet Size
Total Enrolled Devices = 
COUNTROWS(Dim_Devices)

// 2. Storage Pressure Exposure (Endpoints with < 20 GB free space or >= 90% full)
Devices Low Storage = 
CALCULATE(
    COUNTROWS(Dim_Devices),
    Dim_Devices[FreeStorageGB] < 20 || Dim_Devices[StorageUsedPct] >= 90
)

// 3. Low Storage Fleet Percentage
Fleet Low Storage Rate = 
DIVIDE([Devices Low Storage], [Total Enrolled Devices], 0)

// 4. Critical Battery Degradation Exposure (Health < 75%)
Battery Replacement Required = 
CALCULATE(
    COUNTROWS(Dim_Devices),
    Dim_Devices[BatteryHealthPct] > 0 && Dim_Devices[BatteryHealthPct] < 75
)

// 5. Windows 11 Hardware TPM 2.0 Compliance Rate
TPM 2.0 Compliance % = 
DIVIDE(
    CALCULATE(
        COUNTROWS(Dim_Devices),
        Dim_Devices[TPMVersion] >= "2.0"
    ),
    CALCULATE(
        COUNTROWS(Dim_Devices),
        NOT(ISBLANK(Dim_Devices[TPMVersion]))
    ),
    0
)

// 6. Stale Inactive Endpoints (No sync in 30+ days)
Stale Inactive Devices = 
CALCULATE(
    COUNTROWS(Dim_Devices),
    DATEDIFF(Dim_Devices[LastSyncDateTime], TODAY(), DAY) > 30
)

Automated Ingestion via Power Query (M Script)

For cloud-hosted data pipelines, store the daily CSV snapshots in an Azure Blob Storage container (e.g. https://yourstorage.blob.core.windows.net/intune-telemetry/). PowerBI can ingest the latest snapshot automatically using the following Power Query (M) pattern:

let
    // Connect to Azure Blob Storage Container
    Source = AzureStorage.Blobs("yourstorageaccount"),
    TelemetryContainer = Source{[Name="intune-telemetry"]}[Data],
    
    // Filter to the latest device inventory snapshot
    DeviceFiles = Table.SelectRows(TelemetryContainer, each Text.Contains([Name], "Dim_Devices")),
    SortedFiles = Table.Sort(DeviceFiles, {{"Date modified", Order.Descending}}),
    LatestFile = SortedFiles{0}[Content],
    
    // Parse CSV with UTF-8 encoding
    ImportedCsv = Csv.Document(LatestFile, [Delimiter=",", Columns=16, Encoding=65001, QuoteStyle=QuoteStyle.None]),
    PromotedHeaders = Table.PromoteHeaders(ImportedCsv, [PromoteAllScalars=true]),
    
    // Enforce Strongly Typed Schema
    TypedTable = Table.TransformColumnTypes(PromotedHeaders, {
        {"DeviceId", type text},
        {"DeviceName", type text},
        {"SerialNumber", type text},
        {"Manufacturer", type text},
        {"Model", type text},
        {"OS", type text},
        {"OSVersion", type text},
        {"TotalStorageGB", type number},
        {"FreeStorageGB", type number},
        {"StorageUsedPct", type number},
        {"UserPrincipalName", type text},
        {"ComplianceState", type text},
        {"LastSyncDateTime", type datetimezone},
        {"EnrolledDateTime", type datetimezone},
        {"TPMVersion", type text},
        {"BatteryHealthPct", Int64.Type}
    })
in
    TypedTable

Security, Governance & Implementation Checklist

  1. Entra ID App Registration: Register a dedicated Enterprise Application in Microsoft Entra ID. Assign least-privilege application permissions: DeviceManagementManagedDevices.Read.All and DeviceManagementApps.Read.All. Grant tenant-wide Admin Consent.
  2. Secretless Authentication: Create an asymmetric X.509 certificate (2048-bit RSA) and upload the public .cer to the Entra App Registration. Store the private .pfx securely in Azure Key Vault or the local certificate store (avoid fragile client secrets).
  3. Runbook Orchestration: Deploy the PowerShell Harvester into an Azure Automation Account (PowerShell 7.2 Runbook) or a scheduled GitHub Actions workflow running on a daily cron schedule (e.g. 0 2 * * * at 2:00 AM UTC).
  4. Staging Storage Lifecycle: Configure an Azure Blob Storage container with lifecycle management rules: automatically transition snapshots older than 30 days to Cool storage, and delete or archive after 180 days.
  5. PowerBI Service Scheduled Refresh: In PowerBI Service, configure scheduled refresh twice daily (04:00 AM and 12:00 PM) using organizational credentials against Azure Blob Storage. Configure Row-Level Security (RLS) to restrict regional support managers to their department's endpoints.
💡 Engineering Field Note By cross-referencing physical hardware attributes (battery health degradation %, low free disk space) with software inventory in PowerBI, IT engineering teams transform reactive ticket management into proactive fleet renewal. You can replace swollen or failing laptop batteries weeks before hardware failure and identify high-risk legacy application versions across tens of thousands of endpoints in seconds.

Was this article helpful?

🎯
MSEndpoint Academy

Assess Your Microsoft 365 & Intune Skills (MD-102)

100% Free • 5 Min

Applying this guide in production? Test your technical readiness against real exam scenarios from Microsoft 365 Certified: Endpoint Administrator (MD-102). Identify your strengths and knowledge gaps instantly.

💡 Express Knowledge Check Question 1 of 10

Which official utility is required to convert a Win32 application installer (.exe) into the package format (.intunewin) for deployment via Microsoft Intune?

🔒 100% Free • 📊 Instant Scorecard • 🤖 AI Explanations
Take Full Diagnostic Exam (10 Questions) →

🎓 Ready to go deeper?

Practice real MD-102 exam questions, get AI feedback on your weak areas, and fast-track your Intune certification.

Start Free Practice → Book a Session
Souhaiel Morhag
Souhaiel Morhag
Microsoft Endpoint & Modern Workplace Engineer

Souhaiel Morhag is a Microsoft Intune and endpoint management specialist with hands-on experience deploying and securing enterprise environments across Microsoft 365. He founded MSEndpoint.com to share practical, real-world guides for IT admins navigating Microsoft technologies — and built the MSEndpoint Academy at app.msendpoint.com/academy, a dedicated learning platform for professionals preparing for the MD-102 (Microsoft 365 Endpoint Administrator) certification. Through in-depth articles and AI-powered practice exams, Souhaiel helps IT teams move faster and certify with confidence.

Related Articles

Popular on MSEndpoint