Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Config/CIPPDBCacheTypes.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@
"friendlyName": "Exchange Quarantine Policy",
"description": "Exchange Online quarantine policy"
},
{
"type": "ExoGlobalQuarantinePolicy",
"friendlyName": "Exchange Global Quarantine Policy",
"description": "Exchange Online tenant-wide Global Quarantine policy (end-user notification settings)"
},
{
"type": "ExoRemoteDomain",
"friendlyName": "Exchange Remote Domain",
Expand All @@ -239,6 +244,16 @@
"friendlyName": "Exchange Tenant Allow/Block List",
"description": "Exchange Online tenant allow/block list"
},
{
"type": "ExoInboundConnector",
"friendlyName": "Exchange Inbound Connectors",
"description": "Exchange Online inbound connectors (includes enhanced filtering settings)"
},
{
"type": "ExoProtectionAlert",
"friendlyName": "Exchange Protection Alerts",
"description": "Microsoft 365 protection alert policies (Security & Compliance endpoint)"
},
{
"type": "Mailboxes",
"friendlyName": "Mailboxes",
Expand Down
30 changes: 25 additions & 5 deletions Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,14 @@ function New-CippAuditLogSearch {
} catch {
$AuditLogError = $null
$AuditLogErrorMessage = [string]$_.Exception.Message
$TrimmedAuditLogErrorMessage = $AuditLogErrorMessage.TrimStart()
if ($TrimmedAuditLogErrorMessage.StartsWith('{') -or $TrimmedAuditLogErrorMessage.StartsWith('[')) {
$AuditLogError = $AuditLogErrorMessage | ConvertFrom-Json -ErrorAction SilentlyContinue
$RawErrorBody = $_.Exception.Data['RawErrorBody']
if ($RawErrorBody) {
$AuditLogError = [string]$RawErrorBody | ConvertFrom-Json -ErrorAction SilentlyContinue
} else {
$TrimmedAuditLogErrorMessage = $AuditLogErrorMessage.TrimStart()
if ($TrimmedAuditLogErrorMessage.StartsWith('{') -or $TrimmedAuditLogErrorMessage.StartsWith('[')) {
$AuditLogError = $AuditLogErrorMessage | ConvertFrom-Json -ErrorAction SilentlyContinue
}
}

if (($null -ne $AuditLogError) -and $AuditLogError.Status -eq 'AuditingDisabledTenant') {
Expand Down Expand Up @@ -186,7 +191,12 @@ function New-CippAuditLogSearch {
# Handle HTML error pages (e.g. Azure Front Door 502/504 gateway timeouts)
if ($TrimmedAuditLogErrorMessage -match '<!DOCTYPE|<html' -and $TrimmedAuditLogErrorMessage -match '<title>([^<]+)</title>') {
$HtmlTitle = $Matches[1].Trim()
Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with gateway error for tenant $TenantFilter ($HtmlTitle)" -sev Warning
$GatewayLogData = [PSCustomObject]@{
HtmlTitle = $HtmlTitle
NormalizedMessage = $AuditLogErrorMessage
RawResponseBody = if ($RawErrorBody) { [string]$RawErrorBody } else { $AuditLogErrorMessage }
}
Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with gateway error for tenant $TenantFilter ($HtmlTitle)" -sev Warning -LogData $GatewayLogData
return [PSCustomObject]@{
id = $null
displayName = [string]$DisplayName
Expand All @@ -199,7 +209,17 @@ function New-CippAuditLogSearch {
# Handle Microsoft-side timeouts / transient errors (e.g. UnknownError with empty message)
$ErrorCode = $AuditLogError.error.code ?? $AuditLogError.code
if ($ErrorCode -in @('UnknownError', 'ServiceUnavailable', 'RequestTimeout', 'GatewayTimeout', 'TooManyRequests')) {
Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with transient error for tenant $TenantFilter ($ErrorCode)" -sev Warning
$TransientLogData = [PSCustomObject]@{
ErrorCode = $ErrorCode
ErrorMessage = $AuditLogError.error.message ?? $AuditLogError.message
InnerRequestId = $AuditLogError.error.innerError.'request-id' ?? $AuditLogError.error.innererror.'request-id'
InnerClientReqId = $AuditLogError.error.innerError.'client-request-id' ?? $AuditLogError.error.innererror.'client-request-id'
InnerErrorDate = $AuditLogError.error.innerError.date ?? $AuditLogError.error.innererror.date
NormalizedMessage = $AuditLogErrorMessage
RawResponseBody = if ($RawErrorBody) { [string]$RawErrorBody } else { $AuditLogErrorMessage }
ParsedError = $AuditLogError
}
Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed for tenant $TenantFilter - Microsoft returned $ErrorCode" -sev Warning -LogData $TransientLogData
return [PSCustomObject]@{
id = $null
displayName = [string]$DisplayName
Expand Down
45 changes: 45 additions & 0 deletions Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function New-ExoBulkRequest {
# Initialize the ID to Cmdlet Name mapping
$IdToCmdletName = @{}
$IdToOperationGuid = @{} # Track operation GUIDs when provided
$IdToBatchRequest = @{} # Original sub-requests, reused for nextLink continuations

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

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

Write-Host "Batch #$($batches.IndexOf($batch) + 1) of $($batches.Count) processed"
}

# Follow @odata.nextLink continuations so results are not capped at one page (mirrors New-GraphBulkRequest).
# The EXO admin API pages by re-POSTing the same CmdletInput body to the nextLink URL.
$IdToResponse = @{}
$NextLinkQueue = [System.Collections.Generic.Queue[object]]::new()
foreach ($Response in $ReturnedData) {
if ($Response.id -and -not $IdToResponse.ContainsKey($Response.id)) {
$IdToResponse[$Response.id] = $Response
}
if ($Response.body.'@odata.nextLink' -and $IdToBatchRequest.ContainsKey($Response.id)) {
$NextLinkQueue.Enqueue(@{ id = $Response.id; url = $Response.body.'@odata.nextLink' })
}
}

while ($NextLinkQueue.Count -gt 0) {
# Drain up to 10 nextLinks into a single $batch, same size as the main loop
$NextBatchRequests = [System.Collections.Generic.List[object]]::new()
while ($NextLinkQueue.Count -gt 0 -and $NextBatchRequests.Count -lt 10) {
$Item = $NextLinkQueue.Dequeue()
$ContinuationRequest = $IdToBatchRequest[$Item.id].Clone()
$ContinuationRequest['url'] = $Item.url
$NextBatchRequests.Add($ContinuationRequest)
}

Write-Host "Fetching next page for $($NextBatchRequests.Count) request(s)"
$NextBatchBodyJson = ConvertTo-Json -InputObject @{ requests = @($NextBatchRequests) } -Depth 10
$NextBatchBodyJson = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $NextBatchBodyJson
$NextResults = Invoke-CIPPRestMethod $BatchURL -Method POST -Body $NextBatchBodyJson -Headers $Headers -ContentType 'application/json; charset=utf-8'

foreach ($NextResponse in $NextResults.responses) {
$OriginalResponse = $IdToResponse[$NextResponse.id]
if (-not $OriginalResponse) { continue }
if ($NextResponse.body.value) {
$MergedValues = [System.Collections.Generic.List[object]]::new()
foreach ($val in @($OriginalResponse.body.value)) { $MergedValues.Add($val) }
foreach ($val in @($NextResponse.body.value)) { $MergedValues.Add($val) }
$OriginalResponse.body.value = $MergedValues
}
if ($NextResponse.body.'@odata.nextLink') {
$NextLinkQueue.Enqueue(@{ id = $NextResponse.id; url = $NextResponse.body.'@odata.nextLink' })
}
}
}
} catch {
# Error handling (omitted for brevity)
}
Expand Down
7 changes: 7 additions & 0 deletions Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function New-GraphPOSTRequest {

$RetryCount = 0
$RequestSuccessful = $false
$RawErrorBody = $null
do {
try {
Write-Information "$($type.ToUpper()) [ $uri ] | tenant: $tenantid | attempt: $($RetryCount + 1) of $maxRetries"
Expand All @@ -53,6 +54,7 @@ function New-GraphPOSTRequest {
} catch {
$ShouldRetry = $false
$WaitTime = 0
$RawErrorBody = $_.ErrorDetails.Message
$Message = if ($_.ErrorDetails.Message) {
Get-NormalizedError -Message $_.ErrorDetails.Message
} else {
Expand Down Expand Up @@ -133,6 +135,11 @@ function New-GraphPOSTRequest {
}

if ($RequestSuccessful -eq $false) {
if ($RawErrorBody) {
$GraphException = [System.Exception]::new($Message)
$GraphException.Data['RawErrorBody'] = $RawErrorBody
throw $GraphException
}
throw $Message
}

Expand Down
2 changes: 2 additions & 0 deletions Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ function Invoke-CIPPDBCacheCollection {
'ExoAdminAuditLogConfig'
'ExoPresetSecurityPolicy'
'ExoTenantAllowBlockList'
'ExoInboundConnector'
'ExoProtectionAlert'
'OwaMailboxPolicy'
'ReportSubmissionPolicy'
'ExoTransportConfig'
Expand Down
80 changes: 43 additions & 37 deletions Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,21 @@ function Get-CIPPStandards {
# can compute correct precedence. The $TemplateId filter is applied after merge so that
# manual runs of a single template don't bypass tenant-specific overrides.
$Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Sort-Object TimeStamp).JSON |
ForEach-Object {
try {
# Fix old "Action" => "action"
$JSON = $_ -replace '"Action":', '"action":' -replace '"permissionlevel":', '"permissionLevel":'
ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue
} catch {}
} |
Where-Object {
$_.runManually -eq $runManually
ForEach-Object {
try {
# Fix old "Action" => "action"
$JSON = $_ -replace '"Action":', '"action":' -replace '"permissionlevel":', '"permissionLevel":'
ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue
} catch {}
} |
Where-Object {
$_.runManually -eq $runManually
}

if ($TemplateId -ne '*' -and ![string]::IsNullOrEmpty($TemplateId)) {
$Templates = $Templates | Where-Object {
$_.GUID -like $TemplateId
}
}

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

if ($IsArray) {
$NewArray = @(foreach ($Item in $StandardValue) {
if ($Item.'TemplateList-Tags'.value) {
$HasExpansions = $true
$Table = Get-CippTable -tablename 'templates'
$PartitionKey = switch ($StandardName) {
'ConditionalAccessTemplate' { 'CATemplate' }
'IntuneTemplate' { 'IntuneTemplate' }
default { 'IntuneTemplate' }
}
$Filter = "PartitionKey eq '$PartitionKey'"
$TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value
Write-Information "Expanding $StandardName tag '$($Item.'TemplateList-Tags'.value)' from partition '$PartitionKey': found $(@($TemplatesList).Count) templates"

foreach ($TemplateItem in $TemplatesList) {
$TemplateJSON = $TemplateItem.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue
$TemplateLabel = if ($TemplateJSON.displayName) { $TemplateJSON.displayName } else { "$($TemplateItem.RowKey)" }
$NewItem = $Item.PSObject.Copy()
$NewItem.PSObject.Properties.Remove('TemplateList-Tags')
$NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{
label = $TemplateLabel
value = "$($TemplateItem.RowKey)"
}) -Force
$NewItem | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force
$NewItem
if ($Item.'TemplateList-Tags'.value) {
$HasExpansions = $true
$Table = Get-CippTable -tablename 'templates'
$PartitionKey = switch ($StandardName) {
'ConditionalAccessTemplate' { 'CATemplate' }
'IntuneTemplate' { 'IntuneTemplate' }
default { 'IntuneTemplate' }
}
$Filter = "PartitionKey eq '$PartitionKey'"
$TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value
Write-Information "Expanding $StandardName tag '$($Item.'TemplateList-Tags'.value)' from partition '$PartitionKey': found $(@($TemplatesList).Count) templates"

foreach ($TemplateItem in $TemplatesList) {
$TemplateJSON = $TemplateItem.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue
$TemplateLabel = if ($TemplateJSON.displayName) { $TemplateJSON.displayName } else { "$($TemplateItem.RowKey)" }
$NewItem = $Item.PSObject.Copy()
$NewItem.PSObject.Properties.Remove('TemplateList-Tags')
$NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{
label = $TemplateLabel
value = "$($TemplateItem.RowKey)"
}) -Force
$NewItem | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force
$NewItem
}
} else {
$Item | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force
$Item
}
} else {
$Item | Add-Member -NotePropertyName TemplateId -NotePropertyValue $Template.GUID -Force
$Item
}
})
})
if ($NewArray.Count -gt 0) {
$ExpandedStandards[$StandardName] = $NewArray
}
Expand Down
29 changes: 21 additions & 8 deletions Modules/CIPPCore/Public/Test-CIPPRerun.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -62,28 +62,41 @@ function Test-CIPPRerun {
$NewSettings = $($Settings | ConvertTo-Json -Depth 10 -Compress)
if ($NewSettings.Length -ne $PreviousSettings.Length) {
Write-Host "$($NewSettings.Length) vs $($PreviousSettings.Length) - settings have changed."
$RerunData.EstimatedNextRun = $EstimatedNextRun
$RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)"
$RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force
$RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$CurrentUnixTime" -Force
$RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force
Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force
return $false # Not a rerun because settings have changed.
}
}
# If the task was rescheduled (ScheduledTime changed since last cache write),
# treat it as a new execution rather than a duplicate.
if ($BaseTime -gt 0 -and $RerunData.LastScheduledTime -and [int64]$RerunData.LastScheduledTime -ne $BaseTime) {
Write-Information "Task $API has a new ScheduledTime ($BaseTime vs cached $($RerunData.LastScheduledTime)). Treating as new execution."
$RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force
$RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$BaseTime" -Force
$RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force
Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force
return $false
}
if ($RerunData.EstimatedNextRun -gt $CurrentUnixTime) {
Write-LogMessage -API $API -message "$Type rerun detected for $($API). Prevented from running again." -tenant $TenantFilter -headers $Headers -Sev 'Info'
return $true
} else {
$RerunData.EstimatedNextRun = $EstimatedNextRun
$RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)"
$RerunData | Add-Member -MemberType NoteProperty -Name 'EstimatedNextRun' -Value $EstimatedNextRun -Force
$RerunData | Add-Member -MemberType NoteProperty -Name 'LastScheduledTime' -Value "$BaseTime" -Force
$RerunData | Add-Member -MemberType NoteProperty -Name 'Settings' -Value "$($Settings | ConvertTo-Json -Depth 10 -Compress)" -Force
Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force
return $false
}
} else {
$EstimatedNextRun = $CurrentUnixTime + $EstimatedDifference
$NewEntity = @{
PartitionKey = "$TenantFilter"
RowKey = "$($Type)_$($API)"
Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)"
EstimatedNextRun = $EstimatedNextRun
PartitionKey = "$TenantFilter"
RowKey = "$($Type)_$($API)"
Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)"
EstimatedNextRun = $EstimatedNextRun
LastScheduledTime = "$CurrentUnixTime"
}
Add-CIPPAzDataTableEntity @RerunTable -Entity $NewEntity -Force
return $false
Expand Down
Loading