Skip to content

Commit 69b0196

Browse files
authored
Merge pull request #1036 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents a56a15c + 10f2478 commit 69b0196

7 files changed

Lines changed: 259 additions & 125 deletions

File tree

Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-IntuneReportExportSubmit.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ function Push-IntuneReportExportSubmit {
2525
'UserId', 'UserName', 'EmailAddress'
2626
)
2727
}
28+
'AppInstallStatusAggregate' {
29+
@(
30+
'ApplicationId', 'DisplayName', 'Publisher', 'Platform', 'AppVersion', 'AppPlatform',
31+
'InstalledDeviceCount', 'FailedDeviceCount', 'FailedUserCount',
32+
'PendingInstallDeviceCount', 'NotInstalledDeviceCount', 'FailedDevicePercentage'
33+
)
34+
}
2835
default { throw "Unknown Intune report '$ReportName'" }
2936
}
3037

Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ function Get-CIPPAlertIntunePolicyConflicts {
4949
}
5050

5151
$AlertableStatuses = @(
52-
if ($Config.AlertErrors) { 'error'; 'failed' }
52+
if ($Config.AlertErrors) { 'error' }
5353
if ($Config.AlertConflicts) { 'conflict' }
5454
)
5555

56-
if (-not $AlertableStatuses) {
56+
if (-not $AlertableStatuses -and -not ($Config.IncludeApplications -and $Config.AlertErrors)) {
5757
return
5858
}
5959

@@ -64,56 +64,66 @@ function Get-CIPPAlertIntunePolicyConflicts {
6464

6565
$Issues = [System.Collections.Generic.List[object]]::new()
6666

67-
if ($Config.IncludePolicies) {
68-
try {
69-
$ManagedDevices = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$select=id,deviceName,userPrincipalName&`$expand=deviceConfigurationStates(`$select=displayName,state,settingStates)" -tenantid $TenantFilter
70-
71-
foreach ($Device in $ManagedDevices) {
72-
$PolicyStates = $Device.deviceConfigurationStates | Where-Object { $_.state -and ($AlertableStatuses -contains $_.state) }
73-
foreach ($State in $PolicyStates) {
74-
$Issues.Add([PSCustomObject]@{
75-
Message = "Policy '$($State.displayName)' is $($State.state) on device '$($Device.deviceName)' for $($Device.userPrincipalName)."
76-
Tenant = $TenantFilter
77-
Type = 'Policy'
78-
PolicyName = $State.displayName
79-
IssueStatus = $State.state
80-
DeviceName = $Device.deviceName
81-
UserPrincipalName = $Device.userPrincipalName
82-
DeviceId = $Device.id
83-
})
67+
if ($Config.IncludePolicies -and $AlertableStatuses) {
68+
$PolicySources = @(
69+
@{ Type = 'IntuneDeviceCompliancePolicies'; Kind = 'Compliance' }
70+
@{ Type = 'IntuneDeviceConfigurations'; Kind = 'Configuration' }
71+
)
72+
73+
foreach ($Source in $PolicySources) {
74+
try {
75+
$PolicyItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type $Source.Type | Where-Object { $_.RowKey -notlike '*-Count' }
76+
foreach ($PolicyItem in $PolicyItems) {
77+
$Policy = try { $PolicyItem.Data | ConvertFrom-Json -ErrorAction Stop } catch { $null }
78+
if (-not $Policy.id) { continue }
79+
80+
$StatusItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type "$($Source.Type)_$($Policy.id)" | Where-Object { $_.RowKey -notlike '*-Count' }
81+
foreach ($StatusItem in $StatusItems) {
82+
$State = try { $StatusItem.Data | ConvertFrom-Json -ErrorAction Stop } catch { $null }
83+
if (-not $State.status -or ($AlertableStatuses -notcontains $State.status.ToLowerInvariant())) { continue }
84+
85+
$Issues.Add([PSCustomObject]@{
86+
Message = "$($Source.Kind) policy '$($Policy.displayName)' is $($State.status) on device '$($State.deviceDisplayName)' for $($State.userPrincipalName)."
87+
Tenant = $TenantFilter
88+
Type = 'Policy'
89+
PolicyType = $Source.Kind
90+
PolicyName = $Policy.displayName
91+
IssueStatus = $State.status
92+
DeviceName = $State.deviceDisplayName
93+
UserPrincipalName = $State.userPrincipalName
94+
DeviceId = $State.id
95+
})
96+
}
8497
}
98+
} catch {
99+
$ErrorMessage = Get-CippException -Exception $_
100+
Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Failed to read cached $($Source.Kind) policy states: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
85101
}
86-
} catch {
87-
$ErrorMessage = Get-CippException -Exception $_
88-
Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Failed to query Intune policy states: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
89102
}
90103
}
91104

92-
if ($Config.IncludeApplications) {
105+
if ($Config.IncludeApplications -and $Config.AlertErrors) {
93106
try {
94-
$Applications = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?`$select=id,displayName&`$expand=deviceStatuses(`$select=installState,deviceName,userPrincipalName,deviceId)" -tenantid $TenantFilter
95-
96-
foreach ($App in $Applications) {
97-
$BadStatuses = $App.deviceStatuses | Where-Object {
98-
$_.installState -and ($AlertableStatuses -contains $_.installState.ToLowerInvariant())
99-
}
100-
101-
foreach ($Status in $BadStatuses) {
102-
$Issues.Add([PSCustomObject]@{
103-
Message = "App '$($App.displayName)' install is $($Status.installState) on device '$($Status.deviceName)' for $($Status.userPrincipalName)."
104-
Tenant = $TenantFilter
105-
Type = 'Application'
106-
AppName = $App.displayName
107-
IssueStatus = $Status.installState
108-
DeviceName = $Status.deviceName
109-
UserPrincipalName = $Status.userPrincipalName
110-
DeviceId = $Status.deviceId
111-
})
112-
}
107+
$AppItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppInstallStatusAggregate' | Where-Object { $_.RowKey -notlike '*-Count' }
108+
foreach ($AppItem in $AppItems) {
109+
$App = try { $AppItem.Data | ConvertFrom-Json -ErrorAction Stop } catch { $null }
110+
if (-not $App -or [int]($App.failedDeviceCount) -le 0) { continue }
111+
112+
$Issues.Add([PSCustomObject]@{
113+
Message = "App '$($App.displayName)' failed to install on $($App.failedDeviceCount) device(s) ($($App.failedDevicePercentage)%)."
114+
Tenant = $TenantFilter
115+
Type = 'Application'
116+
AppName = $App.displayName
117+
IssueStatus = 'failed'
118+
FailedDeviceCount = [int]$App.failedDeviceCount
119+
FailedUserCount = [int]$App.failedUserCount
120+
FailedPercentage = $App.failedDevicePercentage
121+
Platform = $App.platform
122+
})
113123
}
114124
} catch {
115125
$ErrorMessage = Get-CippException -Exception $_
116-
Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Failed to query Intune application states: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
126+
Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Failed to read cached Intune app install status: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
117127
}
118128
}
119129

Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-IntuneReportExportOrchestrator.ps1

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@ function Start-IntuneReportExportOrchestrator {
3131
return
3232
}
3333

34-
$Queue = New-CippQueueEntry -Name 'Intune Report Export Submission' -TotalTasks $LicensedTenants.Count
34+
$ReportNames = @('AppInvRawData', 'AppInstallStatusAggregate')
35+
36+
$Queue = New-CippQueueEntry -Name 'Intune Report Export Submission' -TotalTasks ($LicensedTenants.Count * $ReportNames.Count)
3537

3638
$Batch = foreach ($Tenant in $LicensedTenants) {
37-
[PSCustomObject]@{
38-
FunctionName = 'IntuneReportExportSubmit'
39-
TenantFilter = $Tenant.defaultDomainName
40-
ReportName = 'AppInvRawData'
41-
QueueId = $Queue.RowKey
42-
QueueName = "Intune Export Submit - $($Tenant.defaultDomainName)"
39+
foreach ($ReportName in $ReportNames) {
40+
[PSCustomObject]@{
41+
FunctionName = 'IntuneReportExportSubmit'
42+
TenantFilter = $Tenant.defaultDomainName
43+
ReportName = $ReportName
44+
QueueId = $Queue.RowKey
45+
QueueName = "Intune Export Submit ($ReportName) - $($Tenant.defaultDomainName)"
46+
}
4347
}
4448
}
4549

Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ function Invoke-CIPPDBCacheCollection {
124124
'IntuneScripts'
125125
'IntuneReusableSettings'
126126
'DetectedApps'
127+
'IntuneAppInstallStatus'
127128
'MDEOnboarding'
128129
)
129130
Compliance = @(
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
function Set-CIPPDBCacheIntuneAppInstallStatus {
2+
<#
3+
.SYNOPSIS
4+
Caches per-application install status counts from the AppInstallStatusAggregate
5+
export submitted earlier.
6+
7+
.DESCRIPTION
8+
The AppInstallStatusAggregate report is the only tenant-wide app install report Intune
9+
exposes without a per-app filter, so it carries rollup counts (FailedDeviceCount etc.)
10+
rather than per-device detail. Get-CIPPAlertIntunePolicyConflicts reads the cached rows
11+
to flag applications that are failing to install.
12+
13+
.PARAMETER TenantFilter
14+
The tenant to cache app install status for.
15+
16+
.PARAMETER QueueId
17+
Optional queue ID for progress tracking.
18+
#>
19+
[CmdletBinding()]
20+
param(
21+
[Parameter(Mandatory = $true)]
22+
[string]$TenantFilter,
23+
[string]$QueueId
24+
)
25+
26+
$ReportName = 'AppInstallStatusAggregate'
27+
28+
try {
29+
$JobsTable = Get-CIPPTable -tablename 'IntuneReportJobs'
30+
$JobRow = Get-CIPPAzDataTableEntity @JobsTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$ReportName'"
31+
32+
if (-not $JobRow) {
33+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "No $ReportName job submitted - skipping app install status cache" -sev Info
34+
return
35+
}
36+
37+
$JobId = $JobRow.JobId
38+
if (-not $JobId) {
39+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'IntuneReportJobs row missing JobId - removing' -sev Warning
40+
Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue
41+
return
42+
}
43+
44+
try {
45+
$Job = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs/$JobId" -tenantid $TenantFilter
46+
} catch {
47+
$ErrorMessage = Get-CippException -Exception $_
48+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "$ReportName job $JobId not retrievable: $($ErrorMessage.NormalizedError)" -sev Warning -LogData $ErrorMessage
49+
Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue
50+
return
51+
}
52+
53+
switch ($Job.status) {
54+
'completed' { }
55+
'failed' {
56+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "$ReportName job $JobId failed" -sev Error
57+
Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue
58+
return
59+
}
60+
default {
61+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "$ReportName job $JobId still '$($Job.status)' - skipping" -sev Info
62+
return
63+
}
64+
}
65+
66+
if (-not $Job.url) {
67+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "$ReportName job $JobId completed but no url returned" -sev Error
68+
Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue
69+
return
70+
}
71+
72+
$ZipBytes = (Invoke-WebRequest -Uri $Job.url -UseBasicParsing -ErrorAction Stop).Content
73+
if ($ZipBytes -isnot [byte[]]) { throw "Expected binary content from $ReportName download" }
74+
75+
$JsonText = $null
76+
$ZipStream = [System.IO.MemoryStream]::new($ZipBytes, $false)
77+
try {
78+
$Archive = [System.IO.Compression.ZipArchive]::new($ZipStream, [System.IO.Compression.ZipArchiveMode]::Read)
79+
try {
80+
$Entry = $Archive.Entries | Where-Object { $_.Name -like '*.json' } | Select-Object -First 1
81+
if (-not $Entry) { throw "No JSON entry in $ReportName archive" }
82+
$EntryStream = $Entry.Open()
83+
try {
84+
$Reader = [System.IO.StreamReader]::new($EntryStream)
85+
try { $JsonText = $Reader.ReadToEnd() } finally { $Reader.Dispose() }
86+
} finally { $EntryStream.Dispose() }
87+
} finally { $Archive.Dispose() }
88+
} finally {
89+
$ZipStream.Dispose()
90+
$ZipBytes = $null
91+
}
92+
93+
$ExportRows = @(($JsonText | ConvertFrom-Json).values)
94+
$JsonText = $null
95+
96+
$AppStatuses = foreach ($Row in $ExportRows) {
97+
if (-not $Row.ApplicationId) { continue }
98+
[pscustomobject]@{
99+
id = $Row.ApplicationId
100+
displayName = $Row.DisplayName
101+
publisher = $Row.Publisher
102+
platform = $Row.AppPlatform ?? $Row.Platform
103+
appVersion = $Row.AppVersion
104+
installedDeviceCount = [int]($Row.InstalledDeviceCount ?? 0)
105+
failedDeviceCount = [int]($Row.FailedDeviceCount ?? 0)
106+
failedUserCount = [int]($Row.FailedUserCount ?? 0)
107+
pendingInstallDeviceCount = [int]($Row.PendingInstallDeviceCount ?? 0)
108+
notInstalledDeviceCount = [int]($Row.NotInstalledDeviceCount ?? 0)
109+
failedDevicePercentage = [double]($Row.FailedDevicePercentage ?? 0)
110+
}
111+
}
112+
$AppStatuses = @($AppStatuses)
113+
114+
Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAppInstallStatusAggregate' -Data $AppStatuses -AddCount
115+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AppStatuses.Count) app install status rows from export $JobId" -sev Info
116+
117+
Remove-AzDataTableEntity @JobsTable -Entity $JobRow -Force -ErrorAction SilentlyContinue
118+
} catch {
119+
$ErrorMessage = Get-CippException -Exception $_
120+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache app install status: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
121+
}
122+
}

Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheIntunePolicies.ps1

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function Set-CIPPDBCacheIntunePolicies {
2828

2929
$PolicyTypes = @(
3030
@{ Type = 'DeviceCompliancePolicies'; Uri = '/deviceManagement/deviceCompliancePolicies?$top=999&$expand=assignments'; FetchDeviceStatuses = $true }
31-
@{ Type = 'DeviceConfigurations'; Uri = '/deviceManagement/deviceConfigurations?$top=999&$expand=assignments' }
31+
@{ Type = 'DeviceConfigurations'; Uri = '/deviceManagement/deviceConfigurations?$top=999&$expand=assignments'; FetchDeviceStatuses = $true }
3232
@{ Type = 'ConfigurationPolicies'; Uri = '/deviceManagement/configurationPolicies?$top=999&$expand=assignments,settings' }
3333
@{ Type = 'GroupPolicyConfigurations'; Uri = '/deviceManagement/groupPolicyConfigurations?$top=999&$expand=assignments' }
3434
@{ Type = 'MobileAppConfigurations'; Uri = '/deviceManagement/mobileAppConfigurations?$top=999&$expand=assignments' }
@@ -107,9 +107,8 @@ function Set-CIPPDBCacheIntunePolicies {
107107
Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies -AddCount
108108
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) $($PolicyType.Type)" -sev Debug
109109

110-
# Fetch device statuses for compliance policies using bulk requests
111110
if ($PolicyType.FetchDeviceStatuses -and ($Policies | Measure-Object).Count -gt 0) {
112-
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching device statuses for $($Policies.Count) compliance policies using bulk request" -sev Debug
111+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching device statuses for $($Policies.Count) $($PolicyType.Type) using bulk request" -sev Debug
113112

114113
$BaseUri = ($PolicyType.Uri -split '\?')[0]
115114
# Build bulk request array

0 commit comments

Comments
 (0)