Skip to content

Commit 289ae9a

Browse files
authored
Merge pull request #1009 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents d415208 + 8f1069c commit 289ae9a

12 files changed

Lines changed: 373 additions & 173 deletions

File tree

Config/CIPPTimers.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,16 @@
237237
"TZOffset": true,
238238
"IsSystem": true
239239
},
240+
{
241+
"Id": "5e8a9b4c-2d6f-4a3e-b7c1-9d0e5f3a8b2c",
242+
"Command": "Start-IntuneReportExportOrchestrator",
243+
"Description": "Submit Intune report-export jobs ahead of nightly DB cache run",
244+
"Cron": "0 0 2 * * *",
245+
"Priority": 22,
246+
"RunOnProcessor": true,
247+
"TZOffset": true,
248+
"IsSystem": true
249+
},
240250
{
241251
"Id": "9a7f8e6d-5c4b-3a2d-1e0f-9b8c7d6e5f4a",
242252
"Command": "Start-CIPPDBCacheOrchestrator",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ function Push-ExecScheduledCommand {
179179
return
180180
}
181181

182-
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) {
182+
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
183183
$State = 'Failed'
184184
Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$($Item.Command)" -Sev 'Warning'
185185
$Results = "Task blocked: The command '$($Item.Command)' is not permitted to run as a scheduled task."
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
function Push-IntuneReportExportSubmit {
2+
<#
3+
.SYNOPSIS
4+
Submits an Intune report export job for a tenant and stores the job id.
5+
.FUNCTIONALITY
6+
Entrypoint
7+
#>
8+
[CmdletBinding()]
9+
param($Item)
10+
11+
$TenantFilter = $Item.TenantFilter
12+
$ReportName = $Item.ReportName
13+
14+
if (-not $TenantFilter -or -not $ReportName) {
15+
Write-LogMessage -API 'IntuneReportExport' -message 'Missing TenantFilter or ReportName on activity item' -sev Error
16+
return @{ Status = 'Failed'; Reason = 'MissingInput' }
17+
}
18+
19+
try {
20+
$Select = switch ($ReportName) {
21+
'AppInvRawData' {
22+
@(
23+
'ApplicationKey', 'ApplicationName', 'ApplicationPublisher', 'ApplicationVersion',
24+
'DeviceId', 'DeviceName', 'OSDescription', 'OSVersion', 'Platform',
25+
'UserId', 'UserName', 'EmailAddress'
26+
)
27+
}
28+
default { throw "Unknown Intune report '$ReportName'" }
29+
}
30+
31+
$Body = @{
32+
reportName = $ReportName
33+
format = 'json'
34+
localizationType = 'replaceLocalizableValues'
35+
select = $Select
36+
} | ConvertTo-Json -Depth 5
37+
38+
$Job = New-GraphPOSTRequest `
39+
-uri 'https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs' `
40+
-tenantid $TenantFilter `
41+
-body $Body
42+
43+
if (-not $Job.id) { throw "Intune returned no job id for $ReportName" }
44+
45+
$JobsTable = Get-CIPPTable -tablename 'IntuneReportJobs'
46+
$Existing = Get-CIPPAzDataTableEntity @JobsTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$ReportName'"
47+
if ($Existing) {
48+
Remove-AzDataTableEntity @JobsTable -Entity $Existing -Force -ErrorAction SilentlyContinue
49+
}
50+
51+
Add-CIPPAzDataTableEntity @JobsTable -Entity @{
52+
PartitionKey = $TenantFilter
53+
RowKey = $ReportName
54+
JobId = $Job.id
55+
ReportName = $ReportName
56+
SubmittedAt = ([DateTime]::UtcNow).ToString('o')
57+
} -Force
58+
59+
Write-LogMessage -API 'IntuneReportExport' -tenant $TenantFilter -message "Submitted $ReportName export job $($Job.id)" -sev Info
60+
return @{ Status = 'Submitted'; JobId = $Job.id; ReportName = $ReportName; TenantFilter = $TenantFilter }
61+
} catch {
62+
$ErrorMessage = Get-CippException -Exception $_
63+
Write-LogMessage -API 'IntuneReportExport' -tenant $TenantFilter -message "Failed to submit $ReportName export: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
64+
return @{ Status = 'Failed'; ReportName = $ReportName; TenantFilter = $TenantFilter; Error = $ErrorMessage.NormalizedError }
65+
}
66+
}

Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function Add-CIPPScheduledTask {
7070
$ImportedModules = [System.Collections.Generic.List[string]]::new()
7171
if (-not $Command) {
7272
try {
73-
foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB')) {
73+
foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
7474
if (-not (Get-Module -Name $SiblingModule)) {
7575
Import-Module $SiblingModule -ErrorAction SilentlyContinue
7676
if (Get-Module -Name $SiblingModule) {
@@ -91,7 +91,7 @@ function Add-CIPPScheduledTask {
9191
return "Error - The command '$RequestedCommand' does not exist and cannot be scheduled."
9292
}
9393

94-
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) {
94+
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
9595
Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$RequestedCommand" -Sev 'Warning'
9696
return "Error - The command '$RequestedCommand' is not permitted to run as a scheduled task."
9797
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
function Start-IntuneReportExportOrchestrator {
2+
<#
3+
.SYNOPSIS
4+
Submits Intune report-export jobs at 02:00 UTC ahead of the 03:00 cache run.
5+
.FUNCTIONALITY
6+
Entrypoint
7+
#>
8+
[CmdletBinding()]
9+
param()
10+
11+
try {
12+
Write-LogMessage -API 'IntuneReportExport' -message 'Starting Intune report export submission' -sev Info
13+
14+
$TenantList = Get-Tenants | Where-Object { $_.defaultDomainName -ne $null }
15+
if ($TenantList.Count -eq 0) {
16+
return
17+
}
18+
19+
$LicensedTenants = @(foreach ($Tenant in $TenantList) {
20+
try {
21+
if (Test-CIPPStandardLicense -StandardName 'IntuneReportExportSubmission' -TenantFilter $Tenant.defaultDomainName -Preset Intune -SkipLog) {
22+
$Tenant
23+
}
24+
} catch {
25+
$ErrorMessage = Get-CippException -Exception $_
26+
Write-LogMessage -API 'IntuneReportExport' -tenant $Tenant.defaultDomainName -message "Intune license check failed: $($ErrorMessage.NormalizedError)" -sev Warning -LogData $ErrorMessage
27+
}
28+
})
29+
30+
if ($LicensedTenants.Count -eq 0) {
31+
return
32+
}
33+
34+
$Queue = New-CippQueueEntry -Name 'Intune Report Export Submission' -TotalTasks $LicensedTenants.Count
35+
36+
$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)"
43+
}
44+
}
45+
46+
Start-CIPPOrchestrator -InputObject ([PSCustomObject]@{
47+
Batch = @($Batch)
48+
OrchestratorName = 'IntuneReportExportOrchestrator'
49+
SkipLog = $false
50+
})
51+
52+
} catch {
53+
$ErrorMessage = Get-CippException -Exception $_
54+
Write-LogMessage -API 'IntuneReportExport' -message "Failed to start orchestration: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
55+
throw
56+
}
57+
}
Lines changed: 91 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
function 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

Comments
 (0)