Skip to content

Commit 3f8f020

Browse files
authored
Merge pull request #28 from KelvinTegelaar/master
[pull] master from KelvinTegelaar:master
2 parents b08483b + 5a0ddb2 commit 3f8f020

26 files changed

Lines changed: 600 additions & 226 deletions

Config/CIPPDBCacheTypes.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@
214214
"friendlyName": "Exchange Quarantine Policy",
215215
"description": "Exchange Online quarantine policy"
216216
},
217+
{
218+
"type": "ExoGlobalQuarantinePolicy",
219+
"friendlyName": "Exchange Global Quarantine Policy",
220+
"description": "Exchange Online tenant-wide Global Quarantine policy (end-user notification settings)"
221+
},
217222
{
218223
"type": "ExoRemoteDomain",
219224
"friendlyName": "Exchange Remote Domain",
@@ -239,6 +244,16 @@
239244
"friendlyName": "Exchange Tenant Allow/Block List",
240245
"description": "Exchange Online tenant allow/block list"
241246
},
247+
{
248+
"type": "ExoInboundConnector",
249+
"friendlyName": "Exchange Inbound Connectors",
250+
"description": "Exchange Online inbound connectors (includes enhanced filtering settings)"
251+
},
252+
{
253+
"type": "ExoProtectionAlert",
254+
"friendlyName": "Exchange Protection Alerts",
255+
"description": "Microsoft 365 protection alert policies (Security & Compliance endpoint)"
256+
},
242257
{
243258
"type": "Mailboxes",
244259
"friendlyName": "Mailboxes",

Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,14 @@ function New-CippAuditLogSearch {
153153
} catch {
154154
$AuditLogError = $null
155155
$AuditLogErrorMessage = [string]$_.Exception.Message
156-
$TrimmedAuditLogErrorMessage = $AuditLogErrorMessage.TrimStart()
157-
if ($TrimmedAuditLogErrorMessage.StartsWith('{') -or $TrimmedAuditLogErrorMessage.StartsWith('[')) {
158-
$AuditLogError = $AuditLogErrorMessage | ConvertFrom-Json -ErrorAction SilentlyContinue
156+
$RawErrorBody = $_.Exception.Data['RawErrorBody']
157+
if ($RawErrorBody) {
158+
$AuditLogError = [string]$RawErrorBody | ConvertFrom-Json -ErrorAction SilentlyContinue
159+
} else {
160+
$TrimmedAuditLogErrorMessage = $AuditLogErrorMessage.TrimStart()
161+
if ($TrimmedAuditLogErrorMessage.StartsWith('{') -or $TrimmedAuditLogErrorMessage.StartsWith('[')) {
162+
$AuditLogError = $AuditLogErrorMessage | ConvertFrom-Json -ErrorAction SilentlyContinue
163+
}
159164
}
160165

161166
if (($null -ne $AuditLogError) -and $AuditLogError.Status -eq 'AuditingDisabledTenant') {
@@ -186,7 +191,12 @@ function New-CippAuditLogSearch {
186191
# Handle HTML error pages (e.g. Azure Front Door 502/504 gateway timeouts)
187192
if ($TrimmedAuditLogErrorMessage -match '<!DOCTYPE|<html' -and $TrimmedAuditLogErrorMessage -match '<title>([^<]+)</title>') {
188193
$HtmlTitle = $Matches[1].Trim()
189-
Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with gateway error for tenant $TenantFilter ($HtmlTitle)" -sev Warning
194+
$GatewayLogData = [PSCustomObject]@{
195+
HtmlTitle = $HtmlTitle
196+
NormalizedMessage = $AuditLogErrorMessage
197+
RawResponseBody = if ($RawErrorBody) { [string]$RawErrorBody } else { $AuditLogErrorMessage }
198+
}
199+
Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with gateway error for tenant $TenantFilter ($HtmlTitle)" -sev Warning -LogData $GatewayLogData
190200
return [PSCustomObject]@{
191201
id = $null
192202
displayName = [string]$DisplayName
@@ -199,7 +209,17 @@ function New-CippAuditLogSearch {
199209
# Handle Microsoft-side timeouts / transient errors (e.g. UnknownError with empty message)
200210
$ErrorCode = $AuditLogError.error.code ?? $AuditLogError.code
201211
if ($ErrorCode -in @('UnknownError', 'ServiceUnavailable', 'RequestTimeout', 'GatewayTimeout', 'TooManyRequests')) {
202-
Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with transient error for tenant $TenantFilter ($ErrorCode)" -sev Warning
212+
$TransientLogData = [PSCustomObject]@{
213+
ErrorCode = $ErrorCode
214+
ErrorMessage = $AuditLogError.error.message ?? $AuditLogError.message
215+
InnerRequestId = $AuditLogError.error.innerError.'request-id' ?? $AuditLogError.error.innererror.'request-id'
216+
InnerClientReqId = $AuditLogError.error.innerError.'client-request-id' ?? $AuditLogError.error.innererror.'client-request-id'
217+
InnerErrorDate = $AuditLogError.error.innerError.date ?? $AuditLogError.error.innererror.date
218+
NormalizedMessage = $AuditLogErrorMessage
219+
RawResponseBody = if ($RawErrorBody) { [string]$RawErrorBody } else { $AuditLogErrorMessage }
220+
ParsedError = $AuditLogError
221+
}
222+
Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed for tenant $TenantFilter - Microsoft returned $ErrorCode" -sev Warning -LogData $TransientLogData
203223
return [PSCustomObject]@{
204224
id = $null
205225
displayName = [string]$DisplayName

Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ function New-ExoBulkRequest {
4444
# Initialize the ID to Cmdlet Name mapping
4545
$IdToCmdletName = @{}
4646
$IdToOperationGuid = @{} # Track operation GUIDs when provided
47+
$IdToBatchRequest = @{} # Original sub-requests, reused for nextLink continuations
4748

4849
# Split the cmdletArray into batches of 10
4950
$batches = [System.Collections.Generic.List[object]]::new()
@@ -93,6 +94,7 @@ function New-ExoBulkRequest {
9394

9495
# Map the Request ID to the Cmdlet Name and Operation GUID (if provided)
9596
$IdToCmdletName[$RequestId] = $cmd.CmdletInput.CmdletName
97+
$IdToBatchRequest[$RequestId] = $BatchRequest
9698
if ($cmd.OperationGuid) {
9799
$IdToOperationGuid[$RequestId] = $cmd.OperationGuid
98100
}
@@ -106,6 +108,49 @@ function New-ExoBulkRequest {
106108

107109
Write-Host "Batch #$($batches.IndexOf($batch) + 1) of $($batches.Count) processed"
108110
}
111+
112+
# Follow @odata.nextLink continuations so results are not capped at one page (mirrors New-GraphBulkRequest).
113+
# The EXO admin API pages by re-POSTing the same CmdletInput body to the nextLink URL.
114+
$IdToResponse = @{}
115+
$NextLinkQueue = [System.Collections.Generic.Queue[object]]::new()
116+
foreach ($Response in $ReturnedData) {
117+
if ($Response.id -and -not $IdToResponse.ContainsKey($Response.id)) {
118+
$IdToResponse[$Response.id] = $Response
119+
}
120+
if ($Response.body.'@odata.nextLink' -and $IdToBatchRequest.ContainsKey($Response.id)) {
121+
$NextLinkQueue.Enqueue(@{ id = $Response.id; url = $Response.body.'@odata.nextLink' })
122+
}
123+
}
124+
125+
while ($NextLinkQueue.Count -gt 0) {
126+
# Drain up to 10 nextLinks into a single $batch, same size as the main loop
127+
$NextBatchRequests = [System.Collections.Generic.List[object]]::new()
128+
while ($NextLinkQueue.Count -gt 0 -and $NextBatchRequests.Count -lt 10) {
129+
$Item = $NextLinkQueue.Dequeue()
130+
$ContinuationRequest = $IdToBatchRequest[$Item.id].Clone()
131+
$ContinuationRequest['url'] = $Item.url
132+
$NextBatchRequests.Add($ContinuationRequest)
133+
}
134+
135+
Write-Host "Fetching next page for $($NextBatchRequests.Count) request(s)"
136+
$NextBatchBodyJson = ConvertTo-Json -InputObject @{ requests = @($NextBatchRequests) } -Depth 10
137+
$NextBatchBodyJson = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $NextBatchBodyJson
138+
$NextResults = Invoke-CIPPRestMethod $BatchURL -Method POST -Body $NextBatchBodyJson -Headers $Headers -ContentType 'application/json; charset=utf-8'
139+
140+
foreach ($NextResponse in $NextResults.responses) {
141+
$OriginalResponse = $IdToResponse[$NextResponse.id]
142+
if (-not $OriginalResponse) { continue }
143+
if ($NextResponse.body.value) {
144+
$MergedValues = [System.Collections.Generic.List[object]]::new()
145+
foreach ($val in @($OriginalResponse.body.value)) { $MergedValues.Add($val) }
146+
foreach ($val in @($NextResponse.body.value)) { $MergedValues.Add($val) }
147+
$OriginalResponse.body.value = $MergedValues
148+
}
149+
if ($NextResponse.body.'@odata.nextLink') {
150+
$NextLinkQueue.Enqueue(@{ id = $NextResponse.id; url = $NextResponse.body.'@odata.nextLink' })
151+
}
152+
}
153+
}
109154
} catch {
110155
# Error handling (omitted for brevity)
111156
}

Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function New-GraphPOSTRequest {
4545

4646
$RetryCount = 0
4747
$RequestSuccessful = $false
48+
$RawErrorBody = $null
4849
do {
4950
try {
5051
Write-Information "$($type.ToUpper()) [ $uri ] | tenant: $tenantid | attempt: $($RetryCount + 1) of $maxRetries"
@@ -53,6 +54,7 @@ function New-GraphPOSTRequest {
5354
} catch {
5455
$ShouldRetry = $false
5556
$WaitTime = 0
57+
$RawErrorBody = $_.ErrorDetails.Message
5658
$Message = if ($_.ErrorDetails.Message) {
5759
Get-NormalizedError -Message $_.ErrorDetails.Message
5860
} else {
@@ -133,6 +135,11 @@ function New-GraphPOSTRequest {
133135
}
134136

135137
if ($RequestSuccessful -eq $false) {
138+
if ($RawErrorBody) {
139+
$GraphException = [System.Exception]::new($Message)
140+
$GraphException.Data['RawErrorBody'] = $RawErrorBody
141+
throw $GraphException
142+
}
136143
throw $Message
137144
}
138145

Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ function Invoke-CIPPDBCacheCollection {
8787
'ExoAdminAuditLogConfig'
8888
'ExoPresetSecurityPolicy'
8989
'ExoTenantAllowBlockList'
90+
'ExoInboundConnector'
91+
'ExoProtectionAlert'
9092
'OwaMailboxPolicy'
9193
'ReportSubmissionPolicy'
9294
'ExoTransportConfig'

Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,21 @@ function Get-CIPPStandards {
2626
# can compute correct precedence. The $TemplateId filter is applied after merge so that
2727
# manual runs of a single template don't bypass tenant-specific overrides.
2828
$Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Sort-Object TimeStamp).JSON |
29-
ForEach-Object {
30-
try {
31-
# Fix old "Action" => "action"
32-
$JSON = $_ -replace '"Action":', '"action":' -replace '"permissionlevel":', '"permissionLevel":'
33-
ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue
34-
} catch {}
35-
} |
36-
Where-Object {
37-
$_.runManually -eq $runManually
29+
ForEach-Object {
30+
try {
31+
# Fix old "Action" => "action"
32+
$JSON = $_ -replace '"Action":', '"action":' -replace '"permissionlevel":', '"permissionLevel":'
33+
ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue
34+
} catch {}
35+
} |
36+
Where-Object {
37+
$_.runManually -eq $runManually
38+
}
39+
40+
if ($TemplateId -ne '*' -and ![string]::IsNullOrEmpty($TemplateId)) {
41+
$Templates = $Templates | Where-Object {
42+
$_.GUID -like $TemplateId
43+
}
3844
}
3945

4046
# 1.5. Expand templates that contain TemplateList-Tags into multiple standards
@@ -50,35 +56,35 @@ function Get-CIPPStandards {
5056

5157
if ($IsArray) {
5258
$NewArray = @(foreach ($Item in $StandardValue) {
53-
if ($Item.'TemplateList-Tags'.value) {
54-
$HasExpansions = $true
55-
$Table = Get-CippTable -tablename 'templates'
56-
$PartitionKey = switch ($StandardName) {
57-
'ConditionalAccessTemplate' { 'CATemplate' }
58-
'IntuneTemplate' { 'IntuneTemplate' }
59-
default { 'IntuneTemplate' }
60-
}
61-
$Filter = "PartitionKey eq '$PartitionKey'"
62-
$TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value
63-
Write-Information "Expanding $StandardName tag '$($Item.'TemplateList-Tags'.value)' from partition '$PartitionKey': found $(@($TemplatesList).Count) templates"
64-
65-
foreach ($TemplateItem in $TemplatesList) {
66-
$TemplateJSON = $TemplateItem.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue
67-
$TemplateLabel = if ($TemplateJSON.displayName) { $TemplateJSON.displayName } else { "$($TemplateItem.RowKey)" }
68-
$NewItem = $Item.PSObject.Copy()
69-
$NewItem.PSObject.Properties.Remove('TemplateList-Tags')
70-
$NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{
71-
label = $TemplateLabel
72-
value = "$($TemplateItem.RowKey)"
73-
}) -Force
74-
$NewItem | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force
75-
$NewItem
59+
if ($Item.'TemplateList-Tags'.value) {
60+
$HasExpansions = $true
61+
$Table = Get-CippTable -tablename 'templates'
62+
$PartitionKey = switch ($StandardName) {
63+
'ConditionalAccessTemplate' { 'CATemplate' }
64+
'IntuneTemplate' { 'IntuneTemplate' }
65+
default { 'IntuneTemplate' }
66+
}
67+
$Filter = "PartitionKey eq '$PartitionKey'"
68+
$TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value
69+
Write-Information "Expanding $StandardName tag '$($Item.'TemplateList-Tags'.value)' from partition '$PartitionKey': found $(@($TemplatesList).Count) templates"
70+
71+
foreach ($TemplateItem in $TemplatesList) {
72+
$TemplateJSON = $TemplateItem.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue
73+
$TemplateLabel = if ($TemplateJSON.displayName) { $TemplateJSON.displayName } else { "$($TemplateItem.RowKey)" }
74+
$NewItem = $Item.PSObject.Copy()
75+
$NewItem.PSObject.Properties.Remove('TemplateList-Tags')
76+
$NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{
77+
label = $TemplateLabel
78+
value = "$($TemplateItem.RowKey)"
79+
}) -Force
80+
$NewItem | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force
81+
$NewItem
82+
}
83+
} else {
84+
$Item | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force
85+
$Item
7686
}
77-
} else {
78-
$Item | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force
79-
$Item
80-
}
81-
})
87+
})
8288
if ($NewArray.Count -gt 0) {
8389
$ExpandedStandards[$StandardName] = $NewArray
8490
}

Modules/CIPPCore/Public/Test-CIPPRerun.ps1

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,28 +62,41 @@ function Test-CIPPRerun {
6262
$NewSettings = $($Settings | ConvertTo-Json -Depth 10 -Compress)
6363
if ($NewSettings.Length -ne $PreviousSettings.Length) {
6464
Write-Host "$($NewSettings.Length) vs $($PreviousSettings.Length) - settings have changed."
65-
$RerunData.EstimatedNextRun = $EstimatedNextRun
66-
$RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)"
65+
$RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force
66+
$RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$CurrentUnixTime" -Force
67+
$RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force
6768
Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force
6869
return $false # Not a rerun because settings have changed.
6970
}
7071
}
72+
# If the task was rescheduled (ScheduledTime changed since last cache write),
73+
# treat it as a new execution rather than a duplicate.
74+
if ($BaseTime -gt 0 -and $RerunData.LastScheduledTime -and [int64]$RerunData.LastScheduledTime -ne $BaseTime) {
75+
Write-Information "Task $API has a new ScheduledTime ($BaseTime vs cached $($RerunData.LastScheduledTime)). Treating as new execution."
76+
$RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force
77+
$RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$BaseTime" -Force
78+
$RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force
79+
Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force
80+
return $false
81+
}
7182
if ($RerunData.EstimatedNextRun -gt $CurrentUnixTime) {
7283
Write-LogMessage -API $API -message "$Type rerun detected for $($API). Prevented from running again." -tenant $TenantFilter -headers $Headers -Sev 'Info'
7384
return $true
7485
} else {
75-
$RerunData.EstimatedNextRun = $EstimatedNextRun
76-
$RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)"
86+
$RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force
87+
$RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$BaseTime" -Force
88+
$RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force
7789
Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force
7890
return $false
7991
}
8092
} else {
8193
$EstimatedNextRun = $CurrentUnixTime + $EstimatedDifference
8294
$NewEntity = @{
83-
PartitionKey = "$TenantFilter"
84-
RowKey = "$($Type)_$($API)"
85-
Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)"
86-
EstimatedNextRun = $EstimatedNextRun
95+
PartitionKey = "$TenantFilter"
96+
RowKey = "$($Type)_$($API)"
97+
Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)"
98+
EstimatedNextRun = $EstimatedNextRun
99+
LastScheduledTime = "$CurrentUnixTime"
87100
}
88101
Add-CIPPAzDataTableEntity @RerunTable -Entity $NewEntity -Force
89102
return $false

0 commit comments

Comments
 (0)