11function Set-CIPPDBCacheDetectedApps {
22 <#
33 . SYNOPSIS
4- Caches all detected apps for a tenant, including devices that have each app
4+ Caches detected apps using the AppInvRawData export submitted earlier,
5+ enriched with the live /detectedApps catalog.
56
67 . PARAMETER TenantFilter
7- The tenant to cache detected apps for
8+ The tenant to cache detected apps for.
89
910 . PARAMETER QueueId
10- The queue ID to update with total tasks (optional)
11+ Optional queue ID for progress tracking.
1112 #>
1213 [CmdletBinding ()]
1314 param (
@@ -16,86 +17,111 @@ function Set-CIPPDBCacheDetectedApps {
1617 [string ]$QueueId
1718 )
1819
20+ $ReportName = ' AppInvRawData'
21+
1922 try {
20- Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message ' Caching detected apps' - sev Debug
23+ $JobsTable = Get-CIPPTable - tablename ' IntuneReportJobs'
24+ $JobRow = Get-CIPPAzDataTableEntity @JobsTable - Filter " PartitionKey eq '$TenantFilter ' and RowKey eq '$ReportName '"
2125
22- # Step 1: Get first page with noPaginate to avoid sequential chase, and read @odata.count
23- $FirstPageResult = New-GraphBulkRequest - Requests @ (
24- [PSCustomObject ]@ {
25- id = ' detectedApps-0'
26- method = ' GET'
27- url = ' deviceManagement/detectedApps'
28- }
29- ) - tenantid $TenantFilter - NoPaginateIds @ (' detectedApps-0' )
26+ if (-not $JobRow ) {
27+ Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " No $ReportName job submitted - skipping detected apps cache" - sev Info
28+ return
29+ }
3030
31- $FirstResponse = ($FirstPageResult | Where-Object { $_.id -eq ' detectedApps-0' }).body
32- $TotalCount = $FirstResponse .' @odata.count'
33- $DetectedApps = [System.Collections.Generic.List [PSCustomObject ]]::new()
34- foreach ($app in $FirstResponse.value ) { $DetectedApps.Add ($app ) }
31+ $JobId = $JobRow.JobId
32+ if (-not $JobId ) {
33+ Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " IntuneReportJobs row missing JobId - removing" - sev Warning
34+ Remove-AzDataTableEntity @JobsTable - Entity $JobRow - Force - ErrorAction SilentlyContinue
35+ return
36+ }
3537
36- Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " DetectedApps total count: $TotalCount , first page: $ ( $DetectedApps.Count ) " - sev Debug
38+ try {
39+ $Job = New-GraphGetRequest - uri " https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs/$JobId " - tenantid $TenantFilter
40+ } catch {
41+ $ErrorMessage = Get-CippException - Exception $_
42+ Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " $ReportName job $JobId not retrievable: $ ( $ErrorMessage.NormalizedError ) " - sev Warning - LogData $ErrorMessage
43+ Remove-AzDataTableEntity @JobsTable - Entity $JobRow - Force - ErrorAction SilentlyContinue
44+ return
45+ }
3746
38- # Step 2: If more pages exist, pre-calculate all skip offsets and fire as batches
39- if ($FirstResponse .' @odata.nextLink' -and $TotalCount -gt 50 ) {
40- $SkipRequests = [System.Collections.Generic.List [PSCustomObject ]]::new()
41- for ($skip = 50 ; $skip -lt $TotalCount ; $skip += 50 ) {
42- $SkipRequests.Add ([PSCustomObject ]@ {
43- id = " detectedApps-$skip "
44- method = ' GET'
45- url = " deviceManagement/detectedApps?`$ skip=$skip "
46- })
47+ switch ($Job.status ) {
48+ ' completed' { }
49+ ' failed' {
50+ Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " $ReportName job $JobId failed" - sev Error
51+ Remove-AzDataTableEntity @JobsTable - Entity $JobRow - Force - ErrorAction SilentlyContinue
52+ return
4753 }
48- Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " Fetching $ ( $SkipRequests.Count ) remaining pages in bulk" - sev Debug
49-
50- # New-GraphBulkRequest auto-batches into groups of 20, NoPaginateIds prevents chasing empty nextLinks
51- $SkipResults = New-GraphBulkRequest - Requests @ ($SkipRequests ) - tenantid $TenantFilter - NoPaginateIds @ ($SkipRequests.id )
52-
53- foreach ($Result in $SkipResults ) {
54- if ($Result.status -eq 200 -and $Result.body.value ) {
55- foreach ($app in $Result.body.value ) { $DetectedApps.Add ($app ) }
56- }
54+ default {
55+ Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " $ReportName job $JobId still '$ ( $Job.status ) ' - skipping" - sev Info
56+ return
5757 }
5858 }
5959
60- Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " Retrieved $ ( $DetectedApps.Count ) detected apps (expected $TotalCount )" - sev Debug
61-
62- if ($DetectedApps.Count -eq 0 ) {
63- Add-CIPPDbItem - TenantFilter $TenantFilter - Type ' DetectedApps' - Data @ () - AddCount
60+ if (-not $Job.url ) {
61+ Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " $ReportName job $JobId completed but no url returned" - sev Error
62+ Remove-AzDataTableEntity @JobsTable - Entity $JobRow - Force - ErrorAction SilentlyContinue
6463 return
6564 }
6665
67- # Step 3: Bulk fetch managed devices for each app (unchanged from original)
68- $DeviceRequests = $DetectedApps | Where-Object { $_.id } | ForEach-Object {
69- [PSCustomObject ]@ {
70- id = $_.id
71- method = ' GET'
72- url = " deviceManagement/detectedApps('$ ( $_.id ) ')/managedDevices"
73- }
66+ $ZipBytes = (Invoke-WebRequest - Uri $Job.url - UseBasicParsing - ErrorAction Stop).Content
67+ if ($ZipBytes -isnot [byte []]) { throw " Expected binary content from $ReportName download" }
68+
69+ $JsonText = $null
70+ $ZipStream = [System.IO.MemoryStream ]::new($ZipBytes , $false )
71+ try {
72+ $Archive = [System.IO.Compression.ZipArchive ]::new($ZipStream , [System.IO.Compression.ZipArchiveMode ]::Read)
73+ try {
74+ $Entry = $Archive.Entries | Where-Object { $_.Name -like ' *.json' } | Select-Object - First 1
75+ if (-not $Entry ) { throw " No JSON entry in $ReportName archive" }
76+ $EntryStream = $Entry.Open ()
77+ try {
78+ $Reader = [System.IO.StreamReader ]::new($EntryStream )
79+ try { $JsonText = $Reader.ReadToEnd () } finally { $Reader.Dispose () }
80+ } finally { $EntryStream.Dispose () }
81+ } finally { $Archive.Dispose () }
82+ } finally {
83+ $ZipStream.Dispose ()
84+ $ZipBytes = $null
7485 }
7586
76- if ($DeviceRequests ) {
77- Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " Fetching devices for $ ( $DetectedApps.Count ) detected apps" - sev Debug
78- $DeviceResults = New-GraphBulkRequest - Requests @ ($DeviceRequests ) - tenantid $TenantFilter
79-
80- # Add devices to each detected app object
81- $DetectedAppsWithDevices = foreach ($App in $DetectedApps ) {
82- $Devices = Get-GraphBulkResultByID - Results $DeviceResults - ID $App.id - Value
83- $App | Add-Member - NotePropertyName ' managedDevices' - NotePropertyValue ($Devices ?? @ ()) - Force
84- $App
87+ $ExportRows = @ (($JsonText | ConvertFrom-Json ).values)
88+ $JsonText = $null
89+
90+ $AppsByKey = @ {}
91+ foreach ($Row in $ExportRows ) {
92+ $AppId = $Row.ApplicationKey
93+ if (-not $AppId ) { continue }
94+ if (-not $AppsByKey.ContainsKey ($AppId )) {
95+ $AppsByKey [$AppId ] = [pscustomobject ]@ {
96+ id = $AppId
97+ displayName = $Row.ApplicationName
98+ version = $Row.ApplicationVersion
99+ publisher = $Row.ApplicationPublisher
100+ platform = $Row.Platform
101+ deviceCount = 0
102+ managedDevices = [System.Collections.Generic.List [object ]]::new()
103+ }
85104 }
86-
87- Add-CIPPDbItem - TenantFilter $TenantFilter - Type ' DetectedApps' - Data $DetectedAppsWithDevices - AddCount
88- $DetectedApps = $null
89- $DetectedAppsWithDevices = $null
90- } else {
91- Add-CIPPDbItem - TenantFilter $TenantFilter - Type ' DetectedApps' - Data $DetectedApps - AddCount
92- $DetectedApps = $null
105+ $App = $AppsByKey [$AppId ]
106+ $App.managedDevices.Add ([pscustomobject ]@ {
107+ id = $Row.DeviceId
108+ deviceName = $Row.DeviceName
109+ osVersion = $Row.OSVersion
110+ platform = $Row.Platform
111+ userId = $Row.UserId
112+ userPrincipalName = $Row.UserName
113+ emailAddress = $Row.EmailAddress
114+ })
115+ $App.deviceCount ++
93116 }
94117
95- Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message ' Cached detected apps with devices successfully' - sev Debug
118+ $DetectedApps = @ ($AppsByKey.Values )
119+ Add-CIPPDbItem - TenantFilter $TenantFilter - Type ' DetectedApps' - Data $DetectedApps - AddCount
120+ Write-LogMessage - API ' CIPPDBCache' - tenant $TenantFilter - message " Cached $ ( $DetectedApps.Count ) detected apps with devices from export $JobId " - sev Info
96121
122+ Remove-AzDataTableEntity @JobsTable - Entity $JobRow - Force - ErrorAction SilentlyContinue
97123 } catch {
98- Write-LogMessage - API ' CIPPDBCache ' - tenant $TenantFilter `
99- - message " Failed to cache detected apps: $ ( $_ .Exception.Message ) " - sev Error
124+ $ErrorMessage = Get-CippException - Exception $_
125+ Write-LogMessage - API ' CIPPDBCache ' - tenant $TenantFilter - message " Failed to cache detected apps: $ ( $ErrorMessage .NormalizedError ) " - sev Error - LogData $ErrorMessage
100126 }
101127}
0 commit comments