From 64836c02a801a3718c2bd2e598bcf206c973541d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 9 Jun 2026 12:15:57 -0400 Subject: [PATCH 01/20] fix: rerun detection on scheduled tasks --- Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index 6c4dece1f8ba..f59727017896 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -63,16 +63,28 @@ function Test-CIPPRerun { if ($NewSettings.Length -ne $PreviousSettings.Length) { Write-Host "$($NewSettings.Length) vs $($PreviousSettings.Length) - settings have changed." $RerunData.EstimatedNextRun = $EstimatedNextRun + $RerunData.LastScheduledTime = "$CurrentUnixTime" $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" 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.EstimatedNextRun = $EstimatedNextRun + $RerunData.LastScheduledTime = "$BaseTime" + $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" + 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.LastScheduledTime = "$BaseTime" $RerunData.Settings = "$($Settings | ConvertTo-Json -Depth 10 -Compress)" Add-CIPPAzDataTableEntity @RerunTable -Entity $RerunData -Force return $false @@ -80,10 +92,11 @@ function Test-CIPPRerun { } 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 From 9dd0e56750810b7a85ef4fb079dbf6725b2b6ce0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:27:26 +0800 Subject: [PATCH 02/20] Update Invoke-ListTenantAlignment.ps1 --- .../Standards/Invoke-ListTenantAlignment.ps1 | 94 +++++++++++-------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 index b123bddf07c2..8a6b86259fcf 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 @@ -12,9 +12,14 @@ function Invoke-ListTenantAlignment { $APIName = $Request.Params.CIPPEndpoint $Granular = $Request.Query.granular -eq 'true' + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter try { + $AlignmentParams = @{} + if ($TenantFilter -and $TenantFilter -ne 'AllTenants') { + $AlignmentParams.TenantFilter = $TenantFilter + } # Use the new Get-CIPPTenantAlignment function to get alignment data - $AlignmentData = Get-CIPPTenantAlignment + $AlignmentData = Get-CIPPTenantAlignment @AlignmentParams # Build a GUID -> displayName lookup from the templates table for all template types $TemplateLookup = @{} @@ -23,12 +28,13 @@ function Invoke-ListTenantAlignment { $TemplatePartitions = @('IntuneTemplate', 'ConditionalAccessTemplate', 'QuarantineTemplate') foreach ($Partition in $TemplatePartitions) { Get-CIPPAzDataTableEntity @TemplateTable -Filter "PartitionKey eq '$Partition'" | ForEach-Object { + $TemplateRow = $_ try { - $Parsed = $_.JSON | ConvertFrom-Json -ErrorAction Stop - $DisplayName = $Parsed.displayName ?? $Parsed.Displayname ?? $Parsed.DisplayName ?? $Parsed.name ?? $_.RowKey - $TemplateLookup[$_.RowKey] = $DisplayName + $Parsed = $TemplateRow.JSON | ConvertFrom-Json -ErrorAction Stop + $DisplayName = $Parsed.displayName ?? $Parsed.Displayname ?? $Parsed.DisplayName ?? $Parsed.name ?? $TemplateRow.RowKey + $TemplateLookup[$TemplateRow.RowKey] = $DisplayName } catch { - $TemplateLookup[$_.RowKey] = $_.RowKey + $TemplateLookup[$TemplateRow.RowKey] = $TemplateRow.RowKey } } } @@ -42,44 +48,51 @@ function Invoke-ListTenantAlignment { $TemplateId = $Row.StandardId $StandardType = $Row.standardType ? $Row.standardType : 'Classic Standard' $Row.ComparisonDetails | ForEach-Object { - $StandardId = $_.StandardName - $FriendlyType = $StandardType - $ResolvedName = if ($StandardId -match '^standards\.(\w+Template)\.(.+)$') { - $LookupKey = if ($Matches[1] -eq 'QuarantineTemplate') { - $KeyBytes = [byte[]]::new($Matches[2].Length / 2) - for ($i = 0; $i -lt $KeyBytes.Length; $i++) { - $KeyBytes[$i] = [Convert]::ToByte($Matches[2].Substring($i * 2, 2), 16) + $Detail = $_ + try { + $StandardId = $Detail.StandardName + $FriendlyType = $StandardType + $ResolvedName = if ($StandardId -and $StandardId -match '^standards\.(\w+Template)\.(.+)$') { + $MatchType = $Matches[1] + $MatchValue = $Matches[2] + $LookupKey = if ($MatchType -eq 'QuarantineTemplate') { + $KeyBytes = [byte[]]::new($MatchValue.Length / 2) + for ($i = 0; $i -lt $KeyBytes.Length; $i++) { + $KeyBytes[$i] = [Convert]::ToByte($MatchValue.Substring($i * 2, 2), 16) + } + [System.Text.Encoding]::UTF8.GetString($KeyBytes) + } else { + $MatchValue + } + $PolicyName = $TemplateLookup[$LookupKey] ?? $LookupKey + $FriendlyType = switch ($MatchType) { + 'IntuneTemplate' { 'Intune Template' } + 'ConditionalAccessTemplate' { 'Conditional Access Template' } + 'QuarantineTemplate' { 'Quarantine Template' } + default { $MatchType } } - [System.Text.Encoding]::UTF8.GetString($KeyBytes) + "$FriendlyType - $PolicyName" } else { - $Matches[2] + $StandardId } - $PolicyName = $TemplateLookup[$LookupKey] ?? $LookupKey - $FriendlyType = switch ($Matches[1]) { - 'IntuneTemplate' { 'Intune Template' } - 'ConditionalAccessTemplate' { 'Conditional Access Template' } - 'QuarantineTemplate' { 'Quarantine Template' } - default { $Matches[1] } + [PSCustomObject]@{ + tenantFilter = $Row.TenantFilter + templateName = $TemplateName + templateId = $TemplateId + templateType = $Row.standardType + standardType = $FriendlyType + standardId = $StandardId + standardName = $ResolvedName + complianceStatus = $Detail.ComplianceStatus + compliant = $Detail.Compliant + deviationStatus = $Detail.DeviationStatus + licenseAvailable = $Detail.LicenseAvailable + currentValue = $Detail.CurrentValue + expectedValue = $Detail.ExpectedValue + latestDataCollection = $Row.LatestDataCollection } - "$FriendlyType - $PolicyName" - } else { - $StandardId - } - [PSCustomObject]@{ - tenantFilter = $Row.TenantFilter - templateName = $TemplateName - templateId = $TemplateId - templateType = $Row.standardType - standardType = $FriendlyType - standardId = $StandardId - standardName = $ResolvedName - complianceStatus = $_.ComplianceStatus - compliant = $_.Compliant - deviationStatus = $_.DeviationStatus - licenseAvailable = $_.LicenseAvailable - currentValue = $_.CurrentValue - expectedValue = $_.ExpectedValue - latestDataCollection = $Row.LatestDataCollection + } catch { + Write-LogMessage -API $APIName -tenant $Row.TenantFilter -message "Failed to flatten alignment row for $($Row.TenantFilter)/$($Detail.StandardName): $($_.Exception.Message)" -sev Warning } } } @@ -106,7 +119,8 @@ function Invoke-ListTenantAlignment { Body = @($Results) }) } catch { - Write-LogMessage -API $APIName -message "Failed to get tenant alignment data: $($_.Exception.Message)" -sev Error + $ErrorDetail = "$($_.Exception.Message) at $($_.InvocationInfo.PositionMessage -replace '\r?\n', ' ')" + Write-LogMessage -API $APIName -message "Failed to get tenant alignment data: $ErrorDetail" -sev Error -LogData (Get-CippException -Exception $_) return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::InternalServerError Body = @{ error = "Failed to get tenant alignment data: $($_.Exception.Message)" } From a0a79852f89fb82c0f5bab15822b46870198956e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:45:31 +0800 Subject: [PATCH 03/20] Update Invoke-ListTenantAlignment.ps1 --- .../Tenant/Standards/Invoke-ListTenantAlignment.ps1 | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 index 8a6b86259fcf..aae480c97d51 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListTenantAlignment.ps1 @@ -12,14 +12,9 @@ function Invoke-ListTenantAlignment { $APIName = $Request.Params.CIPPEndpoint $Granular = $Request.Query.granular -eq 'true' - $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter try { - $AlignmentParams = @{} - if ($TenantFilter -and $TenantFilter -ne 'AllTenants') { - $AlignmentParams.TenantFilter = $TenantFilter - } # Use the new Get-CIPPTenantAlignment function to get alignment data - $AlignmentData = Get-CIPPTenantAlignment @AlignmentParams + $AlignmentData = Get-CIPPTenantAlignment # Build a GUID -> displayName lookup from the templates table for all template types $TemplateLookup = @{} From e8d1342774427bee3572ebcb9f2aad8c0d30c800 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:03:18 +0800 Subject: [PATCH 04/20] audit log detailed logging for debugging --- .../AuditLogs/New-CippAuditLogSearch.ps1 | 30 +++++++++++++++---- .../GraphHelper/New-GraphPOSTRequest.ps1 | 7 +++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 index e135d29d6d74..fac6880e178b 100644 --- a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 @@ -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') { @@ -186,7 +191,12 @@ function New-CippAuditLogSearch { # Handle HTML error pages (e.g. Azure Front Door 502/504 gateway timeouts) if ($TrimmedAuditLogErrorMessage -match '([^<]+)') { $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 @@ -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 diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index 5624e05108a3..4bdcf468ad9c 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -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" @@ -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 { @@ -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 } From b8fefcfdd085b5f25782f7f3da70324252f3c630 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:20:53 +0800 Subject: [PATCH 05/20] Update Invoke-ExecMcp.ps1 --- .../Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 index f2d9b427868a..f889b0a52090 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/MCP/Invoke-ExecMcp.ps1 @@ -11,7 +11,7 @@ function Invoke-ExecMcp { is enforced for each tool exactly as it would be for a normal API request. This endpoint's own role (CIPP.Core.Read) is only the floor required to use MCP at all. .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE CIPP.Core.Read #> From 83b130347aa81fb373cb77de32583dab9628e2e8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:33:53 +0800 Subject: [PATCH 06/20] bulk request next link following --- .../Public/GraphHelper/New-ExoBulkRequest.ps1 | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 index 78a082a329c2..2fdf0196a560 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-ExoBulkRequest.ps1 @@ -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() @@ -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 } @@ -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) } From 9f0bacd62c74d6e65ee0f51623884569de2eb8c1 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:49:50 +0800 Subject: [PATCH 07/20] manual pagination support for Invoke-ListMailQuarantine --- .../Spamfilter/Invoke-ListMailQuarantine.ps1 | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 index bf523175d64b..6e608766541d 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 @@ -14,15 +14,27 @@ function Invoke-ListMailQuarantine { try { $GraphRequest = if ($TenantFilter -ne 'AllTenants') { - $Page = 1 $PageSize = 1000 - $AllMessages = [System.Collections.Generic.List[object]]::new() - do { + if ($Request.Query.manualPagination -and [System.Convert]::ToBoolean($Request.Query.manualPagination)) { + # Manual pagination: return one page per request. The frontend chains requests via + # Metadata.nextLink, which for this endpoint is the next Get-QuarantineMessage page number. + $Page = if ($Request.Query.nextLink -match '^\d+$') { [int]$Request.Query.nextLink } else { 1 } $Results = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = $PageSize; Page = $Page } | Select-Object -ExcludeProperty *data.type* - if ($Results) { $AllMessages.AddRange(@($Results)) } - $Page++ - } while (@($Results).Count -eq $PageSize) - $AllMessages + # Get-QuarantineMessage supports a maximum Page of 1000 + if (@($Results).Count -eq $PageSize -and $Page -lt 1000) { + $Metadata = [PSCustomObject]@{ nextLink = [string]($Page + 1) } + } + $Results + } else { + $Page = 1 + $AllMessages = [System.Collections.Generic.List[object]]::new() + do { + $Results = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = $PageSize; Page = $Page } | Select-Object -ExcludeProperty *data.type* + if ($Results) { $AllMessages.AddRange(@($Results)) } + $Page++ + } while (@($Results).Count -eq $PageSize) + $AllMessages + } } else { $Table = Get-CIPPTable -TableName cacheQuarantineMessages $PartitionKey = 'QuarantineMessage' From cc84a49df1633a1b5de2dfdeda991f9d8b7b660c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:22:58 +0800 Subject: [PATCH 08/20] Fix ORCA104 --- .../ORCA/Identity/Invoke-CippTestORCA104.ps1 | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 index 2382b39b8301..0d320414f579 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 @@ -6,27 +6,27 @@ function Invoke-CippTestORCA104 { param($Tenant) try { - $AntiPhishPolicies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' - if (-not $AntiPhishPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' return } $FailedPolicies = [System.Collections.Generic.List[object]]::new() $PassedPolicies = [System.Collections.Generic.List[object]]::new() - foreach ($Policy in $AntiPhishPolicies) { + foreach ($Policy in $Policies) { if ($Policy.HighConfidencePhishAction -eq 'Quarantine') { - $PassedPolicies.Add($Policy) + $PassedPolicies.Add($Policy) | Out-Null } else { - $FailedPolicies.Add($Policy) + $FailedPolicies.Add($Policy) | Out-Null } } if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All anti-phishing policies have High Confidence Phish action set to Quarantine.`n`n") + $Result = [System.Text.StringBuilder]::new("All anti-spam policies have High Confidence Phish action set to Quarantine.`n`n") $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") if ($PassedPolicies.Count -gt 0) { $null = $Result.Append("| Policy Name | Action |`n") @@ -37,7 +37,7 @@ function Invoke-CippTestORCA104 { } } else { $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("Some anti-phishing policies do not have High Confidence Phish action set to Quarantine.`n`n") + $Result = [System.Text.StringBuilder]::new("Some anti-spam policies do not have High Confidence Phish action set to Quarantine.`n`n") $null = $Result.Append("**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n") $null = $Result.Append("### Non-Compliant Policies`n`n") $null = $Result.Append("| Policy Name | Current Action | Recommended Action |`n") @@ -48,10 +48,10 @@ function Invoke-CippTestORCA104 { $null = $Result.Append("`n**Remediation:** Update the HighConfidencePhishAction to 'Quarantine' for enhanced security.") } - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' } } From 8cd8d1d94e51ce793d79567822483073715392ad Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:51:56 +0800 Subject: [PATCH 09/20] Fix for ORCA107 and add Exchange Global Quarantine policy to cache --- Config/CIPPDBCacheTypes.json | 5 ++ .../Set-CIPPDBCacheExoQuarantinePolicy.ps1 | 14 ++++++ .../ORCA/Identity/Invoke-CippTestORCA107.ps1 | 46 ++++++++++++------- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/Config/CIPPDBCacheTypes.json b/Config/CIPPDBCacheTypes.json index eb833092befd..639f3ffb90d1 100644 --- a/Config/CIPPDBCacheTypes.json +++ b/Config/CIPPDBCacheTypes.json @@ -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", diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 index cd7fb1afbbd1..63cb47149ae2 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoQuarantinePolicy.ps1 @@ -29,4 +29,18 @@ function Set-CIPPDBCacheExoQuarantinePolicy { } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Quarantine policy data: $($_.Exception.Message)" -sev Error } + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Global Quarantine policy' -sev Debug + + $GlobalQuarantinePolicy = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantinePolicy' -cmdParams @{ QuarantinePolicyType = 'GlobalQuarantinePolicy' } + if ($GlobalQuarantinePolicy) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoGlobalQuarantinePolicy' -Data $GlobalQuarantinePolicy -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Global Quarantine policy' -sev Debug + } + $GlobalQuarantinePolicy = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Global Quarantine policy data: $($_.Exception.Message)" -sev Error + } } diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 index 90655ffcf9c1..d962fa7b91b0 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 @@ -6,45 +6,59 @@ function Invoke-CippTestORCA107 { param($Tenant) try { - $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoQuarantinePolicy' + $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoGlobalQuarantinePolicy' if (-not $Policies) { Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA107' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'End-user spam notification is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Quarantine' return } + # Exo returns EndUserSpamNotificationFrequency as an ISO 8601 duration string ('PT4H', 'P1D', 'P7D'). + # 'PT0S' or null means notifications are disabled. The placeholder policy name 'DefaultGlobalPolicy' + # indicates the global policy has never been configured. $FailedPolicies = [System.Collections.Generic.List[object]]::new() $PassedPolicies = [System.Collections.Generic.List[object]]::new() foreach ($Policy in $Policies) { - if ($Policy.EndUserSpamNotificationFrequency -gt 0) { - $PassedPolicies.Add($Policy) | Out-Null + $Frequency = $Policy.EndUserSpamNotificationFrequency + $IsConfigured = $Policy.Name -ne 'DefaultGlobalPolicy' + $IsEnabled = $false + if ($IsConfigured -and $Frequency) { + try { + $TimeSpan = [System.Xml.XmlConvert]::ToTimeSpan([string]$Frequency) + $IsEnabled = $TimeSpan.TotalSeconds -gt 0 + } catch { + $IsEnabled = $false + } + } + + $DisplayFrequency = if ($Frequency) { [string]$Frequency } else { 'Not set' } + $Annotated = $Policy | Select-Object *, @{ Name = 'DisplayFrequency'; Expression = { $DisplayFrequency } } + + if ($IsEnabled) { + $PassedPolicies.Add($Annotated) | Out-Null } else { - $FailedPolicies.Add($Policy) | Out-Null + $FailedPolicies.Add($Annotated) | Out-Null } } if ($FailedPolicies.Count -eq 0 -and $PassedPolicies.Count -gt 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All quarantine policies have end-user spam notifications enabled.`n`n") - $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") - $null = $Result.Append("| Policy Name | Notification Frequency (days) |`n") - $null = $Result.Append("|------------|-------------------------------|`n") + $Result = [System.Text.StringBuilder]::new("The Global Quarantine policy has end-user spam notifications enabled.`n`n") + $null = $Result.Append("| Policy Name | Notification Frequency |`n") + $null = $Result.Append("|------------|------------------------|`n") foreach ($Policy in $PassedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | $($Policy.EndUserSpamNotificationFrequency) |`n") + $null = $Result.Append("| $($Policy.Identity) | $($Policy.DisplayFrequency) |`n") } - } elseif ($PassedPolicies.Count -eq 0) { - $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("No quarantine policies have end-user spam notifications enabled.`n`n") } else { $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) quarantine policies do not have end-user spam notifications enabled.`n`n") - $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") + $Result = [System.Text.StringBuilder]::new("The Global Quarantine policy does not have end-user spam notifications enabled.`n`n") $null = $Result.Append("| Policy Name | Notification Frequency |`n") - $null = $Result.Append("|------------|----------------------|`n") + $null = $Result.Append("|------------|------------------------|`n") foreach ($Policy in $FailedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | Disabled |`n") + $null = $Result.Append("| $($Policy.Identity) | $($Policy.DisplayFrequency) |`n") } + $null = $Result.Append("`n**Remediation:** Configure the Global Quarantine policy with a notification frequency (e.g. PT4H, P1D, or P7D) via `Set-QuarantinePolicy -EndUserSpamNotificationFrequency`.") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA107' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'End-user spam notification is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Quarantine' From 2ab0e0e27c77d146301b427365e0d305e813137d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 10 Jun 2026 12:06:17 -0400 Subject: [PATCH 10/20] fix: rerun issue --- Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index f59727017896..6a259ea06a9d 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -62,9 +62,9 @@ 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.LastScheduledTime = "$CurrentUnixTime" - $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. } @@ -73,9 +73,9 @@ function Test-CIPPRerun { # 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.EstimatedNextRun = $EstimatedNextRun - $RerunData.LastScheduledTime = "$BaseTime" - $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 } @@ -83,9 +83,9 @@ function Test-CIPPRerun { 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.LastScheduledTime = "$BaseTime" - $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 } From 238001c13e0d62a7f13b00d46c99c7be82c11cad Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:13:53 +0800 Subject: [PATCH 11/20] Update policy based on MS and Orca guidance --- .../Invoke-CIPPStandardSpamFilterPolicy.ps1 | 32 +++++++++---------- .../ORCA/Identity/Invoke-CippTestORCA102.ps1 | 24 +++++++++++--- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index f01a369d3406..a305b1510973 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -7,8 +7,8 @@ function Invoke-CIPPStandardSpamFilterPolicy { .SYNOPSIS (Label) Default Spam Filter Policy .DESCRIPTION - (Helptext) This standard creates a Spam filter policy similar to the default strict policy. - (DocsDescription) This standard creates a Spam filter policy similar to the default strict policy, the following settings are configured to on by default: IncreaseScoreWithNumericIps, IncreaseScoreWithRedirectToOtherPort, MarkAsSpamEmptyMessages, MarkAsSpamJavaScriptInHtml, MarkAsSpamSpfRecordHardFail, MarkAsSpamFromAddressAuthFail, MarkAsSpamNdrBackscatter, MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled + (Helptext) This standard creates a Spam filter policy aligned with the Microsoft Strict preset. + (DocsDescription) This standard creates a Spam filter policy aligned with the Microsoft Strict preset. All Advanced Spam Filter (ASF) settings are left Off per Microsoft guidance (ASF is deprecated and prevents false-positive reporting). The following settings are configured On by default: MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled. .NOTES CAT Defender Standards @@ -127,20 +127,20 @@ function Invoke-CIPPStandardSpamFilterPolicy { ($CurrentState.BulkThreshold -eq [int]$Settings.BulkThreshold) -and ($CurrentState.QuarantineRetentionPeriod -eq 30) -and ($CurrentState.IncreaseScoreWithImageLinks -eq $IncreaseScoreWithImageLinks) -and - ($CurrentState.IncreaseScoreWithNumericIps -eq 'On') -and - ($CurrentState.IncreaseScoreWithRedirectToOtherPort -eq 'On') -and + ($CurrentState.IncreaseScoreWithNumericIps -eq 'Off') -and + ($CurrentState.IncreaseScoreWithRedirectToOtherPort -eq 'Off') -and ($CurrentState.IncreaseScoreWithBizOrInfoUrls -eq $IncreaseScoreWithBizOrInfoUrls) -and - ($CurrentState.MarkAsSpamEmptyMessages -eq 'On') -and - ($CurrentState.MarkAsSpamJavaScriptInHtml -eq 'On') -and + ($CurrentState.MarkAsSpamEmptyMessages -eq 'Off') -and + ($CurrentState.MarkAsSpamJavaScriptInHtml -eq 'Off') -and ($CurrentState.MarkAsSpamFramesInHtml -eq $MarkAsSpamFramesInHtml) -and ($CurrentState.MarkAsSpamObjectTagsInHtml -eq $MarkAsSpamObjectTagsInHtml) -and ($CurrentState.MarkAsSpamEmbedTagsInHtml -eq $MarkAsSpamEmbedTagsInHtml) -and ($CurrentState.MarkAsSpamFormTagsInHtml -eq $MarkAsSpamFormTagsInHtml) -and ($CurrentState.MarkAsSpamWebBugsInHtml -eq $MarkAsSpamWebBugsInHtml) -and ($CurrentState.MarkAsSpamSensitiveWordList -eq $MarkAsSpamSensitiveWordList) -and - ($CurrentState.MarkAsSpamSpfRecordHardFail -eq 'On') -and - ($CurrentState.MarkAsSpamFromAddressAuthFail -eq 'On') -and - ($CurrentState.MarkAsSpamNdrBackscatter -eq 'On') -and + ($CurrentState.MarkAsSpamSpfRecordHardFail -eq 'Off') -and + ($CurrentState.MarkAsSpamFromAddressAuthFail -eq 'Off') -and + ($CurrentState.MarkAsSpamNdrBackscatter -eq 'Off') -and ($CurrentState.MarkAsSpamBulkMail -eq 'On') -and ($CurrentState.InlineSafetyTipsEnabled -eq $true) -and ($CurrentState.PhishZapEnabled -eq $true) -and @@ -183,20 +183,20 @@ function Invoke-CIPPStandardSpamFilterPolicy { BulkThreshold = [int]$Settings.BulkThreshold QuarantineRetentionPeriod = 30 IncreaseScoreWithImageLinks = $IncreaseScoreWithImageLinks - IncreaseScoreWithNumericIps = 'On' - IncreaseScoreWithRedirectToOtherPort = 'On' + IncreaseScoreWithNumericIps = 'Off' + IncreaseScoreWithRedirectToOtherPort = 'Off' IncreaseScoreWithBizOrInfoUrls = $IncreaseScoreWithBizOrInfoUrls - MarkAsSpamEmptyMessages = 'On' - MarkAsSpamJavaScriptInHtml = 'On' + MarkAsSpamEmptyMessages = 'Off' + MarkAsSpamJavaScriptInHtml = 'Off' MarkAsSpamFramesInHtml = $MarkAsSpamFramesInHtml MarkAsSpamObjectTagsInHtml = $MarkAsSpamObjectTagsInHtml MarkAsSpamEmbedTagsInHtml = $MarkAsSpamEmbedTagsInHtml MarkAsSpamFormTagsInHtml = $MarkAsSpamFormTagsInHtml MarkAsSpamWebBugsInHtml = $MarkAsSpamWebBugsInHtml MarkAsSpamSensitiveWordList = $MarkAsSpamSensitiveWordList - MarkAsSpamSpfRecordHardFail = 'On' - MarkAsSpamFromAddressAuthFail = 'On' - MarkAsSpamNdrBackscatter = 'On' + MarkAsSpamSpfRecordHardFail = 'Off' + MarkAsSpamFromAddressAuthFail = 'Off' + MarkAsSpamNdrBackscatter = 'Off' MarkAsSpamBulkMail = 'On' InlineSafetyTipsEnabled = $true PhishZapEnabled = $true diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 index 5d4459f46995..747442bd974c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 @@ -54,12 +54,28 @@ function Invoke-CippTestORCA102 { $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") $null = $Result.Append("| Policy Name | Enabled ASF Options |`n") $null = $Result.Append("|------------|---------------------|`n") + $ASFSettingMap = [ordered]@{ + IncreaseScoreWithImageLinks = 'ImageLinks' + IncreaseScoreWithNumericIps = 'NumericIPs' + IncreaseScoreWithRedirectToOtherPort = 'RedirectToOtherPort' + IncreaseScoreWithBizOrInfoUrls = 'BizOrInfoUrls' + MarkAsSpamEmptyMessages = 'EmptyMessages' + MarkAsSpamJavaScriptInHtml = 'JavaScript' + MarkAsSpamFramesInHtml = 'Frames' + MarkAsSpamObjectTagsInHtml = 'ObjectTags' + MarkAsSpamEmbedTagsInHtml = 'EmbedTags' + MarkAsSpamFormTagsInHtml = 'FormTags' + MarkAsSpamWebBugsInHtml = 'WebBugs' + MarkAsSpamSensitiveWordList = 'SensitiveWordList' + MarkAsSpamSpfRecordHardFail = 'SpfRecordHardFail' + MarkAsSpamFromAddressAuthFail = 'FromAddressAuthFail' + MarkAsSpamNdrBackscatter = 'NdrBackscatter' + } foreach ($Policy in $FailedPolicies) { $EnabledOptions = [System.Collections.Generic.List[string]]::new() - if ($Policy.IncreaseScoreWithImageLinks -eq 'On') { $EnabledOptions.Add('ImageLinks') | Out-Null } - if ($Policy.IncreaseScoreWithNumericIps -eq 'On') { $EnabledOptions.Add('NumericIPs') | Out-Null } - if ($Policy.MarkAsSpamEmptyMessages -eq 'On') { $EnabledOptions.Add('EmptyMessages') | Out-Null } - if ($Policy.MarkAsSpamJavaScriptInHtml -eq 'On') { $EnabledOptions.Add('JavaScript') | Out-Null } + foreach ($Property in $ASFSettingMap.Keys) { + if ($Policy.$Property -eq 'On') { $EnabledOptions.Add($ASFSettingMap[$Property]) | Out-Null } + } $null = $Result.Append("| $($Policy.Identity) | $($EnabledOptions -join ', ') |`n") } } From 0640f07ccdf22a8294a91502def41b987be84ab5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:25:11 +0800 Subject: [PATCH 12/20] Fixes ORCA179 --- .../Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 index c2ea2cf19ed6..9013546daa21 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 @@ -13,10 +13,15 @@ function Invoke-CippTestORCA179 { return } + # Exclude the Built-In Protection preset — Microsoft scopes it to external senders by design. + $CustomPolicies = $Policies | Where-Object { + $_.IsBuiltInProtection -ne $true + } + $FailedPolicies = [System.Collections.Generic.List[object]]::new() $PassedPolicies = [System.Collections.Generic.List[object]]::new() - foreach ($Policy in $Policies) { + foreach ($Policy in $CustomPolicies) { if ($Policy.EnableForInternalSenders -eq $true) { $PassedPolicies.Add($Policy) | Out-Null } else { @@ -24,10 +29,13 @@ function Invoke-CippTestORCA179 { } } - if ($FailedPolicies.Count -eq 0) { + if ($PassedPolicies.Count -gt 0 -and $FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All Safe Links policies are enabled for internal senders.`n`n") + $Result = [System.Text.StringBuilder]::new("All custom Safe Links policies are enabled for internal senders.`n`n") $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") + } elseif ($PassedPolicies.Count -eq 0 -and $FailedPolicies.Count -eq 0) { + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("No custom Safe Links policies are configured. The Built-In Protection policy does not cover internal senders.`n`n**Remediation:** Create a custom Safe Links policy with `EnableForInternalSenders = `$true`.") } else { $Status = 'Failed' $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) Safe Links policies are not enabled for internal senders.`n`n") From db3de75a6c6fc7cbf6e1c775ffb485e9ca2a215d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:26:36 +0800 Subject: [PATCH 13/20] Fixes ORCA244 --- .../ORCA/Identity/Invoke-CippTestORCA244.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 index 7b413894a8a4..f3146399098c 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 @@ -6,10 +6,10 @@ function Invoke-CippTestORCA244 { param($Tenant) try { - $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + $Policies = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' if (-not $Policies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' return } @@ -17,7 +17,7 @@ function Invoke-CippTestORCA244 { $PassedPolicies = [System.Collections.Generic.List[object]]::new() foreach ($Policy in $Policies) { - if ($Policy.HonorDMARCPolicy -eq $true) { + if ($Policy.HonorDmarcPolicy -eq $true) { $PassedPolicies.Add($Policy) | Out-Null } else { $FailedPolicies.Add($Policy) | Out-Null @@ -26,24 +26,24 @@ function Invoke-CippTestORCA244 { if ($FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All anti-spam policies honor sending domain DMARC.`n`n") + $Result = [System.Text.StringBuilder]::new("All anti-phishing policies honor sending domain DMARC.`n`n") $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)") } else { $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-spam policies do not honor sending domain DMARC.`n`n") + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) anti-phishing policies do not honor sending domain DMARC.`n`n") $null = $Result.Append("**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n") $null = $Result.Append("| Policy Name | Honor DMARC Policy |`n") $null = $Result.Append("|------------|--------------------|`n") foreach ($Policy in $FailedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | $($Policy.HonorDMARCPolicy) |`n") + $null = $Result.Append("| $($Policy.Identity) | $($Policy.HonorDmarcPolicy) |`n") } } - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' } } From a556b98bdbd08fb9d97df79ae8c79f4d47011e39 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:34:47 +0800 Subject: [PATCH 14/20] Fixes ORCA113 --- .../ORCA/Identity/Invoke-CippTestORCA113.ps1 | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 index af98497bee2a..9a455e4b5661 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 @@ -13,39 +13,45 @@ function Invoke-CippTestORCA113 { return } + # Exclude the Built-In Protection preset — Microsoft owns it and intentionally allows click-through on the baseline. + $CustomPolicies = $SafeLinksPolicies | Where-Object { + $_.IsBuiltInProtection -ne $true + } + $FailedPolicies = [System.Collections.Generic.List[object]]::new() $PassedPolicies = [System.Collections.Generic.List[object]]::new() - foreach ($Policy in $SafeLinksPolicies) { - if ($Policy.DoNotAllowClickThrough -eq $true) { - $PassedPolicies.Add($Policy) + foreach ($Policy in $CustomPolicies) { + if ($Policy.AllowClickThrough -eq $false) { + $PassedPolicies.Add($Policy) | Out-Null } else { - $FailedPolicies.Add($Policy) + $FailedPolicies.Add($Policy) | Out-Null } } - if ($FailedPolicies.Count -eq 0) { + if ($PassedPolicies.Count -gt 0 -and $FailedPolicies.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("All Safe Links policies have click-through disabled (DoNotAllowClickThrough = true).`n`n") + $Result = [System.Text.StringBuilder]::new("All custom Safe Links policies have click-through disabled (AllowClickThrough = false).`n`n") $null = $Result.Append("**Compliant Policies:** $($PassedPolicies.Count)`n`n") - if ($PassedPolicies.Count -gt 0) { - $null = $Result.Append("| Policy Name | DoNotAllowClickThrough |`n") - $null = $Result.Append("|------------|----------------------|`n") - foreach ($Policy in $PassedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) |`n") - } + $null = $Result.Append("| Policy Name | AllowClickThrough |`n") + $null = $Result.Append("|------------|-------------------|`n") + foreach ($Policy in $PassedPolicies) { + $null = $Result.Append("| $($Policy.Identity) | $($Policy.AllowClickThrough) |`n") } + } elseif ($PassedPolicies.Count -eq 0 -and $FailedPolicies.Count -eq 0) { + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("No custom Safe Links policies are configured. The Built-In Protection policy allows click-through by design.`n`n**Remediation:** Create a custom Safe Links policy with `AllowClickThrough = `$false`.") } else { $Status = 'Failed' - $Result = [System.Text.StringBuilder]::new("Some Safe Links policies allow click-through, which reduces protection.`n`n") + $Result = [System.Text.StringBuilder]::new("$($FailedPolicies.Count) custom Safe Links policies allow click-through, which reduces protection.`n`n") $null = $Result.Append("**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n") $null = $Result.Append("### Non-Compliant Policies`n`n") - $null = $Result.Append("| Policy Name | DoNotAllowClickThrough | Recommended |`n") - $null = $Result.Append("|------------|----------------------|-------------|`n") + $null = $Result.Append("| Policy Name | AllowClickThrough | Recommended |`n") + $null = $Result.Append("|------------|-------------------|-------------|`n") foreach ($Policy in $FailedPolicies) { - $null = $Result.Append("| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) | true |`n") + $null = $Result.Append("| $($Policy.Identity) | $($Policy.AllowClickThrough) | false |`n") } - $null = $Result.Append("`n**Remediation:** Disable click-through (set DoNotAllowClickThrough to true) to prevent users from bypassing Safe Links protection.") + $null = $Result.Append("`n**Remediation:** Set AllowClickThrough to false to prevent users from bypassing Safe Links protection.") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA113' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'AllowClickThrough is disabled in Safe Links policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' From cbcc61b5afd8c7ea9bb9bef1da7878465afc5610 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:48:34 +0800 Subject: [PATCH 15/20] Fixes ORCA103 --- .../Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 index 8ee002e8f83e..ff2fa2fe11d0 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 @@ -20,17 +20,17 @@ function Invoke-CippTestORCA103 { $IsCompliant = $true $Issues = [System.Collections.Generic.List[string]]::new() - if ($Policy.RecipientLimitExternalPerHour -ne 500) { + if ($Policy.RecipientLimitExternalPerHour -le 0 -or $Policy.RecipientLimitExternalPerHour -gt 500) { $IsCompliant = $false - $Issues.Add("RecipientLimitExternalPerHour: $($Policy.RecipientLimitExternalPerHour) (should be 500)") | Out-Null + $Issues.Add("RecipientLimitExternalPerHour: $($Policy.RecipientLimitExternalPerHour) (should be between 1 and 500)") | Out-Null } - if ($Policy.RecipientLimitInternalPerHour -ne 1000) { + if ($Policy.RecipientLimitInternalPerHour -le 0 -or $Policy.RecipientLimitInternalPerHour -gt 1000) { $IsCompliant = $false - $Issues.Add("RecipientLimitInternalPerHour: $($Policy.RecipientLimitInternalPerHour) (should be 1000)") | Out-Null + $Issues.Add("RecipientLimitInternalPerHour: $($Policy.RecipientLimitInternalPerHour) (should be between 1 and 1000)") | Out-Null } - if ($Policy.ActionWhenThresholdReached -ne 'BlockUserForToday') { + if ($Policy.ActionWhenThresholdReached -ne 'BlockUser') { $IsCompliant = $false - $Issues.Add("ActionWhenThresholdReached: $($Policy.ActionWhenThresholdReached) (should be BlockUserForToday)") | Out-Null + $Issues.Add("ActionWhenThresholdReached: $($Policy.ActionWhenThresholdReached) (should be BlockUser)") | Out-Null } if ($IsCompliant) { From 2851db0f52d88c4a7b5ee0d1a739a7d9d95ea27b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:17:20 +0800 Subject: [PATCH 16/20] Fixes ORCA233_1 --- Config/CIPPDBCacheTypes.json | 5 ++ .../Public/Invoke-CIPPDBCacheCollection.ps1 | 1 + .../Set-CIPPDBCacheExoInboundConnector.ps1 | 32 +++++++ .../Identity/Invoke-CippTestORCA233_1.ps1 | 84 +++++++++++++++---- 4 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoInboundConnector.ps1 diff --git a/Config/CIPPDBCacheTypes.json b/Config/CIPPDBCacheTypes.json index 639f3ffb90d1..7ee7d80240ed 100644 --- a/Config/CIPPDBCacheTypes.json +++ b/Config/CIPPDBCacheTypes.json @@ -244,6 +244,11 @@ "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": "Mailboxes", "friendlyName": "Mailboxes", diff --git a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 index 2538fc8493fb..13539fbfbe1b 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 @@ -87,6 +87,7 @@ function Invoke-CIPPDBCacheCollection { 'ExoAdminAuditLogConfig' 'ExoPresetSecurityPolicy' 'ExoTenantAllowBlockList' + 'ExoInboundConnector' 'OwaMailboxPolicy' 'ReportSubmissionPolicy' 'ExoTransportConfig' diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoInboundConnector.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoInboundConnector.ps1 new file mode 100644 index 000000000000..e4ba018b2bfe --- /dev/null +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoInboundConnector.ps1 @@ -0,0 +1,32 @@ +function Set-CIPPDBCacheExoInboundConnector { + <# + .SYNOPSIS + Caches Exchange Online inbound connectors + + .PARAMETER TenantFilter + The tenant to cache inbound connector data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange inbound connectors' -sev Debug + + $InboundConnectors = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-InboundConnector' + if ($InboundConnectors) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoInboundConnector' -Data $InboundConnectors -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($InboundConnectors.Count) inbound connectors" -sev Debug + } + $InboundConnectors = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache inbound connector data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 index 3fb1c2970d87..e195ceb45eea 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 @@ -6,39 +6,87 @@ function Invoke-CippTestORCA233_1 { param($Tenant) try { - $OrgConfig = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + $Connectors = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoInboundConnector' - if (-not $OrgConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No organization config found in database.' -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + if (-not $Connectors) { + # No connectors at all means no third-party mail flow path to misconfigure. + $Result = [System.Text.StringBuilder]::new("No inbound connectors are configured. Enhanced filtering is not required.") + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Passed' -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Connectors' return } - $Config = $OrgConfig | Select-Object -First 1 + # Find enabled connectors with wildcard sender domain ('smtp:*;N' priority pattern). + # These are the third-party mail flow connectors that need enhanced filtering. + $WildcardPattern = '^smtp:\*;(\d+)$' + $RelevantConnectors = [System.Collections.Generic.List[object]]::new() + foreach ($Connector in $Connectors) { + if ($Connector.Enabled -ne $true) { continue } + foreach ($SenderDomain in @($Connector.SenderDomains)) { + if ($SenderDomain -match $WildcardPattern) { + $RelevantConnectors.Add($Connector) | Out-Null + break + } + } + } + + if ($RelevantConnectors.Count -eq 0) { + $Result = [System.Text.StringBuilder]::new("No enabled inbound connectors with wildcard sender domains were found. Enhanced filtering is not required.") + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Passed' -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Connectors' + return + } + + $FailedConnectors = [System.Collections.Generic.List[object]]::new() + $PassedConnectors = [System.Collections.Generic.List[object]]::new() + + foreach ($Connector in $RelevantConnectors) { + $SkipLast = $Connector.EFSkipLastIP -eq $true + $SkipIPsCount = @($Connector.EFSkipIPs).Count + $TestMode = $Connector.EFTestMode -eq $true + $UsersCount = @($Connector.EFUsers).Count + + $IsCompliant = ($SkipLast -or $SkipIPsCount -gt 0) -and -not $TestMode -and $UsersCount -eq 0 + + $Mode = if ($SkipLast) { 'Last IP' } + elseif ($SkipIPsCount -gt 0) { "Skip IPs ($SkipIPsCount)" } + else { 'Not Configured' } + if ($TestMode) { $Mode += ' (Test Mode)' } + if ($UsersCount -gt 0) { $Mode += " (Select Users: $UsersCount)" } - # Check if enhanced filtering is enabled - # This property may vary depending on Exchange Online version - $EnhancedFilteringEnabled = $false + $Entry = [PSCustomObject]@{ + Identity = $Connector.Identity + Mode = $Mode + } - # Check various properties that indicate enhanced filtering - if ($Config.PSObject.Properties.Name -contains 'SkipListedFromForging') { - $EnhancedFilteringEnabled = $Config.SkipListedFromForging -eq $false + if ($IsCompliant) { $PassedConnectors.Add($Entry) | Out-Null } + else { $FailedConnectors.Add($Entry) | Out-Null } } - if ($EnhancedFilteringEnabled) { + if ($FailedConnectors.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("Enhanced filtering appears to be properly configured.`n`n") - $null = $Result.Append("**Configuration:** Reviewed") + $Result = [System.Text.StringBuilder]::new("All inbound connectors with wildcard sender domains have enhanced filtering configured.`n`n") + $null = $Result.Append("**Compliant Connectors:** $($PassedConnectors.Count)`n`n") + $null = $Result.Append("| Connector | EF Mode |`n") + $null = $Result.Append("|-----------|---------|`n") + foreach ($Entry in $PassedConnectors) { + $null = $Result.Append("| $($Entry.Identity) | $($Entry.Mode) |`n") + } } else { - $Status = 'Informational' - $Result = [System.Text.StringBuilder]::new("Unable to fully determine enhanced filtering status. Manual review recommended.`n`n") - $null = $Result.Append("**Action Required:** Review inbound connectors for enhanced filtering configuration") + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("$($FailedConnectors.Count) inbound connectors do not have enhanced filtering configured correctly.`n`n") + $null = $Result.Append("**Failed:** $($FailedConnectors.Count) | **Passed:** $($PassedConnectors.Count)`n`n") + $null = $Result.Append("| Connector | EF Mode |`n") + $null = $Result.Append("|-----------|---------|`n") + foreach ($Entry in $FailedConnectors) { + $null = $Result.Append("| $($Entry.Identity) | $($Entry.Mode) |`n") + } + $null = $Result.Append("`n**Remediation:** Enable enhanced filtering on each connector by setting `EFSkipLastIP = `$true` (or populating `EFSkipIPs`), with `EFTestMode = `$false` and no per-user scoping.") } - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Connectors' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Connectors' } } From 28ba94b43709b89dee2a349742f68c1df5ac1bf8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:17:33 +0800 Subject: [PATCH 17/20] Fixes ORCA242 --- Config/CIPPDBCacheTypes.json | 5 ++ .../Public/Invoke-CIPPDBCacheCollection.ps1 | 1 + .../Set-CIPPDBCacheExoProtectionAlert.ps1 | 36 +++++++++ .../ORCA/Identity/Invoke-CippTestORCA242.ps1 | 75 +++++++++++++++---- 4 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoProtectionAlert.ps1 diff --git a/Config/CIPPDBCacheTypes.json b/Config/CIPPDBCacheTypes.json index 7ee7d80240ed..003fb3457dd1 100644 --- a/Config/CIPPDBCacheTypes.json +++ b/Config/CIPPDBCacheTypes.json @@ -249,6 +249,11 @@ "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", diff --git a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 index 13539fbfbe1b..06f258ec80f4 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 @@ -88,6 +88,7 @@ function Invoke-CIPPDBCacheCollection { 'ExoPresetSecurityPolicy' 'ExoTenantAllowBlockList' 'ExoInboundConnector' + 'ExoProtectionAlert' 'OwaMailboxPolicy' 'ReportSubmissionPolicy' 'ExoTransportConfig' diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoProtectionAlert.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoProtectionAlert.ps1 new file mode 100644 index 000000000000..f0cb8cfcf8fc --- /dev/null +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheExoProtectionAlert.ps1 @@ -0,0 +1,36 @@ +function Set-CIPPDBCacheExoProtectionAlert { + <# + .SYNOPSIS + Caches Exchange Online / Purview protection alert policies + + .DESCRIPTION + Calls Get-ProtectionAlert via the Security & Compliance PowerShell endpoint + (requires the -Compliance switch on New-ExoRequest). + + .PARAMETER TenantFilter + The tenant to cache protection alert data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange protection alerts' -sev Debug + + $ProtectionAlerts = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-ProtectionAlert' -Compliance + if ($ProtectionAlerts) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoProtectionAlert' -Data $ProtectionAlerts -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ProtectionAlerts.Count) protection alerts" -sev Debug + } + $ProtectionAlerts = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache protection alert data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 index b2e15b06483a..1486fffa483a 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 @@ -6,19 +6,68 @@ function Invoke-CippTestORCA242 { param($Tenant) try { - # This test would check for alert policies related to ATP/Defender for Office 365 - # Since we don't have an alert policy cache, we'll provide informational guidance - - $Status = 'Informational' - $Result = [System.Text.StringBuilder]::new("Alert policies for protection features should be enabled and monitored.`n`n") - $null = $Result.Append("**Recommended Alert Policies:**`n`n") - $null = $Result.Append("- Messages reported by users as malware or phish`n") - $null = $Result.Append("- Email sending limit exceeded`n") - $null = $Result.Append("- Suspicious email forwarding activity`n") - $null = $Result.Append("- Malware campaign detected`n") - $null = $Result.Append("- Suspicious connector activity`n") - $null = $Result.Append("- Unusual external user file activity`n") - $null = $Result.Append("`n**Action Required:** Verify alert policies are configured in Microsoft 365 Security & Compliance Center") + $Alerts = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoProtectionAlert' + + if (-not $Alerts) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA242' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No protection alert data found. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Important protection alerts enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Configuration' + return + } + + # ORCA-242: alerts that drive Automated Incident Response (AIR). + # Alerts not present in the tenant are skipped (Microsoft hasn't deployed them). + $ImportantAlerts = @( + 'A potentially malicious URL click was detected' + 'Teams message reported by user as security risk' + 'Email messages containing phish URLs removed after delivery' + 'Suspicious Email Forwarding Activity' + 'Malware not zapped because ZAP is disabled' + 'Phish delivered due to an ETR override' + 'Email messages containing malicious file removed after delivery' + 'Email reported by user as malware or phish' + 'Email messages containing malicious URL removed after delivery' + 'Email messages containing malware removed after delivery' + 'A user clicked through to a potentially malicious URL' + 'Email messages from a campaign removed after delivery' + 'Email messages removed after delivery' + 'Suspicious email sending patterns detected' + ) + + $FailedAlerts = [System.Collections.Generic.List[object]]::new() + $PassedAlerts = [System.Collections.Generic.List[object]]::new() + + foreach ($AlertName in $ImportantAlerts) { + $Found = $Alerts | Where-Object { $_.Name -eq $AlertName } | Select-Object -First 1 + if ($null -eq $Found) { continue } + + if ($Found.Disabled -eq $true) { + $FailedAlerts.Add($Found) | Out-Null + } else { + $PassedAlerts.Add($Found) | Out-Null + } + } + + if ($FailedAlerts.Count -eq 0 -and $PassedAlerts.Count -eq 0) { + $Status = 'Skipped' + $Result = [System.Text.StringBuilder]::new('None of the AIR-related protection alerts are deployed to this tenant. This may indicate missing Defender for Office 365 licensing.') + } elseif ($FailedAlerts.Count -eq 0) { + $Status = 'Passed' + $Result = [System.Text.StringBuilder]::new("All AIR-related protection alerts deployed to this tenant are enabled.`n`n") + $null = $Result.Append("**Enabled Alerts:** $($PassedAlerts.Count)`n`n") + $null = $Result.Append("| Alert Name |`n|------------|`n") + foreach ($Alert in $PassedAlerts) { + $null = $Result.Append("| $($Alert.Name) |`n") + } + } else { + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("$($FailedAlerts.Count) AIR-related protection alerts are disabled.`n`n") + $null = $Result.Append("**Disabled:** $($FailedAlerts.Count) | **Enabled:** $($PassedAlerts.Count)`n`n") + $null = $Result.Append("### Disabled Alerts`n`n") + $null = $Result.Append("| Alert Name | Disabled |`n|------------|----------|`n") + foreach ($Alert in $FailedAlerts) { + $null = $Result.Append("| $($Alert.Name) | $($Alert.Disabled) |`n") + } + $null = $Result.Append("`n**Remediation:** Re-enable these alert policies. Automated Incident Response (AIR) triggers from them and cannot function correctly when they are disabled.") + } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA242' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Important protection alerts enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Configuration' From f37d68adfa17516f05cb9422fe01da589252ada9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:26:09 +0800 Subject: [PATCH 18/20] Fixes ORCA235 --- .../ORCA/Identity/Invoke-CippTestORCA235.ps1 | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 index ae123a59de2c..cfbbd0491932 100644 --- a/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 +++ b/Modules/CIPPTests/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 @@ -6,31 +6,53 @@ function Invoke-CippTestORCA235 { param($Tenant) try { - $AcceptedDomains = Get-CIPPTestData -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + $Results = Get-CIPPDomainAnalyser -TenantFilter $Tenant - if (-not $AcceptedDomains) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' + if (-not $Results) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No Domain Analyser results found for this tenant. Run the CIPP Domain Analyser to populate domain health data.' -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' return } - # Note: This test would ideally check DNS SPF records - # Since we don't have DNS query capability here, we'll provide informational guidance + # ORCA scopes this to custom domains; onmicrosoft.com is handled by Microsoft. + $CustomDomains = $Results | Where-Object { $_.Domain -notlike '*.onmicrosoft.com' } - $CustomDomains = $AcceptedDomains | Where-Object { $_.DomainName -notlike '*.onmicrosoft.com' } + if (-not $CustomDomains -or $CustomDomains.Count -eq 0) { + $Status = 'Passed' + $Result = [System.Text.StringBuilder]::new('No custom domains found. Only onmicrosoft.com in use.') + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' + return + } + + $FailedDomains = [System.Collections.Generic.List[object]]::new() + $PassedDomains = [System.Collections.Generic.List[object]]::new() + + foreach ($Domain in $CustomDomains) { + # Valid SPF must start with v=spf1 AND end with -all (hard fail). + # ~all (soft fail), ?all (neutral), and +all (pass all) are insufficient. + # Third-party includes/IPs (Mimecast/Proofpoint/marketing) are fine alongside -all. + $Spf = [string]$Domain.ActualSPFRecord + $HasSpf = $Spf -match 'v=spf1' + $HasHardFail = $Spf -match '-all\s*$' + + if ($HasSpf -and $HasHardFail) { + $PassedDomains.Add($Domain) | Out-Null + } else { + $FailedDomains.Add($Domain) | Out-Null + } + } - if ($CustomDomains.Count -eq 0) { + if ($FailedDomains.Count -eq 0) { $Status = 'Passed' - $Result = [System.Text.StringBuilder]::new("No custom domains found. Only using onmicrosoft.com domain.`n`n") - $null = $Result.Append("**Total Domains:** $($AcceptedDomains.Count)") + $Result = [System.Text.StringBuilder]::new("All $($PassedDomains.Count) custom domains have a valid SPF record ending in -all.") } else { - $Status = 'Informational' - $Result = [System.Text.StringBuilder]::new("Found $($CustomDomains.Count) custom domains that should have SPF records configured.`n`n") - $null = $Result.Append("**Custom Domains:**`n`n") - foreach ($Domain in $CustomDomains) { - $null = $Result.Append("- $($Domain.DomainName)`n") + $Status = 'Failed' + $Result = [System.Text.StringBuilder]::new("$($FailedDomains.Count) of $($CustomDomains.Count) custom domains are missing a valid SPF record or do not end in -all (hard fail).`n`n") + $null = $Result.Append("| Domain | SPF Record |`n| :----- | :--------- |`n") + foreach ($Domain in ($FailedDomains | Select-Object -First 25)) { + $Display = if ([string]::IsNullOrWhiteSpace($Domain.ActualSPFRecord)) { '*(none)*' } else { $Domain.ActualSPFRecord } + $null = $Result.Append("| $($Domain.Domain) | $Display |`n") } - $null = $Result.Append("`n**Action Required:** Verify that each custom domain has an SPF record including Microsoft 365:`n") - $null = $Result.Append("``v=spf1 include:spf.protection.outlook.com -all``") + $null = $Result.Append("`n**Remediation:** Publish an SPF TXT record ending in `-all` (hard fail). For Microsoft 365 only: `v=spf1 include:spf.protection.outlook.com -all`. If routing through a third-party gateway, include that provider alongside (e.g. Mimecast, Proofpoint, marketing services), but keep `-all` at the end. Avoid `~all`, `?all`, and especially `+all`.") } Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' From e9bc5ad50bce44db439e9d271c37a0ec5b47a5ac Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 10 Jun 2026 16:51:47 -0400 Subject: [PATCH 19/20] fix: add early template filter when supplied improve performance and accuracy of applied templates --- .../Public/Standards/Get-CIPPStandards.ps1 | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index 4b3dea9eda89..6a86545741f5 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -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 @@ -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 } From 8f2198e7cf303ec2726066c95a4ebacb55e75f27 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 10 Jun 2026 17:02:38 -0400 Subject: [PATCH 20/20] chore: bump version to 10.5.2 --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index accd9b7c83bc..7648204274b0 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.5.1", + "defaultVersion": "10.5.2", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 4a6e70e959e5..a39233be07ad 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.5.1 +10.5.2