diff --git a/Config/SchedulerRateLimits.json b/Config/SchedulerRateLimits.json
deleted file mode 100644
index 3d2c65716af0..000000000000
--- a/Config/SchedulerRateLimits.json
+++ /dev/null
@@ -1,10 +0,0 @@
-[
- {
- "Command": "Sync-CIPPExtensionData",
- "MaxRequests": 50
- },
- {
- "Command": "Push-CIPPExtensionData",
- "MaxRequests": 30
- }
-]
\ No newline at end of file
diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1
index 7b96b16bc4cb..be862eabe375 100644
--- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1
+++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1
@@ -41,6 +41,7 @@ function Add-CIPPDbItem {
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[Alias('Data')]
+ [AllowNull()]
[AllowEmptyCollection()]
$InputObject,
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1
index 56182ece6f32..f0ca6d528e40 100644
--- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1
@@ -11,6 +11,11 @@
$TenantFilter
)
+ #Add rerun protection: This Monitor can only run once every hour.
+ $Rerun = Test-CIPPRerun -TenantFilter $TenantFilter -Type 'ExchangeMonitor' -API 'Get-CIPPAlertQuarantineReleaseRequests'
+ if ($Rerun) {
+ return $true
+ }
$HasLicense = Test-CIPPStandardLicense -StandardName 'QuarantineReleaseRequests' -TenantFilter $TenantFilter -RequiredCapabilities @(
'EXCHANGE_S_STANDARD',
'EXCHANGE_S_ENTERPRISE',
@@ -20,11 +25,11 @@
)
if (-not $HasLicense) {
- return
+ return $true
}
try {
- $RequestedReleases = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = 1000; ReleaseStatus = 'Requested' } -ErrorAction Stop | Select-Object -ExcludeProperty *data.type*
+ $RequestedReleases = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = 1000; ReleaseStatus = 'Requested'; StartReceivedDate = (Get-Date).AddHours(-6) } -ErrorAction Stop | Select-Object -ExcludeProperty *data.type*
if ($RequestedReleases) {
# Get the CIPP URL for the Quarantine link
diff --git a/Modules/CIPPCore/Public/Assert-CippVersion.ps1 b/Modules/CIPPCore/Public/Assert-CippVersion.ps1
index ac61237dcb60..c96873ede537 100644
--- a/Modules/CIPPCore/Public/Assert-CippVersion.ps1
+++ b/Modules/CIPPCore/Public/Assert-CippVersion.ps1
@@ -10,8 +10,10 @@ function Assert-CippVersion {
Local version of CIPP frontend
#>
- Param($CIPPVersion)
- $APIVersion = (Get-Content 'version_latest.txt' -Raw).trim()
+ param($CIPPVersion)
+ $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase
+ $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent
+ $APIVersion = (Get-Content -Path $CIPPRoot\version_latest.txt).trim()
$RemoteAPIVersion = (Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP-API/master/version_latest.txt').trim()
$RemoteCIPPVersion = (Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP/main/public/version.json').version
diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1
index e80608d97d6a..54f1dc0f3c16 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1
@@ -39,15 +39,19 @@ function Push-AuditLogTenantDownload {
$LogSearchesTable = Get-CippTable -TableName 'AuditLogSearches'
try {
- $LogSearches = Get-CippAuditLogSearches -TenantFilter $TenantFilter -ReadyToProcess | Select-Object -First 10
- Write-Information ('Audit Logs: Found {0} searches, begin downloading' -f $LogSearches.Count)
+ $LogSearches = Get-CippAuditLogSearches -TenantFilter $TenantFilter -ReadyToProcess | Sort-Object -Property filterStartDateTime | Select-Object -First 10
+ if ($LogSearches.Count -eq 0) {
+ Write-Information "Audit Logs: No searches ready to process for $TenantFilter"
+ return $true
+ }
+ Write-Information ('Audit Logs: Found {0} searches for {1}, begin downloading' -f $LogSearches.Count, $TenantFilter)
foreach ($Search in $LogSearches) {
$SearchEntity = Get-CIPPAzDataTableEntity @LogSearchesTable -Filter "Tenant eq '$($TenantFilter)' and RowKey eq '$($Search.id)'"
$SearchEntity.CippStatus = 'Processing'
Add-CIPPAzDataTableEntity @LogSearchesTable -Entity $SearchEntity -Force
try {
Write-Information "Audit Log search: Processing search ID: $($Search.id) for tenant: $TenantFilter"
- $Downloads = New-CIPPAuditLogSearchResultsCache -TenantFilter $TenantFilter -searchId $Search.id
+ $null = New-CIPPAuditLogSearchResultsCache -TenantFilter $TenantFilter -searchId $Search.id
$SearchEntity.CippStatus = 'Downloaded'
} catch {
if ($_.Exception.Message -match 'Request rate is large. More Request Units may be needed, so no changes were made. Please retry this request later.') {
@@ -68,6 +72,7 @@ function Push-AuditLogTenantDownload {
}
Add-CIPPAzDataTableEntity @LogSearchesTable -Entity $SearchEntity -Force
}
+ return $true
} catch {
Write-Information ('Audit Log search: Error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message)
return $false
diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1
index eb94ced2c322..494b04b7b782 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1
@@ -10,7 +10,7 @@ function Invoke-ListExtensionsConfig {
$Table = Get-CIPPTable -TableName Extensionsconfig
try {
$Config = (Get-CIPPAzDataTableEntity @Table).config
- if (Test-Json -Json $Config -ErrorAction SilentlyContinue) {
+ if ($Config -and (Test-Json -Json $Config -ErrorAction SilentlyContinue)) {
$Body = $Config | ConvertFrom-Json -Depth 10 -ErrorAction Stop
if ($Body.HaloPSA.TicketType -and !$Body.HaloPSA.TicketType.value) {
# translate ticket type to autocomplete format
diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1
index c1ae93dd22a1..a8c64da33fec 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1
@@ -45,7 +45,6 @@ function Start-AuditLogSearchCreation {
}
if (!$TenantInConfig) {
- Write-Information "Tenant $($Tenant.defaultDomainName) has no configured audit log rules, skipping search creation."
continue
}
@@ -67,7 +66,7 @@ function Start-AuditLogSearchCreation {
SkipLog = $true
}
Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)
- Write-Information "Started Audit Log search creation orchestratorwith $($Batch.Count) tenants"
+ Write-Information "Started Audit Log search creation orchestrator with $($Batch.Count) tenants"
} else {
Write-Information 'No tenants found for Audit Log search creation'
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1
index 272c78ee2627..b677afdd2071 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1
@@ -17,33 +17,6 @@ function Start-UserTasksOrchestrator {
$Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Pending' and Timestamp lt datetime'$30MinutesAgo') or (TaskState eq 'Running' and Timestamp lt datetime'$4HoursAgo'))"
$tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter
- $RateLimitTable = Get-CIPPTable -tablename 'SchedulerRateLimits'
- $RateLimits = Get-CIPPAzDataTableEntity @RateLimitTable -Filter "PartitionKey eq 'SchedulerRateLimits'"
-
- $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase
- $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent
- $DefaultRateLimits = Get-Content -Path "$CIPPRoot/Config/SchedulerRateLimits.json" | ConvertFrom-Json
- $NewRateLimits = foreach ($Limit in $DefaultRateLimits) {
- if ($Limit.Command -notin $RateLimits.RowKey) {
- @{
- PartitionKey = 'SchedulerRateLimits'
- RowKey = $Limit.Command
- MaxRequests = $Limit.MaxRequests
- }
- }
- }
-
- if ($NewRateLimits) {
- $null = Add-CIPPAzDataTableEntity @RateLimitTable -Entity $NewRateLimits -Force
- $RateLimits = Get-CIPPAzDataTableEntity @RateLimitTable -Filter "PartitionKey eq 'SchedulerRateLimits'"
- }
-
- # Create a hashtable for quick rate limit lookups
- $RateLimitLookup = @{}
- foreach ($limit in $RateLimits) {
- $RateLimitLookup[$limit.RowKey] = $limit.MaxRequests
- }
-
$Batch = [System.Collections.Generic.List[object]]::new()
$TenantList = Get-Tenants -IncludeErrors
foreach ($task in $tasks) {
@@ -62,9 +35,12 @@ function Start-UserTasksOrchestrator {
TaskState = 'Pending'
}
$task.Parameters = $task.Parameters | ConvertFrom-Json -AsHashtable
- $task.AdditionalProperties = $task.AdditionalProperties | ConvertFrom-Json
-
if (!$task.Parameters) { $task.Parameters = @{} }
+
+ # Cache Get-Command result to avoid repeated expensive reflection calls
+ $CommandInfo = Get-Command $task.Command
+ $HasTenantFilter = $CommandInfo.Parameters.ContainsKey('TenantFilter')
+
$ScheduledCommand = [pscustomobject]@{
Command = $task.Command
Parameters = $task.Parameters
@@ -77,13 +53,15 @@ function Start-UserTasksOrchestrator {
Write-Host "Excluded Tenants from this task: $ExcludedTenants"
$AllTenantCommands = foreach ($Tenant in $TenantList | Where-Object { $_.defaultDomainName -notin $ExcludedTenants }) {
$NewParams = $task.Parameters.Clone()
- if ((Get-Command $task.Command).Parameters.TenantFilter) {
+ if ($HasTenantFilter) {
$NewParams.TenantFilter = $Tenant.defaultDomainName
}
+ # Clone TaskInfo to prevent shared object references
+ $TaskInfoClone = $task.PSObject.Copy()
[pscustomobject]@{
Command = $task.Command
Parameters = $NewParams
- TaskInfo = $task
+ TaskInfo = $TaskInfoClone
FunctionName = 'ExecScheduledCommand'
}
}
@@ -109,13 +87,15 @@ function Start-UserTasksOrchestrator {
$GroupTenantCommands = foreach ($ExpandedTenant in $ExpandedTenants | Where-Object { $_.value -notin $ExcludedTenants }) {
$NewParams = $task.Parameters.Clone()
- if ((Get-Command $task.Command).Parameters.TenantFilter) {
+ if ($HasTenantFilter) {
$NewParams.TenantFilter = $ExpandedTenant.value
}
+ # Clone TaskInfo to prevent shared object references
+ $TaskInfoClone = $task.PSObject.Copy()
[pscustomobject]@{
Command = $task.Command
Parameters = $NewParams
- TaskInfo = $task
+ TaskInfo = $TaskInfoClone
FunctionName = 'ExecScheduledCommand'
}
}
@@ -125,14 +105,14 @@ function Start-UserTasksOrchestrator {
Write-LogMessage -API 'Scheduler_UserTasks' -tenant $tenant -message "Failed to expand tenant group for task $($task.Name): $($_.Exception.Message)" -sev Error
# Fall back to treating as single tenant
- if ((Get-Command $task.Command).Parameters.TenantFilter) {
+ if ($HasTenantFilter) {
$ScheduledCommand.Parameters['TenantFilter'] = $task.Tenant
}
$Batch.Add($ScheduledCommand)
}
} else {
# Handle single tenant
- if ((Get-Command $task.Command).Parameters.TenantFilter) {
+ if ($HasTenantFilter) {
$ScheduledCommand.Parameters['TenantFilter'] = $task.Tenant
}
$Batch.Add($ScheduledCommand)
@@ -155,51 +135,35 @@ function Start-UserTasksOrchestrator {
Write-Information 'Batching tasks for execution...'
Write-Information "Total tasks to process: $($Batch.Count)"
- if (($Batch | Measure-Object).Count -gt 0) {
- # Group commands by type and apply rate limits
- $CommandGroups = $Batch | Group-Object -Property Command
+ if ($Batch.Count -gt 0) {
+ # Group tasks by tenant instead of command type
+ $TenantGroups = $Batch | Group-Object -Property { $_.Parameters.TenantFilter }
$ProcessedBatches = [System.Collections.Generic.List[object]]::new()
- foreach ($CommandGroup in $CommandGroups) {
- $CommandName = $CommandGroup.Name
- $Commands = [System.Collections.Generic.List[object]]::new($CommandGroup.Group)
-
- # Get rate limit for this command (default to 100 if not found)
- $MaxItemsPerBatch = if ($RateLimitLookup.ContainsKey($CommandName)) {
- $RateLimitLookup[$CommandName]
- } else {
- 100
- }
-
- # Split into batches based on rate limit
- while ($Commands.Count -gt 0) {
- $BatchSize = [Math]::Min($Commands.Count, $MaxItemsPerBatch)
- $CommandBatch = [System.Collections.Generic.List[object]]::new()
-
- for ($i = 0; $i -lt $BatchSize; $i++) {
- $CommandBatch.Add($Commands[0])
- $Commands.RemoveAt(0)
- }
+ foreach ($TenantGroup in $TenantGroups) {
+ $TenantName = $TenantGroup.Name
+ $TenantCommands = [System.Collections.Generic.List[object]]::new($TenantGroup.Group)
- $ProcessedBatches.Add($CommandBatch)
- }
+ Write-Information "Creating batch for tenant: $TenantName with $($TenantCommands.Count) tasks"
+ $ProcessedBatches.Add($TenantCommands)
}
- # Process each batch separately
+ # Process each tenant batch separately
foreach ($ProcessedBatch in $ProcessedBatches) {
- Write-Information "Processing batch with $($ProcessedBatch.Count) tasks..."
+ $TenantName = $ProcessedBatch[0].Parameters.TenantFilter
+ Write-Information "Processing batch for tenant: $TenantName with $($ProcessedBatch.Count) tasks..."
Write-Information 'Tasks by command:'
$ProcessedBatch | Group-Object -Property Command | ForEach-Object {
Write-Information " - $($_.Name): $($_.Count)"
}
- # Create queue entry for each batch
- $Queue = New-CippQueueEntry -Name "Scheduled Tasks - Batch #$($ProcessedBatches.IndexOf($ProcessedBatch) + 1) of $($ProcessedBatches.Count)"
+ # Create queue entry for each tenant batch
+ $Queue = New-CippQueueEntry -Name "Scheduled Tasks - $TenantName"
$QueueId = $Queue.RowKey
- $BatchWithQueue = $ProcessedBatch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, ($_.TaskInfo.Tenant -ne 'AllTenants' ? $_.TaskInfo.Tenant : $_.Parameters.TenantFilter) } }
+ $BatchWithQueue = $ProcessedBatch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, $TenantName } }
$InputObject = [PSCustomObject]@{
- OrchestratorName = 'UserTaskOrchestrator'
+ OrchestratorName = "UserTaskOrchestrator_$TenantName"
Batch = @($BatchWithQueue)
SkipLog = $true
}
diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1
index 67340eaf3ab5..886a8f9201a8 100644
--- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1
+++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1
@@ -13,7 +13,11 @@ function New-CIPPAlertTemplate {
$AlertComment
)
$Appname = '[{"Application Name":"ACOM Azure Website","Application IDs":"23523755-3a2b-41ca-9315-f81f3f566a95"},{"Application Name":"AEM-DualAuth","Application IDs":"69893ee3-dd10-4b1c-832d-4870354be3d8"},{"Application Name":"ASM Campaign Servicing","Application IDs":"0cb7b9ec-5336-483b-bc31-b15b5788de71"},{"Application Name":"Azure Advanced Threat Protection","Application IDs":"7b7531ad-5926-4f2d-8a1d-38495ad33e17"},{"Application Name":"Azure Data Lake","Application IDs":"e9f49c6b-5ce5-44c8-925d-015017e9f7ad"},{"Application Name":"Azure Lab Services Portal","Application IDs":"835b2a73-6e10-4aa5-a979-21dfda45231c"},{"Application Name":"Azure Portal","Application IDs":"c44b4083-3bb0-49c1-b47d-974e53cbdf3c"},{"Application Name":"AzureSupportCenter","Application IDs":"37182072-3c9c-4f6a-a4b3-b3f91cacffce"},{"Application Name":"Bing","Application IDs":"9ea1ad79-fdb6-4f9a-8bc3-2b70f96e34c7"},{"Application Name":"CPIM Service","Application IDs":"bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4"},{"Application Name":"CRM Power BI Integration","Application IDs":"e64aa8bc-8eb4-40e2-898b-cf261a25954f"},{"Application Name":"Dataverse","Application IDs":"00000007-0000-0000-c000-000000000000"},{"Application Name":"Enterprise Roaming and Backup","Application IDs":"60c8bde5-3167-4f92-8fdb-059f6176dc0f"},{"Application Name":"IAM Supportability","Application IDs":"a57aca87-cbc0-4f3c-8b9e-dc095fdc8978"},{"Application Name":"IrisSelectionFrontDoor","Application IDs":"16aeb910-ce68-41d1-9ac3-9e1673ac9575"},{"Application Name":"MCAPI Authorization Prod","Application IDs":"d73f4b35-55c9-48c7-8b10-651f6f2acb2e"},{"Application Name":"Media Analysis and Transformation Service","Application IDs":"944f0bd1-117b-4b1c-af26-804ed95e767e
0cd196ee-71bf-4fd6-a57c-b491ffd4fb1e"},{"Application Name":"Microsoft 365 Support Service","Application IDs":"ee272b19-4411-433f-8f28-5c13cb6fd407"},{"Application Name":"Microsoft App Access Panel","Application IDs":"0000000c-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Approval Management","Application IDs":"65d91a3d-ab74-42e6-8a2f-0add61688c74
38049638-cc2c-4cde-abe4-4479d721ed44"},{"Application Name":"Microsoft Authentication Broker","Application IDs":"29d9ed98-a469-4536-ade2-f981bc1d605e"},{"Application Name":"Microsoft Azure CLI","Application IDs":"04b07795-8ddb-461a-bbee-02f9e1bf7b46"},{"Application Name":"Microsoft Azure PowerShell","Application IDs":"1950a258-227b-4e31-a9cf-717495945fc2"},{"Application Name":"Microsoft Bing Search","Application IDs":"cf36b471-5b44-428c-9ce7-313bf84528de"},{"Application Name":"Microsoft Bing Search for Microsoft Edge","Application IDs":"2d7f3606-b07d-41d1-b9d2-0d0c9296a6e8"},{"Application Name":"Microsoft Bing Default Search Engine","Application IDs":"1786c5ed-9644-47b2-8aa0-7201292175b6"},{"Application Name":"Microsoft Defender for Cloud Apps","Application IDs":"3090ab82-f1c1-4cdf-af2c-5d7a6f3e2cc7"},{"Application Name":"Microsoft Docs","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Dynamics ERP","Application IDs":"00000015-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Edge Insider Addons Prod","Application IDs":"6253bca8-faf2-4587-8f2f-b056d80998a7"},{"Application Name":"Microsoft Exchange Online Protection","Application IDs":"00000007-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Forms","Application IDs":"c9a559d2-7aab-4f13-a6ed-e7e9c52aec87"},{"Application Name":"Microsoft Graph","Application IDs":"00000003-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Intune Web Company Portal","Application IDs":"74bcdadc-2fdc-4bb3-8459-76d06952a0e9"},{"Application Name":"Microsoft Intune Windows Agent","Application IDs":"fc0f3af4-6835-4174-b806-f7db311fd2f3"},{"Application Name":"Microsoft Learn","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Office","Application IDs":"d3590ed6-52b3-4102-aeff-aad2292ab01c"},{"Application Name":"Microsoft Office 365 Portal","Application IDs":"00000006-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Office Web Apps Service","Application IDs":"67e3df25-268a-4324-a550-0de1c7f97287"},{"Application Name":"Microsoft Online Syndication Partner Portal","Application IDs":"d176f6e7-38e5-40c9-8a78-3998aab820e7"},{"Application Name":"Microsoft password reset service","Application IDs":"93625bc8-bfe2-437a-97e0-3d0060024faa"},{"Application Name":"Microsoft Power BI","Application IDs":"871c010f-5e61-4fb1-83ac-98610a7e9110"},{"Application Name":"Microsoft Storefronts","Application IDs":"28b567f6-162c-4f54-99a0-6887f387bbcc"},{"Application Name":"Microsoft Stream Portal","Application IDs":"cf53fce8-def6-4aeb-8d30-b158e7b1cf83"},{"Application Name":"Microsoft Substrate Management","Application IDs":"98db8bd6-0cc0-4e67-9de5-f187f1cd1b41"},{"Application Name":"Microsoft Support","Application IDs":"fdf9885b-dd37-42bf-82e5-c3129ef5a302"},{"Application Name":"Microsoft Teams","Application IDs":"1fec8e78-bce4-4aaf-ab1b-5451cc387264"},{"Application Name":"Microsoft Teams Services","Application IDs":"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"},{"Application Name":"Microsoft Teams Web Client","Application IDs":"5e3ce6c0-2b1f-4285-8d4b-75ee78787346"},{"Application Name":"Microsoft Whiteboard Services","Application IDs":"95de633a-083e-42f5-b444-a4295d8e9314"},{"Application Name":"O365 Suite UX","Application IDs":"4345a7b9-9a63-4910-a426-35363201d503"},{"Application Name":"Office 365 Exchange Online","Application IDs":"00000002-0000-0ff1-ce00-000000000000"},{"Application Name":"Office 365 Management","Application IDs":"00b41c95-dab0-4487-9791-b9d2c32c80f2"},{"Application Name":"Office 365 Search Service","Application IDs":"66a88757-258c-4c72-893c-3e8bed4d6899"},{"Application Name":"Office 365 SharePoint Online","Application IDs":"00000003-0000-0ff1-ce00-000000000000"},{"Application Name":"Office Delve","Application IDs":"94c63fef-13a3-47bc-8074-75af8c65887a"},{"Application Name":"Office Online Add-in SSO","Application IDs":"93d53678-613d-4013-afc1-62e9e444a0a5"},{"Application Name":"Office Online Client AAD- Augmentation Loop","Application IDs":"2abdc806-e091-4495-9b10-b04d93c3f040"},{"Application Name":"Office Online Client AAD- Loki","Application IDs":"b23dd4db-9142-4734-867f-3577f640ad0c"},{"Application Name":"Office Online Client AAD- Maker","Application IDs":"17d5e35f-655b-4fb0-8ae6-86356e9a49f5"},{"Application Name":"Office Online Client MSA- Loki","Application IDs":"b6e69c34-5f1f-4c34-8cdf-7fea120b8670"},{"Application Name":"Office Online Core SSO","Application IDs":"243c63a3-247d-41c5-9d83-7788c43f1c43"},{"Application Name":"Office Online Search","Application IDs":"a9b49b65-0a12-430b-9540-c80b3332c127"},{"Application Name":"Office.com","Application IDs":"4b233688-031c-404b-9a80-a4f3f2351f90"},{"Application Name":"Office365 Shell WCSS-Client","Application IDs":"89bee1f7-5e6e-4d8a-9f3d-ecd601259da7"},{"Application Name":"OfficeClientService","Application IDs":"0f698dd4-f011-4d23-a33e-b36416dcb1e6"},{"Application Name":"OfficeHome","Application IDs":"4765445b-32c6-49b0-83e6-1d93765276ca"},{"Application Name":"OfficeShredderWacClient","Application IDs":"4d5c2d63-cf83-4365-853c-925fd1a64357"},{"Application Name":"OMSOctopiPROD","Application IDs":"62256cef-54c0-4cb4-bcac-4c67989bdc40"},{"Application Name":"OneDrive SyncEngine","Application IDs":"ab9b8c07-8f02-4f72-87fa-80105867a763"},{"Application Name":"OneNote","Application IDs":"2d4d3d8e-2be3-4bef-9f87-7875a61c29de"},{"Application Name":"Outlook Mobile","Application IDs":"27922004-5251-4030-b22d-91ecd9a37ea4"},{"Application Name":"Partner Customer Delegated Admin Offline Processor","Application IDs":"a3475900-ccec-4a69-98f5-a65cd5dc5306"},{"Application Name":"Password Breach Authenticator","Application IDs":"bdd48c81-3a58-4ea9-849c-ebea7f6b6360"},{"Application Name":"Power BI Service","Application IDs":"00000009-0000-0000-c000-000000000000"},{"Application Name":"SharedWithMe","Application IDs":"ffcb16e8-f789-467c-8ce9-f826a080d987"},{"Application Name":"SharePoint Online Web Client Extensibility","Application IDs":"08e18876-6177-487e-b8b5-cf950c1e598c"},{"Application Name":"Signup","Application IDs":"b4bddae8-ab25-483e-8670-df09b9f1d0ea"},{"Application Name":"Skype for Business Online","Application IDs":"00000004-0000-0ff1-ce00-000000000000"},{"Application Name":"Sway","Application IDs":"905fcf26-4eb7-48a0-9ff0-8dcc7194b5ba"},{"Application Name":"Universal Store Native Client","Application IDs":"268761a2-03f3-40df-8a8b-c3db24145b6b"},{"Application Name":"Vortex [wsfed enabled]","Application IDs":"5572c4c0-d078-44ce-b81c-6cbf8d3ed39e"},{"Application Name":"Windows Azure Active Directory","Application IDs":"00000002-0000-0000-c000-000000000000"},{"Application Name":"Windows Azure Service Management API","Application IDs":"797f4846-ba00-4fd7-ba43-dac1f8f63013"},{"Application Name":"WindowsDefenderATP Portal","Application IDs":"a3b79187-70b2-4139-83f9-6016c58cd27b"},{"Application Name":"Windows Search","Application IDs":"26a7ee05-5602-4d76-a7ba-eae8b7b67941"},{"Application Name":"Windows Spotlight","Application IDs":"1b3c667f-cde3-4090-b60b-3d2abd0117f0"},{"Application Name":"Windows Store for Business","Application IDs":"45a330b1-b1ec-4cc1-9161-9f03992aa49f"},{"Application Name":"Yammer","Application IDs":"00000005-0000-0ff1-ce00-000000000000"},{"Application Name":"Yammer Web","Application IDs":"c1c74fed-04c9-4704-80dc-9f79a2e515cb"},{"Application Name":"Yammer Web Embed","Application IDs":"e1ef36fd-b883-4dbf-97f0-9ece4b576fc6"}]' | ConvertFrom-Json | Where-Object -Property 'Application IDs' -EQ $data.applicationId
- $HTMLTemplate = Get-Content 'TemplateEmail.html' -Raw | Out-String
+ # Get the function app root directory by navigating from the module location
+ $ModuleBase = Get-Module CIPPCore | Select-Object -ExpandProperty ModuleBase
+ $FunctionAppRoot = (Get-Item $ModuleBase).Parent.Parent.FullName
+ $TemplatePath = Join-Path $FunctionAppRoot 'TemplateEmail.html'
+ $HTMLTemplate = Get-Content $TemplatePath -Raw | Out-String
$Title = ''
$IntroText = ''
$ButtonUrl = ''
diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1
index 27fb33c5cbc6..3716fa9a5c06 100644
--- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1
@@ -24,11 +24,11 @@ function Set-CIPPDBCacheMailboxes {
Select = $Select
}
# Use Generic List for better memory efficiency with large datasets
- $MailboxList = [System.Collections.Generic.List[PSObject]]::new()
+ $Mailboxes = [System.Collections.Generic.List[PSObject]]::new()
$RawMailboxes = New-ExoRequest @ExoRequest
foreach ($Mailbox in $RawMailboxes) {
- $MailboxList.Add(($Mailbox | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted,
+ $Mailboxes.Add(($Mailbox | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted,
@{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } },
@{ Name = 'displayName'; Expression = { $_.'DisplayName' } },
@{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } },
diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1
index b7b0f4810c15..311a02400c6f 100644
--- a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1
@@ -22,7 +22,7 @@
}
try {
- $null = New-ExoRequest -tenantid $TenantFilter -cmdlet "$State-InboxRule" -Anchor $Username -cmdParams @{Identity = $RuleId; mailbox = $UserId } -Headers $Headers
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet "$State-InboxRule" -Anchor $Username -cmdParams @{Identity = $RuleId; Mailbox = $UserId }
Write-LogMessage -headers $Headers -API $APIName -message "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d" -Sev 'Info' -tenant $TenantFilter
return "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d"
} catch {
diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1
index ac19ef9039e2..198cf87143f9 100644
--- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1
+++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1
@@ -21,6 +21,7 @@ function Test-CIPPRerun {
'Standard' { 9800 } # 2 hours 45 minutes ish.
'BPA' { 85000 } # 24 hours ish.
'CippTests' { 85000 } # 24 hours ish.
+ 'Get-CIPPAlertQuarantineReleaseRequests' { 3500 } #about an hour
default { throw "Unknown type: $Type" }
}
}
diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1
index 1b3783d09304..d169ac04dc97 100644
--- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1
+++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1
@@ -16,13 +16,21 @@ function Invoke-CippTestZTNA21801 {
$PhishResistantMethods = @('passKeyDeviceBound', 'passKeyDeviceBoundAuthenticator', 'windowsHelloForBusiness')
- $results = $UserRegistrationDetails | Where-Object {
- $userId = $_.id
- $matchingUser = $Users | Where-Object { $_.id -eq $userId -and $_.accountEnabled }
- $matchingUser
- } | ForEach-Object {
- $regDetail = $_
- $matchingUser = $Users | Where-Object { $_.id -eq $regDetail.id }
+ # Create hashtable for O(1) user lookup instead of O(n) Where-Object searches
+ $UserLookup = @{}
+ foreach ($user in $Users) {
+ if ($user.accountEnabled) {
+ $UserLookup[$user.id] = $user
+ }
+ }
+
+ $results = foreach ($regDetail in $UserRegistrationDetails) {
+ # Fast O(1) lookup instead of Where-Object
+ if (-not $UserLookup.ContainsKey($regDetail.id)) {
+ continue
+ }
+
+ $matchingUser = $UserLookup[$regDetail.id]
$hasPhishResistant = $false
if ($regDetail.methodsRegistered) {
@@ -42,11 +50,11 @@ function Invoke-CippTestZTNA21801 {
}
}
- $totalUserCount = $results.Length
+ $totalUserCount = $results.Count
$phishResistantUsers = $results | Where-Object { $_.phishResistantAuthMethod }
$phishableUsers = $results | Where-Object { !$_.phishResistantAuthMethod }
- $phishResistantUserCount = $phishResistantUsers.Length
+ $phishResistantUserCount = $phishResistantUsers.Count
$passed = $totalUserCount -eq $phishResistantUserCount
@@ -64,24 +72,42 @@ function Invoke-CippTestZTNA21801 {
$mdInfo = "Found users that have not registered phishing resistant authentication methods.`n`n"
}
+ # Limit output to prevent performance issues with large user sets
+ $maxUsersToDisplay = 500
+
$mdInfo = $mdInfo + "| User | Last sign in | Phishing resistant method registered |`n"
$mdInfo = $mdInfo + "| :--- | :--- | :---: |`n"
$userLinkFormat = 'https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/UserAuthMethods/userId/{0}/hidePreviewBanner~/true'
- $mdLines = @($phishableUsers | Sort-Object displayName | ForEach-Object {
+ # Show phishable users first (up to limit)
+ $phishableUsersToShow = $phishableUsers | Sort-Object displayName | Select-Object -First $maxUsersToDisplay
+ $mdLines = @($phishableUsersToShow | ForEach-Object {
$userLink = $userLinkFormat -f $_.id
$lastSignInDate = if ($_.lastSuccessfulSignInDateTime) { (Get-Date $_.lastSuccessfulSignInDateTime -Format 'yyyy-MM-dd') } else { 'Never' }
"|[$($_.displayName)]($userLink)| $lastSignInDate | ❌ |`n"
})
$mdInfo = $mdInfo + ($mdLines -join '')
- $mdLines = @($phishResistantUsers | Sort-Object displayName | ForEach-Object {
- $userLink = $userLinkFormat -f $_.id
- $lastSignInDate = if ($_.lastSuccessfulSignInDateTime) { (Get-Date $_.lastSuccessfulSignInDateTime -Format 'yyyy-MM-dd') } else { 'Never' }
- "|[$($_.displayName)]($userLink)| $lastSignInDate | ✅ |`n"
- })
- $mdInfo = $mdInfo + ($mdLines -join '')
+ if ($phishableUsers.Count -gt $maxUsersToDisplay) {
+ $mdInfo = $mdInfo + "|... and $($phishableUsers.Count - $maxUsersToDisplay) more users without phish-resistant methods|||`n"
+ }
+
+ # Show phish-resistant users (up to remaining limit)
+ $remainingSlots = $maxUsersToDisplay - [Math]::Min($phishableUsers.Count, $maxUsersToDisplay)
+ if ($remainingSlots -gt 0) {
+ $phishResistantUsersToShow = $phishResistantUsers | Sort-Object displayName | Select-Object -First $remainingSlots
+ $mdLines = @($phishResistantUsersToShow | ForEach-Object {
+ $userLink = $userLinkFormat -f $_.id
+ $lastSignInDate = if ($_.lastSuccessfulSignInDateTime) { (Get-Date $_.lastSuccessfulSignInDateTime -Format 'yyyy-MM-dd') } else { 'Never' }
+ "|[$($_.displayName)]($userLink)| $lastSignInDate | ✅ |`n"
+ })
+ $mdInfo = $mdInfo + ($mdLines -join '')
+
+ if ($phishResistantUsers.Count -gt $remainingSlots) {
+ $mdInfo = $mdInfo + "|... and $($phishResistantUsers.Count - $remainingSlots) more users with phish-resistant methods|||`n"
+ }
+ }
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1
index cbd8229cf7fe..c33d4d006a53 100644
--- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1
+++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1
@@ -8,97 +8,107 @@ function Test-CIPPAuditLogRules {
)
try {
- # Helper function to map GUIDs and partner UPNs to user objects
+ # Pre-compiled regex patterns for GUID matching (performance optimization)
+ $script:StandardGuidRegex = [regex]'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
+ $script:PartnerUpnRegex = [regex]'user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)'
+ $script:PartnerExchangeRegex = [regex]'([^\\]+\.onmicrosoft\.com)\\tenant:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}),\s*object:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})'
+
+ # Helper function to map GUIDs and partner UPNs to user objects (Optimized with hashtable lookups)
function Add-CIPPGuidMappings {
param(
[Parameter(Mandatory = $true)]
$DataObject,
[Parameter(Mandatory = $true)]
- $Users,
+ $UserLookup,
[Parameter(Mandatory = $true)]
- $Groups,
+ $GroupLookup,
[Parameter(Mandatory = $true)]
- $Devices,
+ $DeviceLookup,
[Parameter(Mandatory = $true)]
- $ServicePrincipals,
+ $ServicePrincipalLookup,
[Parameter(Mandatory = $true)]
- $PartnerUsers,
+ $PartnerUserLookup,
[Parameter(Mandatory = $false)]
[string]$PropertyPrefix = ''
)
$DataObject.PSObject.Properties | ForEach-Object {
- # Check for standard GUID format OR partner UPN formats
- if ($_.Value -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' -or
- $_.Value -match 'user_[0-9a-f]{32}@[^@]+\.onmicrosoft\.com' -or
- $_.Value -match '[^\\]+\.onmicrosoft\.com\\tenant:\s*[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12},\s*object:\s*[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}') {
-
- # Use regex from guid-resolver hook to match various partner user formats
- # Format 1: user_@.onmicrosoft.com
- if ($_.Value -match 'user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)') {
- $hexId = $matches[1]
- $tenantDomain = $matches[2]
- if ($hexId.Length -eq 32) {
- # Convert the 32-character hex string to GUID format
- $guid = "$($hexId.Substring(0,8))-$($hexId.Substring(8,4))-$($hexId.Substring(12,4))-$($hexId.Substring(16,4))-$($hexId.Substring(20,12))"
- Write-Information "Found partner UPN format: $($_.Value) with GUID: $guid and tenant: $tenantDomain"
-
- # Check partner users for this GUID
- foreach ($PartnerUser in $PartnerUsers) {
- if ($PartnerUser.id -eq $guid) {
- $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $PartnerUser.userPrincipalName -Force -ErrorAction SilentlyContinue
- Write-Information "Mapped Partner User UPN: $($PartnerUser.userPrincipalName) to $PropertyPrefix$($_.Name)"
- return
- }
- }
+ $propValue = $_.Value
+
+ # Quick type check - skip if not string or empty
+ if ([string]::IsNullOrEmpty($propValue) -or $propValue -isnot [string]) {
+ return
+ }
+
+ # Check for partner UPN format 1: user_@.onmicrosoft.com
+ $match = $script:PartnerUpnRegex.Match($propValue)
+ if ($match.Success) {
+ $hexId = $match.Groups[1].Value
+ $tenantDomain = $match.Groups[2].Value
+ if ($hexId.Length -eq 32) {
+ # Convert hex string to GUID format
+ $guid = "$($hexId.Substring(0,8))-$($hexId.Substring(8,4))-$($hexId.Substring(12,4))-$($hexId.Substring(16,4))-$($hexId.Substring(20,12))"
+ Write-Information "Found partner UPN format: $propValue with GUID: $guid and tenant: $tenantDomain"
+
+ # O(1) hashtable lookup instead of O(n) loop
+ if ($PartnerUserLookup.ContainsKey($guid)) {
+ $PartnerUser = $PartnerUserLookup[$guid]
+ $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $PartnerUser.userPrincipalName -Force -ErrorAction SilentlyContinue
+ Write-Information "Mapped Partner User UPN: $($PartnerUser.userPrincipalName) to $PropertyPrefix$($_.Name)"
+ return
}
}
+ }
- # Format 2: TenantName.onmicrosoft.com\tenant: , object:
- if ($_.Value -match '([^\\]+\.onmicrosoft\.com)\\tenant:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}),\s*object:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})') {
- $customerTenantDomain = $matches[1]
- $partnerTenantGuid = $matches[2]
- $objectGuid = $matches[3]
- Write-Information "Found partner exchange format: customer tenant $customerTenantDomain, partner tenant $partnerTenantGuid, object $objectGuid"
-
- # Check partner users for this object GUID
- foreach ($PartnerUser in $PartnerUsers) {
- if ($PartnerUser.id -eq $objectGuid) {
- $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $PartnerUser.userPrincipalName -Force -ErrorAction SilentlyContinue
- Write-Information "Mapped Partner User UPN: $($PartnerUser.userPrincipalName) to $PropertyPrefix$($_.Name)"
- return
- }
- }
+ # Check for partner exchange format: TenantName.onmicrosoft.com\tenant: , object:
+ $match = $script:PartnerExchangeRegex.Match($propValue)
+ if ($match.Success) {
+ $customerTenantDomain = $match.Groups[1].Value
+ $partnerTenantGuid = $match.Groups[2].Value
+ $objectGuid = $match.Groups[3].Value
+ Write-Information "Found partner exchange format: customer tenant $customerTenantDomain, partner tenant $partnerTenantGuid, object $objectGuid"
+
+ # O(1) hashtable lookup
+ if ($PartnerUserLookup.ContainsKey($objectGuid)) {
+ $PartnerUser = $PartnerUserLookup[$objectGuid]
+ $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $PartnerUser.userPrincipalName -Force -ErrorAction SilentlyContinue
+ Write-Information "Mapped Partner User UPN: $($PartnerUser.userPrincipalName) to $PropertyPrefix$($_.Name)"
+ return
}
+ }
- # Check standard directory objects (users, groups, devices, service principals)
- foreach ($User in $Users) {
- if ($User.id -eq $_.Value) {
- $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $User.userPrincipalName -Force -ErrorAction SilentlyContinue
- Write-Information "Mapped User: $($User.userPrincipalName) to $PropertyPrefix$($_.Name)"
- return
- }
+ # Check for standard GUID format
+ if ($script:StandardGuidRegex.IsMatch($propValue)) {
+ $guid = $propValue
+
+ # O(1) hashtable lookups in priority order
+ if ($UserLookup.ContainsKey($guid)) {
+ $User = $UserLookup[$guid]
+ $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $User.userPrincipalName -Force -ErrorAction SilentlyContinue
+ Write-Information "Mapped User: $($User.userPrincipalName) to $PropertyPrefix$($_.Name)"
+ return
}
- foreach ($Group in $Groups) {
- if ($Group.id -eq $_.Value) {
- $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $Group -Force -ErrorAction SilentlyContinue
- Write-Information "Mapped Group: $($Group.displayName) to $PropertyPrefix$($_.Name)"
- return
- }
+
+ if ($GroupLookup.ContainsKey($guid)) {
+ $Group = $GroupLookup[$guid]
+ $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $Group -Force -ErrorAction SilentlyContinue
+ Write-Information "Mapped Group: $($Group.displayName) to $PropertyPrefix$($_.Name)"
+ return
}
- foreach ($Device in $Devices) {
- if ($Device.id -eq $_.Value) {
- $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $Device -Force -ErrorAction SilentlyContinue
- Write-Information "Mapped Device: $($Device.displayName) to $PropertyPrefix$($_.Name)"
- return
- }
+
+ if ($DeviceLookup.ContainsKey($guid)) {
+ $Device = $DeviceLookup[$guid]
+ $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $Device -Force -ErrorAction SilentlyContinue
+ Write-Information "Mapped Device: $($Device.displayName) to $PropertyPrefix$($_.Name)"
+ return
}
- foreach ($ServicePrincipal in $ServicePrincipals) {
- if ($ServicePrincipal.id -eq $_.Value -or $ServicePrincipal.appId -eq $_.Value) {
- $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $ServicePrincipal -Force -ErrorAction SilentlyContinue
- Write-Information "Mapped Service Principal: $($ServicePrincipal.displayName) to $PropertyPrefix$($_.Name)"
- return
- }
+
+ # ServicePrincipal indexed by both id and appId
+ if ($ServicePrincipalLookup.ContainsKey($guid)) {
+ $ServicePrincipal = $ServicePrincipalLookup[$guid]
+ $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $ServicePrincipal -Force -ErrorAction SilentlyContinue
+ Write-Information "Mapped Service Principal: $($ServicePrincipal.displayName) to $PropertyPrefix$($_.Name)"
+ return
}
}
}
@@ -151,7 +161,31 @@ function Test-CIPPAuditLogRules {
$Table = Get-CIPPTable -tablename 'cacheauditloglookups'
$1dayago = (Get-Date).AddDays(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
$Lookups = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$TenantFilter' and Timestamp gt datetime'$1dayago'"
- if (!$Lookups) {
+
+ # Check if cached data needs refresh (wrong format or corrupted)
+ $NeedsRefresh = $false
+ if ($Lookups) {
+ try {
+ # Test if we can parse the cached data
+ $TestUser = ($Lookups | Where-Object { $_.RowKey -eq 'users' }).Data
+ if (![string]::IsNullOrEmpty($TestUser)) {
+ $ParsedTest = $TestUser | ConvertFrom-Json -ErrorAction Stop
+ # Check if data is valid (either array for legacy or PSCustomObject for hashtable)
+ if ($null -eq $ParsedTest) {
+ Write-Warning 'Cached data is null after parsing, triggering refresh'
+ $NeedsRefresh = $true
+ }
+ } else {
+ Write-Warning 'Cached data is empty, triggering refresh'
+ $NeedsRefresh = $true
+ }
+ } catch {
+ Write-Warning "Error parsing cached data: $($_.Exception.Message), triggering refresh"
+ $NeedsRefresh = $true
+ }
+ }
+
+ if (!$Lookups -or $NeedsRefresh) {
# Collect bulk data for users/groups/devices/applications
$Requests = @(
@{
@@ -180,44 +214,145 @@ function Test-CIPPAuditLogRules {
$Groups = ($Response | Where-Object { $_.id -eq 'groups' }).body.value ?? @()
$Devices = ($Response | Where-Object { $_.id -eq 'devices' }).body.value ?? @()
$ServicePrincipals = ($Response | Where-Object { $_.id -eq 'servicePrincipals' }).body.value ?? @()
- # Cache the lookups for 1 day
+
+ # Build hashtables for O(1) GUID lookups
+ Write-Information "Building hashtable lookups for tenant $TenantFilter"
+ $UserLookup = @{}
+ foreach ($User in $Users) {
+ if (![string]::IsNullOrEmpty($User.id)) {
+ $UserLookup[$User.id] = $User
+ }
+ }
+
+ $GroupLookup = @{}
+ foreach ($Group in $Groups) {
+ if (![string]::IsNullOrEmpty($Group.id)) {
+ $GroupLookup[$Group.id] = $Group
+ }
+ }
+
+ $DeviceLookup = @{}
+ foreach ($Device in $Devices) {
+ if (![string]::IsNullOrEmpty($Device.id)) {
+ $DeviceLookup[$Device.id] = $Device
+ }
+ }
+
+ $ServicePrincipalLookup = @{}
+ foreach ($SP in $ServicePrincipals) {
+ if (![string]::IsNullOrEmpty($SP.id)) {
+ $ServicePrincipalLookup[$SP.id] = $SP
+ }
+ # Also index by appId for dual lookup capability
+ if (![string]::IsNullOrEmpty($SP.appId)) {
+ $ServicePrincipalLookup[$SP.appId] = $SP
+ }
+ }
+ Write-Information "Built hashtables: $($UserLookup.Count) users, $($GroupLookup.Count) groups, $($DeviceLookup.Count) devices, $($ServicePrincipalLookup.Count) service principals"
+
+ # Cache the hashtable lookups for 1 day (storing as JSON)
$Entities = @(
@{
PartitionKey = $TenantFilter
RowKey = 'users'
- Data = [string]($Users | ConvertTo-Json -Compress)
+ Data = [string]($UserLookup | ConvertTo-Json -Compress)
+ Format = 'hashtable'
}
@{
PartitionKey = $TenantFilter
RowKey = 'groups'
- Data = [string]($Groups | ConvertTo-Json -Compress)
+ Data = [string]($GroupLookup | ConvertTo-Json -Compress)
+ Format = 'hashtable'
}
@{
PartitionKey = $TenantFilter
RowKey = 'devices'
- Data = [string]($Devices | ConvertTo-Json -Compress)
+ Data = [string]($DeviceLookup | ConvertTo-Json -Compress)
+ Format = 'hashtable'
}
@{
PartitionKey = $TenantFilter
RowKey = 'servicePrincipals'
- Data = [string]($ServicePrincipals | ConvertTo-Json -Compress)
+ Data = [string]($ServicePrincipalLookup | ConvertTo-Json -Compress)
+ Format = 'hashtable'
}
)
# Save the cached lookups
Add-CIPPAzDataTableEntity @Table -Entity $Entities -Force
- Write-Information "Cached directory lookups for tenant $TenantFilter"
+ Write-Information "Cached directory hashtable lookups for tenant $TenantFilter"
} else {
- # Use cached lookups
- $Users = (($Lookups | Where-Object { $_.RowKey -eq 'users' }).Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @()
- $Groups = (($Lookups | Where-Object { $_.RowKey -eq 'groups' }).Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @()
- $Devices = (($Lookups | Where-Object { $_.RowKey -eq 'devices' }).Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @()
- $ServicePrincipals = (($Lookups | Where-Object { $_.RowKey -eq 'servicePrincipals' }).Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @()
- Write-Information "Using cached directory lookups for tenant $TenantFilter"
+ # Use cached lookups - check if they're already hashtables or need conversion
+ $UsersLookup = $Lookups | Where-Object { $_.RowKey -eq 'users' }
+ $GroupsLookup = $Lookups | Where-Object { $_.RowKey -eq 'groups' }
+ $DevicesLookup = $Lookups | Where-Object { $_.RowKey -eq 'devices' }
+ $ServicePrincipalsLookup = $Lookups | Where-Object { $_.RowKey -eq 'servicePrincipals' }
+
+ # Check if cached data is already in hashtable format
+ $IsHashtableFormat = $UsersLookup.Format -eq 'hashtable'
+
+ if ($IsHashtableFormat) {
+ # Load pre-built hashtables directly from cache
+ Write-Information "Loading pre-built hashtable lookups from cache for tenant $TenantFilter"
+ $UserLookup = ($UsersLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue -AsHashtable) ?? @{}
+ $GroupLookup = ($GroupsLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue -AsHashtable) ?? @{}
+ $DeviceLookup = ($DevicesLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue -AsHashtable) ?? @{}
+ $ServicePrincipalLookup = ($ServicePrincipalsLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue -AsHashtable) ?? @{}
+ Write-Information "Loaded hashtables: $($UserLookup.Count) users, $($GroupLookup.Count) groups, $($DeviceLookup.Count) devices, $($ServicePrincipalLookup.Count) service principals"
+ } else {
+ # Old format (array) - convert to hashtables
+ Write-Information "Converting legacy array cache to hashtables for tenant $TenantFilter"
+ $Users = ($UsersLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @()
+ $Groups = ($GroupsLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @()
+ $Devices = ($DevicesLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @()
+ $ServicePrincipals = ($ServicePrincipalsLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @()
+
+ # Build hashtables
+ $UserLookup = @{}
+ foreach ($User in $Users) {
+ if (![string]::IsNullOrEmpty($User.id)) {
+ $UserLookup[$User.id] = $User
+ }
+ }
+
+ $GroupLookup = @{}
+ foreach ($Group in $Groups) {
+ if (![string]::IsNullOrEmpty($Group.id)) {
+ $GroupLookup[$Group.id] = $Group
+ }
+ }
+
+ $DeviceLookup = @{}
+ foreach ($Device in $Devices) {
+ if (![string]::IsNullOrEmpty($Device.id)) {
+ $DeviceLookup[$Device.id] = $Device
+ }
+ }
+
+ $ServicePrincipalLookup = @{}
+ foreach ($SP in $ServicePrincipals) {
+ if (![string]::IsNullOrEmpty($SP.id)) {
+ $ServicePrincipalLookup[$SP.id] = $SP
+ }
+ if (![string]::IsNullOrEmpty($SP.appId)) {
+ $ServicePrincipalLookup[$SP.appId] = $SP
+ }
+ }
+ Write-Information "Built hashtables from legacy cache: $($UserLookup.Count) users, $($GroupLookup.Count) groups, $($DeviceLookup.Count) devices, $($ServicePrincipalLookup.Count) service principals"
+ }
}
# partner users
$PartnerUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,displayName,userPrincipalName,accountEnabled&`$top=999" -AsApp $true -NoAuthCheck $true
+ # Build partner user hashtable
+ $PartnerUserLookup = @{}
+ foreach ($PartnerUser in $PartnerUsers) {
+ if (![string]::IsNullOrEmpty($PartnerUser.id)) {
+ $PartnerUserLookup[$PartnerUser.id] = $PartnerUser
+ }
+ }
+ Write-Information "Built partner user hashtable: $($PartnerUserLookup.Count) partner users"
+
Write-Warning '## Audit Log Configuration ##'
Write-Information ($Configuration | ConvertTo-Json -Depth 10)
@@ -245,13 +380,13 @@ function Test-CIPPAuditLogRules {
$RootProperties = $AuditRecord
$Data = $AuditRecord.auditData | Select-Object *, CIPPAction, CIPPClause, CIPPGeoLocation, CIPPBadRepIP, CIPPHostedIP, CIPPIPDetected, CIPPLocationInfo, CIPPExtendedProperties, CIPPDeviceProperties, CIPPParameters, CIPPModifiedProperties, AuditRecord -ErrorAction SilentlyContinue
try {
- # Attempt to locate GUIDs in $Data and match them with their corresponding user, group, device, or service principal recursively by checking each key/value once located lets store these mapped values in a CIPP$KeyName property
+ # Attempt to locate GUIDs in $Data and match them with their corresponding user, group, device, or service principal using O(1) hashtable lookups
Write-Information 'Checking Data for GUIDs to map to users, groups, devices, or service principals'
- Add-CIPPGuidMappings -DataObject $Data -Users $Users -Groups $Groups -Devices $Devices -ServicePrincipals $ServicePrincipals -PartnerUsers $PartnerUsers -PropertyPrefix 'CIPP'
+ Add-CIPPGuidMappings -DataObject $Data -UserLookup $UserLookup -GroupLookup $GroupLookup -DeviceLookup $DeviceLookup -ServicePrincipalLookup $ServicePrincipalLookup -PartnerUserLookup $PartnerUserLookup -PropertyPrefix 'CIPP'
# Also check root properties for GUIDs and partner UPNs
Write-Information 'Checking RootProperties for GUIDs to map to users, groups, devices, or service principals'
- Add-CIPPGuidMappings -DataObject $RootProperties -Users $Users -Groups $Groups -Devices $Devices -ServicePrincipals $ServicePrincipals -PartnerUsers $PartnerUsers
+ Add-CIPPGuidMappings -DataObject $RootProperties -UserLookup $UserLookup -GroupLookup $GroupLookup -DeviceLookup $DeviceLookup -ServicePrincipalLookup $ServicePrincipalLookup -PartnerUserLookup $PartnerUserLookup
if ($Data.ExtendedProperties) {
$Data.CIPPExtendedProperties = ($Data.ExtendedProperties | ConvertTo-Json -Compress)
@@ -432,6 +567,7 @@ function Test-CIPPAuditLogRules {
try {
$ClauseStartTime = Get-Date
Write-Warning "Webhook: Processing clause: $($clause.clause)"
+ Write-Information "Webhook: Available operations in data: $(($ProcessedData.Operation | Select-Object -Unique) -join ', ')"
$ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause }
if ($ReturnedData) {
Write-Warning "Webhook: There is matching data: $(($ReturnedData.operation | Select-Object -Unique) -join ', ')"
diff --git a/host.json b/host.json
index a581a9789068..97957cbf1ed7 100644
--- a/host.json
+++ b/host.json
@@ -16,7 +16,7 @@
"distributedTracingEnabled": false,
"version": "None"
},
- "defaultVersion": "10.0.7",
+ "defaultVersion": "10.0.8",
"versionMatchStrategy": "Strict",
"versionFailureStrategy": "Fail"
}
diff --git a/version_latest.txt b/version_latest.txt
index 9380cfccb8c7..5219a0df74ba 100644
--- a/version_latest.txt
+++ b/version_latest.txt
@@ -1 +1 @@
-10.0.7
+10.0.8