diff --git a/.github/workflows/master_cippjiuus.yml b/.github/workflows/master_cippjiuus.yml deleted file mode 100644 index 6b404e985583..000000000000 --- a/.github/workflows/master_cippjiuus.yml +++ /dev/null @@ -1,30 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Build and deploy Powershell project to Azure Function App - cippjiuus - -on: - push: - branches: - - master - workflow_dispatch: - -env: - AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Run Azure Functions Action' - uses: Azure/functions-action@v1 - id: fa - with: - app-name: 'cippjiuus' - slot-name: 'Production' - package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_E2C0118215FF474E9A6386578DD008AD }} \ No newline at end of file diff --git a/Config/ExcludeSkuList.JSON b/Config/ExcludeSkuList.JSON index e2c80e82a1e0..ec0dfb32d4a9 100644 --- a/Config/ExcludeSkuList.JSON +++ b/Config/ExcludeSkuList.JSON @@ -3,42 +3,10 @@ "GUID": "90d8b3f8-712e-4f7b-aa1e-62e7ae6cbe96", "Product_Display_Name": "Business Apps (free)" }, - { - "GUID": "90d8b3f8-712e-4f7b-aa1e-62e7ae6cbe96", - "Product_Display_Name": "Business Apps (free)" - }, - { - "GUID": "f30db892-07e9-47e9-837c-80727f46fd3d", - "Product_Display_Name": "MICROSOFT FLOW FREE" - }, { "GUID": "f30db892-07e9-47e9-837c-80727f46fd3d", "Product_Display_Name": "MICROSOFT FLOW FREE" }, - { - "GUID": "f30db892-07e9-47e9-837c-80727f46fd3d", - "Product_Display_Name": "MICROSOFT FLOW FREE" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, { "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", "Product_Display_Name": "MICROSOFT TEAMS (FREE)" @@ -47,10 +15,6 @@ "GUID": "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235", "Product_Display_Name": "Power BI (free)" }, - { - "GUID": "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235", - "Product_Display_Name": "Power BI (free)" - }, { "GUID": "61e6bd70-fbdb-4deb-82ea-912842f39431", "Product_Display_Name": "Dynamics 365 Customer Service Insights Trial" @@ -59,26 +23,6 @@ "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", "Product_Display_Name": "Dynamics 365 Customer Voice Trial" }, - { - "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", - "Product_Display_Name": "Dynamics 365 Customer Voice Trial" - }, - { - "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", - "Product_Display_Name": "Dynamics 365 Customer Voice Trial" - }, - { - "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", - "Product_Display_Name": "Dynamics 365 Customer Voice Trial" - }, - { - "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", - "Product_Display_Name": "Dynamics 365 Customer Voice Trial" - }, - { - "GUID": "338148b6-1b11-4102-afb9-f92b6cdc0f8d", - "Product_Display_Name": "DYNAMICS 365 P1 TRIAL FOR INFORMATION WORKERS" - }, { "GUID": "338148b6-1b11-4102-afb9-f92b6cdc0f8d", "Product_Display_Name": "DYNAMICS 365 P1 TRIAL FOR INFORMATION WORKERS" @@ -87,26 +31,6 @@ "GUID": "fcecd1f9-a91e-488d-a918-a96cdb6ce2b0", "Product_Display_Name": "Microsoft Dynamics AX7 User Trial" }, - { - "GUID": "fcecd1f9-a91e-488d-a918-a96cdb6ce2b0", - "Product_Display_Name": "Microsoft Dynamics AX7 User Trial" - }, - { - "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", - "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" - }, - { - "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", - "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" - }, - { - "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", - "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" - }, - { - "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", - "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" - }, { "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" @@ -116,71 +40,59 @@ "Product_Display_Name": "Microsoft Teams Trial" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" - }, - { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" - }, - { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" - }, - { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", + "Product_Display_Name": "Power Virtual Agents Viral Trial" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "1f2f344a-700d-42c9-9427-5cea1d5d7ba6", + "Product_Display_Name": "MICROSOFT STREAM" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "6470687e-a428-4b7a-bef2-8a291ad947c9", + "Product_Display_Name": "WINDOWS STORE FOR BUSINESS" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "710779e8-3d4a-4c88-adb9-386c958d1fdf", + "Product_Display_Name": "MICROSOFT TEAMS EXPLORATORY" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "8c4ce438-32a7-4ac5-91a6-e22ae08d9c8b", + "Product_Display_Name": "Rights Management Adhoc" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "5b631642-bd26-49fe-bd20-1daaa972ef80", + "Product_Display_Name": "Microsoft Power Apps for Developer" }, { - "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", - "Product_Display_Name": "Power Virtual Agents Viral Trial" + "GUID": "6a4a1628-9b9a-424d-bed5-4118f0ede3fd", + "Product_Display_Name": "Dynamics 365 Business Central for IWs" }, { - "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", - "Product_Display_Name": "Power Virtual Agents Viral Trial" + "GUID": "6ec92958-3cc1-49db-95bd-bc6b3798df71", + "Product_Display_Name": "Dynamics 365 Sales Premium Viral Trial" }, { - "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", - "Product_Display_Name": "Power Virtual Agents Viral Trial" + "GUID": "3f9f06f5-3c31-472c-985f-62d9c10ec167", + "Product_Display_Name": "Power Pages vTrial for Makers" }, { - "GUID": "1f2f344a-700d-42c9-9427-5cea1d5d7ba6", - "Product_Display_Name": "MICROSOFT STREAM" + "GUID": "9c7bff7a-3715-4da7-88d3-07f57f8d0fb6", + "Product_Display_Name": "Dynamics 365 For Sales Professional Trial" }, { - "GUID": "1f2f344a-700d-42c9-9427-5cea1d5d7ba6", - "Product_Display_Name": "MICROSOFT STREAM" + "GUID": "8f0c5670-4e56-4892-b06d-91c085d7004f", + "Product_Display_Name": "App Connect IW" }, { - "GUID": "6470687e-a428-4b7a-bef2-8a291ad947c9", - "Product_Display_Name": "WINDOWS STORE FOR BUSINESS" + "GUID": "87bbbc60-4754-4998-8c88-227dca264858", + "Product_Display_Name": "Power Apps and Logic Flows" }, { - "GUID": "6470687e-a428-4b7a-bef2-8a291ad947c9", - "Product_Display_Name": "WINDOWS STORE FOR BUSINESS" + "GUID": "e5788282-6381-469f-84f0-3d7d4021d34d", + "Product_Display_Name": "Office 365 Extra File Storage for GCC" }, { - "GUID": "710779e8-3d4a-4c88-adb9-386c958d1fdf", - "Product_Display_Name": "MICROSOFT TEAMS EXPLORATORY" + "GUID": "99049c9c-6011-4908-bf17-15f496e6519d", + "Product_Display_Name": "Office 365 Extra File Storage" } ] diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 index f2c268a4ed75..31811b78ed68 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 @@ -24,11 +24,6 @@ function Get-CIPPAlertMXRecordChanged { "$($Domain.Domain): MX records changed from [$($PreviousRecords -join ', ')] to [$($CurrentRecords -join ', ')]" } } - - if ($ChangedDomains) { - Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $ChangedDomains - } - # Update cache with current data foreach ($Domain in $DomainData) { $CurrentRecords = $Domain.ActualMXRecords.Hostname | Sort-Object @@ -42,6 +37,12 @@ function Get-CIPPAlertMXRecordChanged { } Add-CIPPAzDataTableEntity @CacheTable -Entity $CacheEntity -Force } + + if ($ChangedDomains) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $ChangedDomains + } + return $true + } catch { Write-LogMessage -message "Failed to check MX record changes: $($_.Exception.Message)" -API 'MX Record Alert' -tenant $TenantFilter -sev Error } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 new file mode 100644 index 000000000000..4bf49b56bbc7 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 @@ -0,0 +1,46 @@ + +function Get-CippAlertSecureScore { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + try { + $SecureScore = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/security/secureScores?$top=1' -tenantid $TenantFilter -noPagination $true + if ($InputValue.ThresholdType.value -eq "absolute") { + if ($SecureScore.currentScore -lt $InputValue.InputValue) { + $SecureScoreResult = [PSCustomObject]@{ + Message = "Secure Score is below acceptable threshold" + Tenant = $TenantFilter + CurrentScore = $SecureScore.currentScore + MaxSecureScore = $SecureScore.maxScore + } + } else { + $SecureScoreResult = @() + } + } elseif ($InputValue.ThresholdType.value -eq "percent") { + $PercentageScore = [math]::Round((($SecureScore.currentScore / $SecureScore.maxScore) * 100),2) + if ($PercentageScore -lt $InputValue.InputValue) { + $SecureScoreResult = [PSCustomObject]@{ + Message = "Secure Score is below acceptable threshold" + Tenant = $TenantFilter + CurrentScore = $SecureScore.currentScore + MaxScore = $SecureScore.maxScore + CurrentScorePercentage = [math]::Round($PercentageScore,2) + ScoreThresholdPercentage = $InputValue.InputValue + } + } else { + $SecureScoreResult = @() + } + } + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $SecureScoreResult -PartitionKey SecureScore + } catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Could not get Secure Score for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index 20f5464d1bfa..616f0ef600a9 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -29,7 +29,8 @@ function Compare-CIPPIntuneObject { 'qualityUpdatesPauseStartDate', 'featureUpdatesPauseStartDate' 'wslDistributions', - 'lastSuccessfulSyncDateTime' + 'lastSuccessfulSyncDateTime', + 'tenantFilter' ) $excludeProps = $defaultExcludeProperties + $ExcludeProperties @@ -58,13 +59,6 @@ function Compare-CIPPIntuneObject { [int]$MaxDepth = 20 ) - # Check for arrays at the start of every recursive call - this catches arrays at any nesting level - $isObj1Array = $Object1 -is [Array] -or $Object1 -is [System.Collections.IList] - $isObj2Array = $Object2 -is [Array] -or $Object2 -is [System.Collections.IList] - if ($isObj1Array -or $isObj2Array) { - return - } - if ($Depth -ge $MaxDepth) { $result.Add([PSCustomObject]@{ Property = $PropertyPath @@ -97,7 +91,7 @@ function Compare-CIPPIntuneObject { } # Short-circuit recursion for primitive types - $primitiveTypes = @([double], [decimal], [datetime], [timespan], [guid] ) + $primitiveTypes = @([string], [int], [long], [bool], [double], [decimal], [datetime], [timespan], [guid] ) foreach ($type in $primitiveTypes) { if ($Object1 -is $type -and $Object2 -is $type) { if ($Object1 -ne $Object2) { @@ -166,7 +160,7 @@ function Compare-CIPPIntuneObject { if ($isObj1Array -or $isObj2Array) { return } - + # Safely get property names - ensure objects are not arrays before accessing PSObject.Properties $allPropertyNames = @() try { @@ -202,7 +196,7 @@ function Compare-CIPPIntuneObject { if ($prop1Exists -and $prop2Exists) { try { # Double-check arrays before accessing properties - if (($Object1 -is [Array] -or $Object1 -is [System.Collections.IList]) -or + if (($Object1 -is [Array] -or $Object1 -is [System.Collections.IList]) -or ($Object2 -is [Array] -or $Object2 -is [System.Collections.IList])) { continue } @@ -316,11 +310,11 @@ function Compare-CIPPIntuneObject { } $results.Add([PSCustomObject]@{ - Key = "GroupChild-$($child.settingDefinitionId)" - Label = $childLabel - Value = $childValue - Source = $Source - }) + Key = "GroupChild-$($child.settingDefinitionId)" + Label = $childLabel + Value = $childValue + Source = $Source + }) } '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' { $childValue = $null @@ -329,11 +323,11 @@ function Compare-CIPPIntuneObject { } $results.Add([PSCustomObject]@{ - Key = "GroupChild-$($child.settingDefinitionId)" - Label = $childLabel - Value = $childValue - Source = $Source - }) + Key = "GroupChild-$($child.settingDefinitionId)" + Label = $childLabel + Value = $childValue + Source = $Source + }) } '#microsoft.graph.deviceManagementConfigurationChoiceSettingCollectionInstance' { if ($child.choiceSettingCollectionValue) { @@ -352,11 +346,11 @@ function Compare-CIPPIntuneObject { $childValue = $values -join ', ' $results.Add([PSCustomObject]@{ - Key = "GroupChild-$($child.settingDefinitionId)" - Label = $childLabel - Value = $childValue - Source = $Source - }) + Key = "GroupChild-$($child.settingDefinitionId)" + Label = $childLabel + Value = $childValue + Source = $Source + }) } } '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance' { @@ -368,11 +362,11 @@ function Compare-CIPPIntuneObject { $childValue = $values -join ', ' $results.Add([PSCustomObject]@{ - Key = "GroupChild-$($child.settingDefinitionId)" - Label = $childLabel - Value = $childValue - Source = $Source - }) + Key = "GroupChild-$($child.settingDefinitionId)" + Label = $childLabel + Value = $childValue + Source = $Source + }) } } default { diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetCalendarPermissionsBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetCalendarPermissionsBatch.ps1 new file mode 100644 index 000000000000..5c98bacd6aec --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetCalendarPermissionsBatch.ps1 @@ -0,0 +1,68 @@ +function Push-GetCalendarPermissionsBatch { + <# + .SYNOPSIS + Process a batch of calendar permission queries + + .DESCRIPTION + Queries calendar permissions for a batch of mailboxes + + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + $TenantFilter = $Item.TenantFilter + $Mailboxes = $Item.Mailboxes + $BatchNumber = $Item.BatchNumber + $TotalBatches = $Item.TotalBatches + + try { + Write-Information "Processing calendar permissions batch $BatchNumber of $TotalBatches for tenant $TenantFilter with $($Mailboxes.Count) mailboxes" + + $AllCalendarPermissions = [System.Collections.Generic.List[object]]::new() + + foreach ($MailboxUPN in $Mailboxes) { + try { + # Step 1: Get the calendar folder name (locale-specific) + $GetCalParam = @{Identity = $MailboxUPN; FolderScope = 'Calendar' } + $CalendarFolder = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -anchor $MailboxUPN -cmdParams $GetCalParam | Select-Object -First 1 + + if ($CalendarFolder -and $CalendarFolder.name) { + # Step 2: Get calendar permissions using the folder name + $CalParam = @{Identity = "$($MailboxUPN):\$($CalendarFolder.name)" } + $CalendarPermissions = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderPermission' -anchor $MailboxUPN -cmdParams $CalParam -UseSystemMailbox $true + + # Normalize the results + foreach ($Perm in $CalendarPermissions) { + $AllCalendarPermissions.Add([PSCustomObject]@{ + id = [guid]::NewGuid().ToString() + Identity = $Perm.Identity + User = $Perm.User + AccessRights = $Perm.AccessRights + FolderName = $Perm.FolderName + }) + } + } else { + Write-Information "No calendar folder found for mailbox $MailboxUPN" + } + } catch { + Write-Information "Failed to get calendar permissions for $MailboxUPN : $($_.Exception.Message)" + # Continue processing other mailboxes + } + } + + Write-Information "Completed calendar permissions batch $BatchNumber of $TotalBatches - processed $($Mailboxes.Count) mailboxes: $($AllCalendarPermissions.Count) calendar permissions" + + # Return results grouped by command type for consistency with mailbox permissions + return @{ + 'Get-MailboxFolderPermission' = $AllCalendarPermissions + } + + } catch { + $ErrorMsg = "Failed to process calendar permissions batch $BatchNumber of $TotalBatches for tenant $TenantFilter : $($_.Exception.Message)" + Write-Information "ERROR in Push-GetCalendarPermissionsBatch: $ErrorMsg" + Write-Information "Stack trace: $($_.ScriptStackTrace)" + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message $ErrorMsg -sev Error + throw $ErrorMsg + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 index fc5a967664b3..911745c1b06b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 @@ -1,7 +1,7 @@ function Push-StoreMailboxPermissions { <# .SYNOPSIS - Post-execution function to aggregate and store all mailbox permissions + Post-execution function to aggregate and store all mailbox and calendar permissions .DESCRIPTION Collects results from all batches and stores them in the reporting database @@ -16,7 +16,7 @@ function Push-StoreMailboxPermissions { $Results = $Item.Results try { - Write-Information "Storing mailbox permissions for tenant $TenantFilter" + Write-Information "Storing mailbox and calendar permissions for tenant $TenantFilter" Write-Information "Received $($Results.Count) batch results" # Log each result for debugging @@ -28,6 +28,7 @@ function Push-StoreMailboxPermissions { # Aggregate results by command type from all batches $AllMailboxPermissions = [System.Collections.Generic.List[object]]::new() $AllRecipientPermissions = [System.Collections.Generic.List[object]]::new() + $AllCalendarPermissions = [System.Collections.Generic.List[object]]::new() foreach ($BatchResult in $Results) { # Activity functions may return an array [hashtable, "status message"] @@ -49,17 +50,22 @@ function Push-StoreMailboxPermissions { Write-Information "Adding $($ActualResult['Get-RecipientPermission'].Count) recipient permissions" $AllRecipientPermissions.AddRange($ActualResult['Get-RecipientPermission']) } + if ($ActualResult['Get-MailboxFolderPermission']) { + Write-Information "Adding $($ActualResult['Get-MailboxFolderPermission'].Count) calendar permissions" + $AllCalendarPermissions.AddRange($ActualResult['Get-MailboxFolderPermission']) + } } else { Write-Information "Skipping non-hashtable result: $($ActualResult.GetType().Name)" } } -# Combine all permissions (mailbox and recipient) into a single collection + # Combine all permissions (mailbox and recipient) into a single collection $AllPermissions = [System.Collections.Generic.List[object]]::new() $AllPermissions.AddRange($AllMailboxPermissions) $AllPermissions.AddRange($AllRecipientPermissions) Write-Information "Aggregated $($AllPermissions.Count) total permissions ($($AllMailboxPermissions.Count) mailbox + $($AllRecipientPermissions.Count) recipient)" + Write-Information "Aggregated $($AllCalendarPermissions.Count) calendar permissions" # Store all permissions together as MailboxPermissions if ($AllPermissions.Count -gt 0) { @@ -67,7 +73,16 @@ function Push-StoreMailboxPermissions { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllPermissions.Count) mailbox permission records" -sev Info } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No permissions found to cache' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailbox permissions found to cache' -sev Info + } + + # Store calendar permissions separately + if ($AllCalendarPermissions.Count -gt 0) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllCalendarPermissions.Count) calendar permission records" -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No calendar permissions found to cache' -sev Info } return diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index d2c0f155009e..69c88b742398 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -1,24 +1,22 @@ function Push-CIPPDBCacheData { <# .SYNOPSIS - Activity function to collect and cache all data for a single tenant + Orchestrator function to collect and cache all data for a single tenant .DESCRIPTION - Calls all collection functions sequentially, storing data immediately after each collection + Builds a dynamic batch of cache collection tasks based on tenant license capabilities .FUNCTIONALITY Entrypoint #> [CmdletBinding()] param($Item) - Write-Host "Starting cache collection for tenant: $($Item.TenantFilter) - Queue: $($Item.QueueName) (ID: $($Item.QueueId))" + Write-Host "Starting cache collection orchestration for tenant: $($Item.TenantFilter) - Queue: $($Item.QueueName) (ID: $($Item.QueueId))" $TenantFilter = $Item.TenantFilter - $Type = $Item.Type ?? 'Default' + $QueueId = $Item.QueueId - #This collects all data for a tenant and caches it in the CIPP Reporting database. DO NOT ADD PROCESSING OR LOGIC HERE. - #The point of this file is to always be <10 minutes execution time. try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Starting database cache collection for tenant' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Starting database cache orchestration for tenant' -sev Info # Check tenant capabilities for license-specific features $IntuneCapable = Test-CIPPStandardLicense -StandardName 'IntuneLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog @@ -28,365 +26,183 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License capabilities - Intune: $IntuneCapable, Conditional Access: $ConditionalAccessCapable, Azure AD Premium P2: $AzureADPremiumP2Capable, Exchange: $ExchangeCapable" -sev Info - switch ($Type) { - 'Default' { - #region All Licenses - Basic tenant data collection - Write-Host 'Getting cache for Users' - try { Set-CIPPDBCacheUsers -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Users collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Groups' - try { Set-CIPPDBCacheGroups -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Groups collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Guests' - try { Set-CIPPDBCacheGuests -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Guests collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ServicePrincipals' - try { Set-CIPPDBCacheServicePrincipals -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipals collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Apps' - try { Set-CIPPDBCacheApps -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Apps collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Devices' - try { Set-CIPPDBCacheDevices -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Devices collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Organization' - try { Set-CIPPDBCacheOrganization -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Organization collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Roles' - try { Set-CIPPDBCacheRoles -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Roles collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AdminConsentRequestPolicy' - try { Set-CIPPDBCacheAdminConsentRequestPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AdminConsentRequestPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthorizationPolicy' - try { Set-CIPPDBCacheAuthorizationPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthorizationPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthenticationMethodsPolicy' - try { Set-CIPPDBCacheAuthenticationMethodsPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationMethodsPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DeviceSettings' - try { Set-CIPPDBCacheDeviceSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceSettings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DirectoryRecommendations' - try { Set-CIPPDBCacheDirectoryRecommendations -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DirectoryRecommendations collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for CrossTenantAccessPolicy' - try { Set-CIPPDBCacheCrossTenantAccessPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CrossTenantAccessPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DefaultAppManagementPolicy' - try { Set-CIPPDBCacheDefaultAppManagementPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DefaultAppManagementPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Settings' - try { Set-CIPPDBCacheSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Settings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for SecureScore' - try { Set-CIPPDBCacheSecureScore -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "SecureScore collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for PIMSettings' - try { Set-CIPPDBCachePIMSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "PIMSettings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Domains' - try { Set-CIPPDBCacheDomains -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Domains collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleEligibilitySchedules' - try { Set-CIPPDBCacheRoleEligibilitySchedules -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleEligibilitySchedules collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleManagementPolicies' - try { Set-CIPPDBCacheRoleManagementPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleManagementPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleAssignmentScheduleInstances' - try { Set-CIPPDBCacheRoleAssignmentScheduleInstances -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleAssignmentScheduleInstances collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for B2BManagementPolicy' - try { Set-CIPPDBCacheB2BManagementPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "B2BManagementPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthenticationFlowsPolicy' - try { Set-CIPPDBCacheAuthenticationFlowsPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationFlowsPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DeviceRegistrationPolicy' - try { Set-CIPPDBCacheDeviceRegistrationPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceRegistrationPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for CredentialUserRegistrationDetails' - try { Set-CIPPDBCacheCredentialUserRegistrationDetails -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CredentialUserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for UserRegistrationDetails' - try { Set-CIPPDBCacheUserRegistrationDetails -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "UserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for OAuth2PermissionGrants' - try { Set-CIPPDBCacheOAuth2PermissionGrants -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OAuth2PermissionGrants collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AppRoleAssignments' - try { Set-CIPPDBCacheAppRoleAssignments -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AppRoleAssignments collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for License Overview' - try { Set-CIPPDBCacheLicenseOverview -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License Overview collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for MFA State' - try { Set-CIPPDBCacheMFAState -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MFA State collection failed: $($_.Exception.Message)" -sev Error - } - #endregion All Licenses - - #region Exchange Licensed - Exchange Online features - if ($ExchangeCapable) { - Write-Host 'Getting cache for ExoAntiPhishPolicies' - try { Set-CIPPDBCacheExoAntiPhishPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoMalwareFilterPolicies' - try { Set-CIPPDBCacheExoMalwareFilterPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeLinksPolicies' - try { Set-CIPPDBCacheExoSafeLinksPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeAttachmentPolicies' - try { Set-CIPPDBCacheExoSafeAttachmentPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoTransportRules' - try { Set-CIPPDBCacheExoTransportRules -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTransportRules collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoDkimSigningConfig' - try { Set-CIPPDBCacheExoDkimSigningConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoDkimSigningConfig collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoOrganizationConfig' - try { Set-CIPPDBCacheExoOrganizationConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoOrganizationConfig collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAcceptedDomains' - try { Set-CIPPDBCacheExoAcceptedDomains -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoHostedContentFilterPolicy' - try { Set-CIPPDBCacheExoHostedContentFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedContentFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoHostedOutboundSpamFilterPolicy' - try { Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedOutboundSpamFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAntiPhishPolicy' - try { Set-CIPPDBCacheExoAntiPhishPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeLinksPolicy' - try { Set-CIPPDBCacheExoSafeLinksPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeAttachmentPolicy' - try { Set-CIPPDBCacheExoSafeAttachmentPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoMalwareFilterPolicy' - try { Set-CIPPDBCacheExoMalwareFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAtpPolicyForO365' - try { Set-CIPPDBCacheExoAtpPolicyForO365 -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAtpPolicyForO365 collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoQuarantinePolicy' - try { Set-CIPPDBCacheExoQuarantinePolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoQuarantinePolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoRemoteDomain' - try { Set-CIPPDBCacheExoRemoteDomain -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoRemoteDomain collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSharingPolicy' - try { Set-CIPPDBCacheExoSharingPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSharingPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAdminAuditLogConfig' - try { Set-CIPPDBCacheExoAdminAuditLogConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAdminAuditLogConfig collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoPresetSecurityPolicy' - try { Set-CIPPDBCacheExoPresetSecurityPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoPresetSecurityPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoTenantAllowBlockList' - try { Set-CIPPDBCacheExoTenantAllowBlockList -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTenantAllowBlockList collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Exchange Online data collection - tenant does not have required license' -sev Info - } - #endregion Exchange Licensed - - #region Conditional Access Licensed - Azure AD Premium features - if ($ConditionalAccessCapable) { - Write-Host 'Getting cache for ConditionalAccessPolicies' - try { Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ConditionalAccessPolicies collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Conditional Access data collection - tenant does not have required license' -sev Info - } - #endregion Conditional Access Licensed - - #region Azure AD Premium P2 - Identity Protection features - if ($AzureADPremiumP2Capable) { - Write-Host 'Getting cache for RiskyUsers' - try { Set-CIPPDBCacheRiskyUsers -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyUsers collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RiskyServicePrincipals' - try { Set-CIPPDBCacheRiskyServicePrincipals -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyServicePrincipals collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ServicePrincipalRiskDetections' - try { Set-CIPPDBCacheServicePrincipalRiskDetections -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipalRiskDetections collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RiskDetections' - try { Set-CIPPDBCacheRiskDetections -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Azure AD Premium P2 Identity Protection data collection - tenant does not have required license' -sev Info - } - #endregion Azure AD Premium P2 - - #region Intune Licensed - Intune management features - if ($IntuneCapable) { - Write-Host 'Getting cache for ManagedDevices' - try { Set-CIPPDBCacheManagedDevices -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDevices collection failed: $($_.Exception.Message)" -sev Error - } + # Build dynamic batch of cache collection tasks based on license capabilities + $Batch = [System.Collections.Generic.List[object]]::new() + + #region All Licenses - Basic tenant data collection + $BasicCacheFunctions = @( + 'Users' + 'Groups' + 'Guests' + 'ServicePrincipals' + 'Apps' + 'Devices' + 'Organization' + 'Roles' + 'AdminConsentRequestPolicy' + 'AuthorizationPolicy' + 'AuthenticationMethodsPolicy' + 'DeviceSettings' + 'DirectoryRecommendations' + 'CrossTenantAccessPolicy' + 'DefaultAppManagementPolicy' + 'Settings' + 'SecureScore' + 'PIMSettings' + 'Domains' + 'RoleEligibilitySchedules' + 'RoleManagementPolicies' + 'RoleAssignmentScheduleInstances' + 'B2BManagementPolicy' + 'AuthenticationFlowsPolicy' + 'DeviceRegistrationPolicy' + 'CredentialUserRegistrationDetails' + 'UserRegistrationDetails' + 'OAuth2PermissionGrants' + 'AppRoleAssignments' + 'LicenseOverview' + 'MFAState' + ) + + foreach ($CacheFunction in $BasicCacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } + #endregion All Licenses + + #region Exchange Licensed - Exchange Online features + if ($ExchangeCapable) { + $ExchangeCacheFunctions = @( + 'ExoAntiPhishPolicies' + 'ExoMalwareFilterPolicies' + 'ExoSafeLinksPolicies' + 'ExoSafeAttachmentPolicies' + 'ExoTransportRules' + 'ExoDkimSigningConfig' + 'ExoOrganizationConfig' + 'ExoAcceptedDomains' + 'ExoHostedContentFilterPolicy' + 'ExoHostedOutboundSpamFilterPolicy' + 'ExoAntiPhishPolicy' + 'ExoSafeLinksPolicy' + 'ExoSafeAttachmentPolicy' + 'ExoMalwareFilterPolicy' + 'ExoAtpPolicyForO365' + 'ExoQuarantinePolicy' + 'ExoRemoteDomain' + 'ExoSharingPolicy' + 'ExoAdminAuditLogConfig' + 'ExoPresetSecurityPolicy' + 'ExoTenantAllowBlockList' + 'Mailboxes' + 'MailboxUsage' + 'OneDriveUsage' + ) + + foreach ($CacheFunction in $ExchangeCacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } + } else { + Write-Host 'Skipping Exchange Online data collection - tenant does not have required license' + } + #endregion Exchange Licensed + + #region Conditional Access Licensed - Azure AD Premium features + if ($ConditionalAccessCapable) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = 'ConditionalAccessPolicies' + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } else { + Write-Host 'Skipping Conditional Access data collection - tenant does not have required license' + } + #endregion Conditional Access Licensed + + #region Azure AD Premium P2 - Identity Protection features + if ($AzureADPremiumP2Capable) { + $P2CacheFunctions = @( + 'RiskyUsers' + 'RiskyServicePrincipals' + 'ServicePrincipalRiskDetections' + 'RiskDetections' + ) + foreach ($CacheFunction in $P2CacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } + } else { + Write-Host 'Skipping Azure AD Premium P2 Identity Protection data collection - tenant does not have required license' + } + #endregion Azure AD Premium P2 + + #region Intune Licensed - Intune management features + if ($IntuneCapable) { + $IntuneCacheFunctions = @( + 'ManagedDevices' + 'IntunePolicies' + 'ManagedDeviceEncryptionStates' + 'IntuneAppProtectionPolicies' + ) + foreach ($CacheFunction in $IntuneCacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } + } else { + Write-Host 'Skipping Intune data collection - tenant does not have required license' + } + #endregion Intune Licensed - Write-Host 'Getting cache for IntunePolicies' - try { Set-CIPPDBCacheIntunePolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntunePolicies collection failed: $($_.Exception.Message)" -sev Error - } + Write-Information "Built batch of $($Batch.Count) cache collection activities for tenant $TenantFilter" - Write-Host 'Getting cache for ManagedDeviceEncryptionStates' - try { Set-CIPPDBCacheManagedDeviceEncryptionStates -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDeviceEncryptionStates collection failed: $($_.Exception.Message)" -sev Error - } + # Start orchestration for this tenant's cache collection + $InputObject = [PSCustomObject]@{ + OrchestratorName = "CIPPDBCacheTenant_$TenantFilter" + Batch = @($Batch) + SkipLog = $true + DurableMode = 'Sequence' + } - Write-Host 'Getting cache for IntuneAppProtectionPolicies' - try { Set-CIPPDBCacheIntuneAppProtectionPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneAppProtectionPolicies collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Intune data collection - tenant does not have required license' -sev Info + if ($Item.TestRun -eq $true) { + $InputObject | Add-Member -NotePropertyName PostExecution -NotePropertyValue @{ + FunctionName = 'CIPPTestsRun' + Parameters = @{ + TenantFilter = $TenantFilter } - #endregion Intune Licensed } - 'Mailboxes' { - if ($ExchangeCapable) { - Write-Host 'Getting cache for Mailboxes' - try { Set-CIPPDBCacheMailboxes -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Mailboxes collection failed: $($_.Exception.Message)" -sev Error - } + } - Write-Host 'Getting cache for MailboxUsage' - try { Set-CIPPDBCacheMailboxUsage -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MailboxUsage collection failed: $($_.Exception.Message)" -sev Error - } + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Information "Started cache collection orchestration for $TenantFilter with ID = '$InstanceId'" + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started cache collection orchestration with $($Batch.Count) activities. Instance ID: $InstanceId" -sev Info - Write-Host 'Getting cache for OneDriveUsage' - try { Set-CIPPDBCacheOneDriveUsage -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OneDriveUsage collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Mailboxes data collection - tenant does not have required Exchange license' -sev Info - } - } + return @{ + InstanceId = $InstanceId + BatchCount = $Batch.Count + Message = "Cache collection orchestration started for $TenantFilter" } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info - } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to complete database cache collection: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to start cache collection orchestration: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + throw $ErrorMessage } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 index 9cc264d9af27..af97857c8e67 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPTestsRun { $TenantsWithData } else { $DbCounts = Get-CIPPDbItem -TenantFilter $TenantFilter -CountsOnly - if (($DbCounts | Measure-Object -Property Count -Sum).Sum -gt 0) { + if (($DbCounts | Measure-Object -Property DataCount -Sum).Sum -gt 0) { @($TenantFilter) } else { Write-LogMessage -API 'Tests' -tenant $TenantFilter -message 'Tenant has no data in database. Skipping tests.' -sev Info @@ -55,7 +55,7 @@ function Invoke-CIPPTestsRun { } } - Write-Information "Built batch of $($Batch.Count) test activities ($($AllTests.Count) tests × $($AllTenantsList.Count) tenants)" + Write-Information "Built batch of $($Batch.Count) test activities ($($AllTests.Count) tests x $($AllTenantsList.Count) tenants)" $InputObject = [PSCustomObject]@{ OrchestratorName = 'TestsRun' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 index 2db5725ce7b6..ccd902350988 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 @@ -21,8 +21,12 @@ function Invoke-ExecCloneTemplate { $NewGuid = [guid]::NewGuid().ToString() $Template.RowKey = $NewGuid $Template.JSON = $Template.JSON -replace $GUID, $NewGuid - $Template.Package = $null - $Template.SHA = $null + if ($Template.Package) { + $Template.Package = $null + } + if ($Template.SHA) { + $Template.SHA = $null + } try { Add-CIPPAzDataTableEntity @Table -Entity $Template $body = @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 index facaa423a70a..af8f95f2edc7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 @@ -89,7 +89,7 @@ function Invoke-ExecExchangeRoleRepair { } } - returns ([HttpResponseContext]@{ + return ([HttpResponseContext]@{ StatusCode = [System.Net.HttpStatusCode]::OK Body = $Results }) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 index 26d3db6edf2f..0c44628dbac1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ExecExcludeLicenses { +function Invoke-ExecExcludeLicenses { <# .FUNCTIONALITY Entrypoint @@ -9,57 +9,61 @@ Function Invoke-ExecExcludeLicenses { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers $Table = Get-CIPPTable -TableName ExcludedLicenses - try { - if ($Request.Query.List) { - $Rows = Get-CIPPAzDataTableEntity @Table - if ($Rows.Count -lt 1) { - $TableBaseData = '[{"GUID":"16ddbbfc-09ea-4de2-b1d7-312db6112d70","Product_Display_Name":"MICROSOFT TEAMS (FREE)"},{"GUID":"1f2f344a-700d-42c9-9427-5cea1d5d7ba6","Product_Display_Name":"MICROSOFT STREAM"},{"GUID":"338148b6-1b11-4102-afb9-f92b6cdc0f8d","Product_Display_Name":"DYNAMICS 365 P1 TRIAL FOR INFORMATION WORKERS"},{"GUID":"606b54a9-78d8-4298-ad8b-df6ef4481c80","Product_Display_Name":"Power Virtual Agents Viral Trial"},{"GUID":"61e6bd70-fbdb-4deb-82ea-912842f39431","Product_Display_Name":"Dynamics 365 Customer Service Insights Trial"},{"GUID":"6470687e-a428-4b7a-bef2-8a291ad947c9","Product_Display_Name":"WINDOWS STORE FOR BUSINESS"},{"GUID":"710779e8-3d4a-4c88-adb9-386c958d1fdf","Product_Display_Name":"MICROSOFT TEAMS EXPLORATORY"},{"GUID":"74fbf1bb-47c6-4796-9623-77dc7371723b","Product_Display_Name":"Microsoft Teams Trial"},{"GUID":"90d8b3f8-712e-4f7b-aa1e-62e7ae6cbe96","Product_Display_Name":"Business Apps (free)"},{"GUID":"a403ebcc-fae0-4ca2-8c8c-7a907fd6c235","Product_Display_Name":"Power BI (free)"},{"GUID":"bc946dac-7877-4271-b2f7-99d2db13cd2c","Product_Display_Name":"Dynamics 365 Customer Voice Trial"},{"GUID":"dcb1a3ae-b33f-4487-846a-a640262fadf4","Product_Display_Name":"Microsoft Power Apps Plan 2 Trial"},{"GUID":"f30db892-07e9-47e9-837c-80727f46fd3d","Product_Display_Name":"MICROSOFT FLOW FREE"},{"GUID":"fcecd1f9-a91e-488d-a918-a96cdb6ce2b0","Product_Display_Name":"Microsoft Dynamics AX7 User Trial"}]' | ConvertFrom-Json -AsHashtable -Depth 10 - $TableRows = foreach ($Row in $TableBaseData) { - $Row.PartitionKey = 'License' - $Row.RowKey = $Row.GUID + # Interact with query parameters or the body of the request. + try { + $Action = $Request.Body.Action + $GUID = $Request.Body.GUID + $DisplayName = $Request.Body.SKUName - Add-CIPPAzDataTableEntity @Table -Entity ([pscustomobject]$Row) -Force | Out-Null + switch ($Action) { + 'AddExclusion' { + $AddObject = @{ + PartitionKey = 'License' + RowKey = $GUID + 'GUID' = $GUID + 'Product_Display_Name' = $DisplayName } + Add-CIPPAzDataTableEntity @Table -Entity $AddObject -Force + $Result = "Success. Added $DisplayName($GUID) to the excluded licenses list." + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Info' - $Rows = Get-CIPPAzDataTableEntity @Table - - Write-LogMessage -API $APINAME -headers $Request.Headers -message 'got excluded licenses list' -Sev 'Info' } - $body = @($Rows) - } + 'RemoveExclusion' { + $Filter = "RowKey eq '{0}' and PartitionKey eq 'License'" -f $GUID + $Entity = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $Entity + $Result = "Success. Removed $DisplayName($GUID) from the excluded licenses list." + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Info' - # Interact with query parameters or the body of the request. - $name = $Request.Query.TenantFilter - if ($Request.Query.AddExclusion) { - $AddObject = @{ - PartitionKey = 'License' - RowKey = $Request.body.GUID - 'GUID' = $Request.body.GUID - 'Product_Display_Name' = $request.body.SKUName } - Add-CIPPAzDataTableEntity @Table -Entity $AddObject -Force + 'RestoreDefaults' { + $FullReset = [bool]$Request.Body.FullReset + if ($FullReset) { + $InitResult = Initialize-CIPPExcludedLicenses -Force -Headers $Headers -APIName $APIName + } else { + $InitResult = Initialize-CIPPExcludedLicenses -Headers $Headers -APIName $APIName + } + $Result = $InitResult.Message - Write-LogMessage -API $APINAME -headers $Request.Headers -message "Added exclusion $($request.body.SKUName)" -Sev 'Info' - $body = [pscustomobject]@{'Results' = "Success. We've added $($request.body.SKUName) to the excluded list." } + } + default { + $StatusCode = [HttpStatusCode]::BadRequest + $Result = "Invalid action specified: $Action" + } } - if ($Request.Query.RemoveExclusion) { - $Filter = "RowKey eq '{0}' and PartitionKey eq 'License'" -f $Request.Body.GUID - $Entity = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey - Remove-AzDataTableEntity -Force @Table -Entity $Entity - Write-LogMessage -API $APINAME -headers $Request.Headers -message "Removed exclusion $($Request.Query.GUID)" -Sev 'Info' - $body = [pscustomobject]@{'Results' = "Success. We've removed $($Request.query.guid) from the excluded list." } - } } catch { - Write-LogMessage -API $APINAME -headers $Request.Headers -message "Exclusion API failed. $($_.Exception.Message)" -Sev 'Error' - $body = [pscustomobject]@{'Results' = "Failed. $($_.Exception.Message)" } + $ErrorMessage = Get-CippException -Exception $_ + $StatusCode = [HttpStatusCode]::InternalServerError + $Result = "Failed to process exclusion request. $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage } return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $body + StatusCode = $StatusCode ?? [HttpStatusCode]::OK + Body = [pscustomobject]@{ 'Results' = $Result } }) - } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 new file mode 100644 index 000000000000..764cbdc2ce79 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 @@ -0,0 +1,37 @@ +function Invoke-ListExcludedLicenses { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.AppSettings.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $Table = Get-CIPPTable -TableName ExcludedLicenses + $Rows = Get-CIPPAzDataTableEntity @Table + + # If no excluded licenses exist, initialize them + if ($Rows.Count -lt 1) { + Write-Information 'Excluded licenses table is empty. Initializing from config file.' + $null = Initialize-CIPPExcludedLicenses -Headers $Headers -APIName $APIName + $Rows = Get-CIPPAzDataTableEntity @Table + } + + $Results = @($Rows) + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $StatusCode = [HttpStatusCode]::InternalServerError + $Results = "Failed to list excluded licenses. $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -headers $Headers -message $Results -Sev 'Error' -LogData $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode ?? [HttpStatusCode]::OK + Body = [pscustomobject]@{ 'Results' = $Results } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 index 61f14eef0be4..902909f7cd64 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 @@ -11,8 +11,35 @@ Function Invoke-ListCalendarPermissions { $APIName = $Request.Params.CIPPEndpoint $UserID = $Request.Query.UserID $TenantFilter = $Request.Query.tenantFilter + $UseReportDB = $Request.Query.UseReportDB + $ByUser = $Request.Query.ByUser try { + # If UseReportDB is specified and no specific UserID, retrieve from report database + if ($UseReportDB -eq 'true' -and -not $UserID) { + + # Call the report function with proper parameters + $ReportParams = @{ + TenantFilter = $TenantFilter + } + if ($ByUser -eq 'true') { + $ReportParams.ByUser = $true + } + try { + $GraphRequest = Get-CIPPCalendarPermissionReport @ReportParams + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + } + + # Original live query logic for specific user $GetCalParam = @{Identity = $UserID; FolderScope = 'Calendar' } $CalendarFolder = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -anchor $UserID -cmdParams $GetCalParam | Select-Object -First 1 -ExcludeProperty *data.type* $CalParam = @{Identity = "$($UserID):\$($CalendarFolder.name)" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 index 95b4a7de57bb..a569ed418d5e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 @@ -24,9 +24,13 @@ function Invoke-ListmailboxPermissions { if ($ByUser -eq 'true') { $ReportParams.ByUser = $true } - - $GraphRequest = Get-CIPPMailboxPermissionReport @ReportParams - $StatusCode = [HttpStatusCode]::OK + try { + $GraphRequest = Get-CIPPMailboxPermissionReport @ReportParams + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } return ([HttpResponseContext]@{ StatusCode = $StatusCode diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 index fc66527eb142..9db18151e54e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 @@ -15,7 +15,7 @@ function Invoke-ListGroups { $ExpandMembers = $Request.Query.expandMembers ?? $false - $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,onPremisesSyncEnabled,resourceProvisioningOptions,assignedLicenses,userPrincipalName' + $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,onPremisesSyncEnabled,resourceProvisioningOptions,assignedLicenses,userPrincipalName,licenseProcessingState' if ($ExpandMembers -ne $false) { $SelectString = '{0}&$expand=members($select=userPrincipalName)' -f $SelectString } @@ -24,7 +24,7 @@ function Invoke-ListGroups { $BulkRequestArrayList = [System.Collections.Generic.List[object]]::new() if ($Request.Query.GroupID) { - $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,assignedLicenses,userPrincipalName,onPremisesSyncEnabled' + $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,assignedLicenses,userPrincipalName,onPremisesSyncEnabled,licenseProcessingState' $BulkRequestArrayList.add(@{ id = 1 method = 'GET' @@ -102,8 +102,8 @@ function Invoke-ListGroups { } }, @{Name = 'dynamicGroupBool'; Expression = { if ($_.groupTypes -contains 'DynamicMembership') { $true } else { $false } } } - members = ($RawGraphRequest | Where-Object { $_.id -eq 2 }).body.value | Sort-Object displayName - owners = ($RawGraphRequest | Where-Object { $_.id -eq 3 }).body.value | Sort-Object displayName + members = @(($RawGraphRequest | Where-Object { $_.id -eq 2 }).body.value | Sort-Object displayName) + owners = @(($RawGraphRequest | Where-Object { $_.id -eq 3 }).body.value | Sort-Object displayName) allowExternal = (!$OnlyAllowInternal) sendCopies = $SendCopies hideFromOutlookClients = if ($GroupType -eq 'Microsoft 365') { $UnifiedGroupInfo.HiddenFromExchangeClientsEnabled } else { $null } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecReprocessUserLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecReprocessUserLicenses.ps1 new file mode 100644 index 000000000000..3aaf3829e0ff --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecReprocessUserLicenses.ps1 @@ -0,0 +1,36 @@ +function Invoke-ExecReprocessUserLicenses { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.User.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $UserID = $Request.Query.ID ?? $Request.Body.ID + $UserPrincipalName = $Request.Query.userPrincipalName ?? $Request.Body.userPrincipalName + + try { + $GraphRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/users/$UserID/reprocessLicenseAssignment" -tenantid $TenantFilter -type POST -body '{}' -AsApp $true + + $Result = "Successfully reprocessed license assignments for user $UserPrincipalName. License assignment states will be updated shortly." + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to reprocess license assignments for $UserPrincipalName. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ 'Results' = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 index 04d6ceed951f..8dd0274fe7f9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 @@ -14,8 +14,14 @@ function Invoke-ListMFAUsers { try { # If UseReportDB is specified, retrieve from report database if ($UseReportDB -eq 'true') { - $GraphRequest = Get-CIPPMFAStateReport -TenantFilter $TenantFilter - $StatusCode = [HttpStatusCode]::OK + try { + $GraphRequest = Get-CIPPMFAStateReport -TenantFilter $TenantFilter -ErrorAction Stop + $StatusCode = [HttpStatusCode]::OK + } catch { + Write-Host "Error retrieving MFA state from report database: $($_.Exception.Message)" + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } return ([HttpResponseContext]@{ StatusCode = $StatusCode @@ -54,11 +60,15 @@ function Invoke-ListMFAUsers { } } else { $Rows = foreach ($Row in $Rows) { - if ($Row.CAPolicies) { - $Row.CAPolicies = try { $Row.CAPolicies | ConvertFrom-Json } catch { $Row.CAPolicies } + if ($Row.CAPolicies -and $Row.CAPolicies -is [string]) { + $Row.CAPolicies = try { $Row.CAPolicies | ConvertFrom-Json } catch { @() } + } elseif (-not $Row.CAPolicies) { + $Row.CAPolicies = @() } - if ($Row.MFAMethods) { - $Row.MFAMethods = try { $Row.MFAMethods | ConvertFrom-Json } catch { $Row.MFAMethods } + if ($Row.MFAMethods -and $Row.MFAMethods -is [string]) { + $Row.MFAMethods = try { $Row.MFAMethods | ConvertFrom-Json } catch { @() } + } elseif (-not $Row.MFAMethods) { + $Row.MFAMethods = @() } $Row } @@ -73,8 +83,9 @@ function Invoke-ListMFAUsers { } return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK + StatusCode = $StatusCode Body = @($GraphRequest) }) + } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 index bbf455e74581..1a228cff5dc3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 @@ -19,17 +19,12 @@ function Invoke-ExecTestRun { TenantFilter = $TenantFilter QueueId = $Queue.RowKey QueueName = "Cache - $TenantFilter" + TestRun = $true } ) $InputObject = [PSCustomObject]@{ OrchestratorName = 'TestDataCollectionAndRun' Batch = $Batch - PostExecution = @{ - FunctionName = 'CIPPTestsRun' - Parameters = @{ - TenantFilter = $TenantFilter - } - } SkipLog = $false } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 08946b093fe9..421a6a4ca38e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -13,7 +13,6 @@ function Invoke-ListTests { param($Request, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' try { $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 index 31844a48a08d..1bf2c4bfaabc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 @@ -17,13 +17,15 @@ function Invoke-ListExternalTenantInfo { $Tenant = $Request.Query.tenant # Normalize to tenantid and determine if tenant exists - $TenantId = (Invoke-RestMethod -Method GET "https://login.windows.net/$Tenant/.well-known/openid-configuration").token_endpoint.Split('/')[3] + $OpenIdConfig = Invoke-RestMethod -Method GET "https://login.windows.net/$Tenant/.well-known/openid-configuration" + $TenantId = $OpenIdConfig.token_endpoint.Split('/')[3] if ($TenantId) { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$TenantId')" -NoAuthCheck $true -tenantid $env:TenantID $StatusCode = [HttpStatusCode]::OK $HttpResponse.Body = [PSCustomObject]@{ GraphRequest = $GraphRequest + OpenIdConfig = $OpenIdConfig } } else { $HttpResponse.StatusCode = [HttpStatusCode]::BadRequest diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 index dd504b9700e7..8c4d4953bdf0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 @@ -21,7 +21,8 @@ function Invoke-ListLogs { } } elseif ($Request.Query.logentryid) { # Return single log entry by RowKey - $Filter = "RowKey eq '{0}'" -f $Request.Query.logentryid + $DateFilter = $Request.Query.DateFilter ?? (Get-Date -UFormat '%Y%m%d') + $Filter = "RowKey eq '{0}'" -f $Request.Query.logentryid, $DateFilter $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList Write-Host "Getting single log entry for RowKey: $($Request.Query.logentryid)" @@ -59,22 +60,23 @@ function Invoke-ListLogs { $Row.LogData | ConvertFrom-Json } else { $Row.LogData } [PSCustomObject]@{ - DateTime = $Row.Timestamp - Tenant = $Row.Tenant - API = $Row.API - Message = $Row.Message - User = $Row.Username - Severity = $Row.Severity - LogData = $LogData - TenantID = if ($Row.TenantID -ne $null) { + DateTime = $Row.Timestamp + Tenant = $Row.Tenant + API = $Row.API + Message = $Row.Message + User = $Row.Username + Severity = $Row.Severity + LogData = $LogData + TenantID = if ($Row.TenantID -ne $null) { $Row.TenantID } else { 'None' } - AppId = $Row.AppId - IP = $Row.IP - RowKey = $Row.RowKey - Standard = $StandardInfo + AppId = $Row.AppId + IP = $Row.IP + RowKey = $Row.RowKey + Standard = $StandardInfo + DateFilter = $Row.PartitionKey } } } @@ -164,6 +166,7 @@ function Invoke-ListLogs { IP = $Row.IP RowKey = $Row.RowKey StandardInfo = $StandardInfo + DateFilter = $Row.PartitionKey } } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 index 51e0861ca294..8717d60bdef9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 @@ -22,7 +22,7 @@ function Start-CIPPDBCacheOrchestrator { return } - $TaskCount = $TenantList.Count * 2 + $TaskCount = $TenantList.Count $Queue = New-CippQueueEntry -Name 'Database Cache Collection' -TotalTasks $TaskCount $Batch = [system.collections.generic.list[object]]::new() @@ -33,13 +33,6 @@ function Start-CIPPDBCacheOrchestrator { QueueId = $Queue.RowKey QueueName = "DB Cache - $($Tenant.defaultDomainName)" }) - $Batch.Add([PSCustomObject]@{ - FunctionName = 'CIPPDBCacheData' - TenantFilter = $Tenant.defaultDomainName - QueueId = $Queue.RowKey - Type = 'Mailboxes' - QueueName = "DB Cache Mailboxes - $($Tenant.defaultDomainName)" - }) } Write-Host "Created queue $($Queue.RowKey) for database cache collection of $($TenantList.Count) tenants" Write-Host "Starting batch of $($Batch.Count) cache collection activities" diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 index 4989829ba7a9..35a101109294 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 @@ -56,6 +56,15 @@ function Start-TableCleanup { Property = @('PartitionKey', 'RowKey', 'ETag') } } + @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'ScheduledTasks' + DataTableProps = @{ + Filter = "PartitionKey eq 'ScheduledTask' and Command eq 'Sync-CippExtensionData'" + Property = @('PartitionKey', 'RowKey', 'ETag') + } + } @{ FunctionName = 'TableCleanupTask' Type = 'DeleteTable' diff --git a/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 new file mode 100644 index 000000000000..4f7bd038fea4 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 @@ -0,0 +1,235 @@ +function Get-CIPPCalendarPermissionReport { + <# + .SYNOPSIS + Generates a calendar permission report from the CIPP Reporting database + + .DESCRIPTION + Retrieves calendar permissions for a tenant and formats them into a report. + Default view shows permissions per calendar. Use -ByUser to pivot by user. + + .PARAMETER TenantFilter + The tenant to generate the report for + + .PARAMETER ByUser + If specified, groups results by user instead of by calendar + + .EXAMPLE + Get-CIPPCalendarPermissionReport -TenantFilter 'contoso.onmicrosoft.com' + Shows which users have access to each calendar + + .EXAMPLE + Get-CIPPCalendarPermissionReport -TenantFilter 'contoso.onmicrosoft.com' -ByUser + Shows what calendars each user has access to + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [switch]$ByUser + ) + + try { + Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message 'Generating calendar permission report' -sev Info + + # Handle AllTenants + if ($TenantFilter -eq 'AllTenants') { + # Get all tenants that have calendar data + $AllCalendarItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'CalendarPermissions' + $Tenants = @($AllCalendarItems | Where-Object { $_.RowKey -ne 'CalendarPermissions-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Tenant in $Tenants) { + try { + $TenantResults = Get-CIPPCalendarPermissionReport -TenantFilter $Tenant -ByUser:$ByUser + foreach ($Result in $TenantResults) { + # Add Tenant property to each result + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'CalendarPermissionReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + # Get mailboxes from reporting DB + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } + if (-not $MailboxItems) { + throw 'No mailbox data found in reporting database. Sync the mailbox permissions first.' + } + + # Get the most recent mailbox cache timestamp + $MailboxCacheTimestamp = ($MailboxItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse mailbox data and create lookup by UPN, ID, and ExternalDirectoryObjectId (case-insensitive) + $MailboxLookup = @{} + $MailboxByIdLookup = @{} + $MailboxByExternalIdLookup = @{} + foreach ($Item in $MailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' }) { + $Mailbox = $Item.Data | ConvertFrom-Json + if ($Mailbox.UPN) { + $MailboxLookup[$Mailbox.UPN.ToLower()] = $Mailbox + } + if ($Mailbox.primarySmtpAddress) { + $MailboxLookup[$Mailbox.primarySmtpAddress.ToLower()] = $Mailbox + } + if ($Mailbox.Id) { + $MailboxByIdLookup[$Mailbox.Id] = $Mailbox + } + if ($Mailbox.ExternalDirectoryObjectId) { + $MailboxByExternalIdLookup[$Mailbox.ExternalDirectoryObjectId] = $Mailbox + } + } + + # Get calendar permissions from reporting DB + $PermissionItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' + if (-not $PermissionItems) { + throw 'No calendar permission data found in reporting database. Run a scan first.' + } + + # Get the most recent permission cache timestamp + $PermissionCacheTimestamp = ($PermissionItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse all permissions + $AllPermissions = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $PermissionItems | Where-Object { $_.RowKey -ne 'CalendarPermissions-Count' }) { + $Permission = $Item.Data | ConvertFrom-Json + + # Skip Default and Anonymous permissions as they're standard and not typically relevant + if ($Permission.User -in @('Default', 'Anonymous', 'NT AUTHORITY\SELF')) { + continue + } + + # Extract the mailbox identifier from Identity (format: "mailbox-id:\Calendar" or "mailbox-upn:\Calendar") + # The Identity can contain either a GUID, UPN, or alias before the colon-backslash separator + $IdentityParts = $Permission.Identity -split ':\\' + if ($IdentityParts.Count -lt 1) { + Write-Verbose "Invalid Identity format: $($Permission.Identity)" + continue + } + $MailboxIdentifier = $IdentityParts[0] + + # Get mailbox info - try multiple match strategies + $Mailbox = $null + + # Try UPN/primarySmtpAddress lookup (case-insensitive) + $Mailbox = $MailboxLookup[$MailboxIdentifier.ToLower()] + + # If not found, try ExternalDirectoryObjectId lookup + if (-not $Mailbox) { + $Mailbox = $MailboxByExternalIdLookup[$MailboxIdentifier] + } + + # If not found, try ID lookup + if (-not $Mailbox) { + $Mailbox = $MailboxByIdLookup[$MailboxIdentifier] + } + + if (-not $Mailbox) { + Write-Verbose "No mailbox found for Identity: $MailboxIdentifier" + continue + } + + $AllPermissions.Add([PSCustomObject]@{ + MailboxUPN = if ($Mailbox.UPN) { $Mailbox.UPN } elseif ($Mailbox.primarySmtpAddress) { $Mailbox.primarySmtpAddress } else { $MailboxIdentifier } + MailboxDisplayName = $Mailbox.displayName + MailboxType = $Mailbox.recipientTypeDetails + User = $Permission.User + UserKey = if ($Permission.User -match '@') { $Permission.User.ToLower() } else { $Permission.User } + AccessRights = ($Permission.AccessRights -join ', ') + FolderName = $Permission.FolderName + }) + } + + if ($AllPermissions.Count -eq 0) { + Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message 'No calendar permissions found (excluding Default/Anonymous)' -sev Debug + Write-Information -Message 'No calendar permissions found (excluding Default/Anonymous)' + return @() + } + + # Format results based on grouping preference + if ($ByUser) { + # Group by user - calculate which calendars each user has access to + # Use UserKey for grouping to handle case-insensitive email addresses + $Report = $AllPermissions | Group-Object -Property UserKey | ForEach-Object { + $UserKey = $_.Name + $UserDisplay = $_.Group[0].User # Use original User value for display + + # Look up the user's mailbox type using multi-strategy approach + $UserMailbox = $null + if ($UserDisplay) { + # Try UPN/primarySmtpAddress lookup (case-insensitive) + $UserMailbox = $MailboxLookup[$UserDisplay.ToLower()] + + # If not found, try ExternalDirectoryObjectId lookup + if (-not $UserMailbox) { + $UserMailbox = $MailboxByExternalIdLookup[$UserDisplay] + } + + # If not found, try ID lookup + if (-not $UserMailbox) { + $UserMailbox = $MailboxByIdLookup[$UserDisplay] + } + } + $UserMailboxType = if ($UserMailbox) { $UserMailbox.recipientTypeDetails } else { 'Unknown' } + + # Build detailed permissions list with calendar and access rights + $PermissionDetails = @($_.Group | ForEach-Object { + [PSCustomObject]@{ + Calendar = $_.MailboxDisplayName + CalendarUPN = $_.MailboxUPN + AccessRights = $_.AccessRights + } + }) + + [PSCustomObject]@{ + User = $UserDisplay + UserMailboxType = $UserMailboxType + CalendarCount = $_.Count + Permissions = $PermissionDetails + Tenant = $TenantFilter + MailboxCacheTimestamp = $MailboxCacheTimestamp + PermissionCacheTimestamp = $PermissionCacheTimestamp + } + } | Sort-Object User + } else { + # Default: Group by calendar + $Report = $AllPermissions | Group-Object -Property MailboxUPN | ForEach-Object { + $CalendarUPN = $_.Name + $CalendarInfo = $_.Group[0] + + # Build detailed permissions list with user and access rights + $PermissionDetails = @($_.Group | ForEach-Object { + [PSCustomObject]@{ + User = $_.User + AccessRights = $_.AccessRights + } + }) + + [PSCustomObject]@{ + CalendarUPN = $CalendarUPN + CalendarDisplayName = $CalendarInfo.MailboxDisplayName + CalendarType = $CalendarInfo.MailboxType + PermissionCount = $_.Count + Permissions = $PermissionDetails + Tenant = $TenantFilter + MailboxCacheTimestamp = $MailboxCacheTimestamp + PermissionCacheTimestamp = $PermissionCacheTimestamp + } + } | Sort-Object CalendarDisplayName + } + + Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message "Generated report with $($Report.Count) entries" -sev Debug + return $Report + + } catch { + Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message "Failed to generate calendar permission report: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw "Failed to generate calendar permission report: $($_.Exception.Message)" + } +} diff --git a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 index 13924c6794d0..9f324f5eaf3d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 @@ -42,11 +42,15 @@ function Get-CIPPDbItem { $Conditions.Add("PartitionKey eq '{0}'" -f $TenantFilter) } if ($Type) { - $Conditions.Add("RowKey ge '{0}-' and RowKey lt '{0}.'" -f $Type) + # Exact match for count row when type is specified + $Conditions.Add("RowKey eq '{0}-Count'" -f $Type) + } else { + # Filter by DataCount property to get only count rows (server-side filtering) + $Conditions.Add('DataCount ge 0') } $Filter = [string]::Join(' and ', $Conditions) $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property 'PartitionKey', 'RowKey', 'DataCount', 'Timestamp' - $Results = $Results | Where-Object { $_.RowKey -like '*-Count' } | Select-Object PartitionKey, RowKey, DataCount, Timestamp + $Results = $Results | Select-Object PartitionKey, RowKey, DataCount, Timestamp } else { if (-not $Type) { throw 'Type parameter is required when CountsOnly is not specified' diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index bca1388b5b65..f6cb38f81a04 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -337,7 +337,7 @@ function Get-CIPPDrift { standardName = $PolicyKey standardDisplayName = "Conditional Access - $($TenantCAPolicy.displayName)" expectedValue = 'This policy only exists in the tenant, not in the template.' - receivedValue = $TenantCAPolicy | Out-String + receivedValue = (ConvertTo-Json -InputObject $TenantCAPolicy -Depth 10 -Compress) state = 'current' Status = $Status Reason = $reason diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 index 1f16664f1da3..193a2b1ca532 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 @@ -18,33 +18,45 @@ function Get-CIPPMFAState { } $Errors = [System.Collections.Generic.List[object]]::new() + $SecureDefaultsState = $null + $CASuccess = $false + $CAError = $null + $PolicyTable = @{} + $AllUserPolicies = @() + $UserGroupMembership = @{} + $UserExcludeGroupMembership = @{} + $GroupNameLookup = @{} + $MFAIndex = @{} + try { $SecureDefaultsState = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $TenantFilter ).IsEnabled } catch { Write-Host "Secure Defaults not available: $($_.Exception.Message)" $Errors.Add(@{Step = 'SecureDefaults'; Message = $_.Exception.Message }) + $SecureDefaultsState = $null } $CAState = [System.Collections.Generic.List[object]]::new() try { - $MFARegistration = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?$top=999&$select=userPrincipalName,isMfaRegistered,isMfaCapable,methodsRegistered" -tenantid $TenantFilter -asapp $true) - $MFAIndex = @{} + $MFARegistration = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&`$select=userPrincipalName,isMfaRegistered,isMfaCapable,methodsRegistered" -tenantid $TenantFilter -asapp $true) foreach ($MFAEntry in $MFARegistration) { - $MFAIndex[$MFAEntry.userPrincipalName] = $MFAEntry + if ($null -ne $MFAEntry.userPrincipalName) { + $MFAIndex[$MFAEntry.userPrincipalName] = $MFAEntry + } } } catch { $CAState.Add('Not Licensed for Conditional Access') | Out-Null $MFARegistration = $null + $CAError = "MFA registration not available - licensing required for Conditional Access reporting" if ($_.Exception.Message -ne "Tenant is not a B2C tenant and doesn't have premium licenses") { $Errors.Add(@{Step = 'MFARegistration'; Message = $_.Exception.Message }) } Write-Host "User registration details not available: $($_.Exception.Message)" - $MFAIndex = @{} } if ($null -ne $MFARegistration) { - $CASuccess = $true try { + $CASuccess = $true $CAPolicies = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999&$filter=state eq ''enabled''&$select=id,displayName,state,grantControls,conditions' -tenantid $TenantFilter -ErrorAction Stop -AsApp $true) $PolicyTable = @{} $AllUserPolicies = [System.Collections.Generic.List[object]]::new() @@ -315,11 +327,7 @@ function Get-CIPPMFAState { $PerUser = $_.PerUserMFAState - $MFARegUser = if ($null -eq ($MFAIndex[$_.UserPrincipalName])) { - $false - } else { - $MFAIndex[$_.UserPrincipalName] - } + $MFARegUser = $MFAIndex[$_.UserPrincipalName] [PSCustomObject]@{ Tenant = $TenantFilter @@ -329,11 +337,11 @@ function Get-CIPPMFAState { AccountEnabled = $_.accountEnabled PerUser = $PerUser isLicensed = $_.isLicensed - MFARegistration = if ($MFARegUser) { $MFARegUser.isMfaRegistered } else { $false } - MFACapable = if ($MFARegUser) { $MFARegUser.isMfaCapable } else { $false } - MFAMethods = if ($MFARegUser) { $MFARegUser.methodsRegistered } else { @() } + MFARegistration = if ($null -ne $MFARegUser) { [bool]$MFARegUser.isMfaRegistered } else { $null } + MFACapable = if ($null -ne $MFARegUser) { [bool]$MFARegUser.isMfaCapable } else { $null } + MFAMethods = if ($null -ne $MFARegUser) { @($MFARegUser.methodsRegistered) } else { @() } CoveredByCA = $CoveredByCA - CAPolicies = $UserCAState + CAPolicies = @($UserCAState) CoveredBySD = $SecureDefaultsState IsAdmin = $IsAdmin RowKey = [string]($_.UserPrincipalName).replace('#', '') diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 index d0b32d9bbf55..89815560e573 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 @@ -27,6 +27,9 @@ function Get-CIPPMFAStateReport { $AllMFAItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'MFAState' $Tenants = @($AllMFAItems | Where-Object { $_.RowKey -ne 'MFAState-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($Tenant in $Tenants) { try { @@ -44,7 +47,7 @@ function Get-CIPPMFAStateReport { } # Get MFA state from reporting DB - $MFAItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' + $MFAItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' | Where-Object { $_.RowKey -ne 'MFAState-Count' } if (-not $MFAItems) { throw 'No MFA state data found in reporting database. Sync the report data first.' } @@ -69,7 +72,7 @@ function Get-CIPPMFAStateReport { $AllMFAState.Add($MFAUser) } - return $AllMFAState + return $AllMFAState | Sort-Object -Property DisplayName } catch { Write-LogMessage -API 'MFAStateReport' -tenant $TenantFilter -message "Failed to generate MFA state report: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 index 29bb8efb1ac7..389dad4ddd1d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -39,6 +39,9 @@ function Get-CIPPMailboxPermissionReport { $AllMailboxItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'Mailboxes' $Tenants = @($AllMailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($Tenant in $Tenants) { try { @@ -56,7 +59,7 @@ function Get-CIPPMailboxPermissionReport { } # Get mailboxes from reporting DB - $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } if (-not $MailboxItems) { throw 'No mailbox data found in reporting database. Sync the mailbox permissions first. ' } diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 index 72fedbbeace3..eadfeef81f39 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 @@ -72,6 +72,9 @@ function Set-CIPPAssignedPolicy { } ) } + 'On' { + # Do not assign to any group - used to turn on policy without assignments + } default { # Use GroupIds if provided, otherwise resolve by name $resolvedGroupIds = @() diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 index 1a430f382a6a..40fd5bb12ddf 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 @@ -22,6 +22,6 @@ function Set-CIPPDBCacheMFAState { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MFAState.Count) MFA state records successfully" -sev Debug } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache MFA state: $($_.Exception.Message)" -sev Error + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache MFA state: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index 071c910c7677..350d97a32af7 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -24,19 +24,19 @@ function Set-CIPPDBCacheMailboxes { Select = $Select } $Mailboxes = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, - @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, - @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, - @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, - @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, - @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, - @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, - @{ Name = 'ForwardingSmtpAddress'; Expression = { $_.'ForwardingSmtpAddress' -replace 'smtp:', '' } }, - @{ Name = 'InternalForwardingAddress'; Expression = { $_.'ForwardingAddress' } }, - DeliverToMailboxAndForward, - HiddenFromAddressListsEnabled, - ExternalDirectoryObjectId, - MessageCopyForSendOnBehalfEnabled, - MessageCopyForSentAsEnabled + @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, + @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, + @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, + @{ Name = 'ForwardingSmtpAddress'; Expression = { $_.'ForwardingSmtpAddress' -replace 'smtp:', '' } }, + @{ Name = 'InternalForwardingAddress'; Expression = { $_.'ForwardingAddress' } }, + DeliverToMailboxAndForward, + HiddenFromAddressListsEnabled, + ExternalDirectoryObjectId, + MessageCopyForSendOnBehalfEnabled, + MessageCopyForSentAsEnabled Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes -Count @@ -54,28 +54,45 @@ function Set-CIPPDBCacheMailboxes { $MailboxCount = ($Mailboxes | Measure-Object).Count if ($MailboxCount -gt 0) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Starting mailbox permission caching for $MailboxCount mailboxes" -sev Debug - - # Create batches of 10 mailboxes each + + # Create batches of 10 mailboxes each for both mailbox and calendar permissions $BatchSize = 10 $Batches = [System.Collections.Generic.List[object]]::new() - + $TotalBatches = [Math]::Ceiling($Mailboxes.Count / $BatchSize) + for ($i = 0; $i -lt $Mailboxes.Count; $i += $BatchSize) { $BatchMailboxes = $Mailboxes[$i..[Math]::Min($i + $BatchSize - 1, $Mailboxes.Count - 1)] - + # Only send UPN to batch function to reduce payload size $BatchMailboxUPNs = $BatchMailboxes | Select-Object -ExpandProperty UPN - + $BatchNumber = [Math]::Floor($i / $BatchSize) + 1 + + # Add mailbox permissions batch + $Batches.Add([PSCustomObject]@{ + FunctionName = 'GetMailboxPermissionsBatch' + TenantFilter = $TenantFilter + Mailboxes = $BatchMailboxUPNs + BatchNumber = $BatchNumber + TotalBatches = $TotalBatches + }) + + # Add calendar permissions batch for the same mailboxes $Batches.Add([PSCustomObject]@{ - FunctionName = 'GetMailboxPermissionsBatch' - TenantFilter = $TenantFilter - Mailboxes = $BatchMailboxUPNs - BatchNumber = [Math]::Floor($i / $BatchSize) + 1 - TotalBatches = [Math]::Ceiling($Mailboxes.Count / $BatchSize) - }) + FunctionName = 'GetCalendarPermissionsBatch' + TenantFilter = $TenantFilter + Mailboxes = $BatchMailboxUPNs + BatchNumber = $BatchNumber + TotalBatches = $TotalBatches + }) } - + + # Split batches into mailbox and calendar permissions for separate post-execution + $MailboxPermBatches = $Batches | Where-Object { $_.FunctionName -eq 'GetMailboxPermissionsBatch' } + $CalendarPermBatches = $Batches | Where-Object { $_.FunctionName -eq 'GetCalendarPermissionsBatch' } + + # Start single orchestrator for both mailbox and calendar permissions $InputObject = [PSCustomObject]@{ - Batch = $Batches + Batch = @($Batches) OrchestratorName = "MailboxPermissions_$TenantFilter" DurableMode = 'Sequence' PostExecution = @{ @@ -86,7 +103,7 @@ function Set-CIPPDBCacheMailboxes { } } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox permission caching orchestrator with $($Batches.Count) batches" -sev Debug + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox and calendar permission caching orchestrator with $($Batches.Count) batches" -sev Debug } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailboxes found to cache permissions for' -sev Debug } diff --git a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 index a12bd21aaefb..ab01757ac4e6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 @@ -26,6 +26,16 @@ function Set-CIPPStandardsCompareField { return [string]$JsonValue } } + function ConvertTo-NormalizedJson { + param([string]$JsonString) + + if ([string]::IsNullOrEmpty($JsonString)) { + return $JsonString + } + #Replace quoted numbers with unquoted numbers for consistent comparison + $JsonString = $JsonString -replace ':"(\d+)"([,}])', ':$1$2' + return $JsonString + } if ($CurrentValue -and $CurrentValue -isnot [string]) { $CurrentValue = [string](ConvertTo-Json -InputObject $CurrentValue -Depth 10 -Compress) @@ -34,6 +44,14 @@ function Set-CIPPStandardsCompareField { $ExpectedValue = [string](ConvertTo-Json -InputObject $ExpectedValue -Depth 10 -Compress) } + # Normalize both values for consistent comparison (handle quoted numbers) + if ($CurrentValue) { + $CurrentValue = ConvertTo-NormalizedJson -JsonString $CurrentValue + } + if ($ExpectedValue) { + $ExpectedValue = ConvertTo-NormalizedJson -JsonString $ExpectedValue + } + # Handle bulk operations if ($BulkFields) { # Get all existing entities for this tenant in one query diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 new file mode 100644 index 000000000000..ab6d73d2d135 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 @@ -0,0 +1,96 @@ +function Invoke-CIPPStandardAutoArchiveMailbox { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) AutoArchiveMailbox + .SYNOPSIS + (Label) Set auto enable archive mailbox state + .DESCRIPTION + (Helptext) Enables or disables the tenant policy that automatically provisions an archive mailbox when a user's primary mailbox reaches 90% of its quota. + (DocsDescription) Enables or disables the tenant policy that automatically provisions an archive mailbox when a user's primary mailbox reaches 90% of its quota. This is separate from auto-archiving thresholds and does not enable archives for all users immediately. + .NOTES + CAT + Exchange Standards + TAG + EXECUTIVETEXT + Automatically provisions archive mailboxes only when users reach 90% of their mailbox capacity, reducing manual intervention and preventing mailbox quota issues without enabling archives for everyone. + ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.AutoArchiveMailbox.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + IMPACT + Low Impact + ADDEDDATE + 2026-01-16 + POWERSHELLEQUIVALENT + Set-OrganizationConfig -AutoEnableArchiveMailbox \$true\|\$false + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchiveMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + + if ($TestResult -eq $false) { + Write-Host "We're exiting as the correct license is not present for this standard." + return $true + } + + $StateValue = $Settings.state.value ?? $Settings.state + + if ([string]::IsNullOrWhiteSpace($StateValue)) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'AutoArchiveMailbox: Invalid state parameter set' -Sev Error + return + } + + $DesiredState = $StateValue -eq 'enabled' + + try { + $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' -Select 'AutoEnableArchiveMailbox').AutoEnableArchiveMailbox + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the AutoArchiveMailbox state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + return + } + + $CorrectState = $CurrentState -eq $DesiredState + + $ExpectedValue = [PSCustomObject]@{ + AutoEnableArchiveMailbox = $DesiredState + } + $CurrentValue = [PSCustomObject]@{ + AutoEnableArchiveMailbox = $CurrentState + } + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate' + + if ($CorrectState) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox is already set to $StateValue." -Sev Info + } else { + try { + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ AutoEnableArchiveMailbox = $DesiredState } + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox has been set to $StateValue." -Sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to set auto enable archive mailbox to $StateValue. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($CorrectState) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox is correctly set to $StateValue." -Sev Info + } else { + Write-StandardsAlert -message "Auto enable archive mailbox is set to $CurrentState but should be $DesiredState." -object @{ CurrentState = $CurrentState; DesiredState = $DesiredState } -tenant $Tenant -standardName 'AutoArchiveMailbox' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox is set to $CurrentState but should be $DesiredState." -Sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.AutoArchiveMailbox' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'AutoArchiveMailbox' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index acdb5f3757f6..95765f0dd839 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -108,7 +108,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject $templateResult try { - $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj + $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj -CompareType 'ca' } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Error comparing CA policy: $($_.Exception.Message)" -sev Error Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Error comparing policy: $($_.Exception.Message)" -Tenant $Tenant @@ -117,7 +117,10 @@ function Invoke-CIPPStandardConditionalAccessTemplate { if (!$Compare) { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue $true -Tenant $Tenant } else { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue $Compare -Tenant $Tenant + #this can still be prettified but is for later. + $ExpectedValue = @{ 'Differences' = @() } + $CurrentValue = @{ 'Differences' = $Compare } + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index 5d5e4b55f0c3..adefbeb157ca 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig' $SMTPusers = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CASMailbox' -cmdParams @{ ResultSize = 'Unlimited' } | - Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } + Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableBasicAuthSMTP state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -110,7 +110,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { $CurrentValue = [PSCustomObject]@{ SmtpClientAuthenticationDisabled = $CurrentInfo.SmtpClientAuthenticationDisabled - UsersWithSmtpAuthEnabled = @($SMTPusers.PrimarySmtpAddress) + UsersWithSmtpAuthEnabled = $SMTPusers.PrimarySmtpAddress ? @($SMTPusers.PrimarySmtpAddress) : @() } $ExpectedValue = [PSCustomObject]@{ SmtpClientAuthenticationDisabled = $true diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index 931795adb66b..9e9e96d44c85 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -79,7 +79,7 @@ function Invoke-CIPPStandardRotateDKIM { Add-CIPPBPAField -FieldName 'DKIM' -FieldValue $DKIM -StoreAs json -Tenant $tenant $CurrentValue = @{ - domainsWith1024BitDKIM = if ($DKIM) { $DKIM.Identity } else { @() } + domainsWith1024BitDKIM = @(@($DKIM.Identity) | Where-Object { $_ }) } $ExpectedValue = @{ domainsWith1024BitDKIM = @() diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 index 91e18f80dc42..4534d08d73ef 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 @@ -181,12 +181,10 @@ function Invoke-CIPPStandardSecureScoreRemediation { }) } } - if ($ReportData.count -eq 0) { - $ReportData = $true - } + $CurrentValue = @{ - ControlsToUpdate = $ReportData + ControlsToUpdate = $ReportData ?? @() } $ExpectedValue = @{ ControlsToUpdate = @() diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 index 87cd2d6b39e4..88deaae2c234 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 @@ -94,12 +94,11 @@ function Invoke-CIPPStandardStaleEntraDevices { if ($StaleDevices.Count -gt 0) { $FieldValue = $StaleDevices | Select-Object -Property displayName, id, approximateLastSignInDateTime, accountEnabled, enrollmentProfileName, operatingSystem, managementType, profileType - } else { - $FieldValue = $true } + $CurrentValue = @{ StaleDevicesCount = $StaleDevices.Count - StaleDevices = @($FieldValue) + StaleDevices = ($FieldValue ? @($FieldValue) :@()) DeviceAgeThreshold = [int]$Settings.deviceAgeThreshold } $ExpectedValue = @{ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index 9e645e6597cd..d129d05ab124 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -80,7 +80,7 @@ function Invoke-CIPPStandardTransportRuleTemplate { $CurrentValue = @{ DeployedTransportRules = $existingRules.DisplayName | Where-Object { $rules.displayname -contains $_ } | Sort-Object - MissingTransportRules = $MissingRules + MissingTransportRules = $MissingRules ? @($MissingRules) : @() } $ExpectedValue = @{ DeployedTransportRules = $rules.displayname | Sort-Object diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 index 5c18d9d18d94..c124c14d352e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 @@ -80,7 +80,7 @@ function Invoke-CIPPStandardUserPreferredLanguage { $CurrentValue = @{ preferredLanguage = $preferredLanguage - incorrectUsers = $FieldValue + incorrectUsers = @($FieldValue) } $ExpectedValue = @{ preferredLanguage = $preferredLanguage diff --git a/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 index 3f8405c0d619..4e10fc0ffb80 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 @@ -17,7 +17,8 @@ function Expand-CIPPTenantGroups { $FilterValue = $_ # Group lookup if ($_.type -eq 'Group') { - $members = (Get-TenantGroups -GroupId $_.value).members + $GroupResult = Get-TenantGroups -GroupId $_.value + $members = if ($GroupResult) { $GroupResult.members } else { @() } $TenantList | Where-Object -Property customerId -In $members.customerId | ForEach-Object { $GroupMember = $_ [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 index 76db653fa17f..0b15f3f62402 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 @@ -67,6 +67,9 @@ function Get-TenantGroups { $script:TenantGroupsCache.MembersByGroup = @{} foreach ($Member in $script:TenantGroupsCache.Members) { $GId = $Member.GroupId + if (-not $GId) { + continue + } if (-not $script:TenantGroupsCache.MembersByGroup.ContainsKey($GId)) { $script:TenantGroupsCache.MembersByGroup[$GId] = [System.Collections.Generic.List[object]]::new() } diff --git a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 index 9cbc794e05fd..185018453aa2 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 @@ -49,6 +49,9 @@ function Update-CIPPDynamicTenantGroups { $script:TenantGroupMembersCache = @{} $AllGroupMembers = Get-CIPPAzDataTableEntity @MembersTable -Filter "PartitionKey eq 'Member'" foreach ($Member in $AllGroupMembers) { + if (-not $Member.GroupId) { + continue + } if (-not $script:TenantGroupMembersCache.ContainsKey($Member.GroupId)) { $script:TenantGroupMembersCache[$Member.GroupId] = [system.collections.generic.list[string]]::new() } diff --git a/Modules/CIPPCore/Public/Tools/Initialize-CIPPExcludedLicenses.ps1 b/Modules/CIPPCore/Public/Tools/Initialize-CIPPExcludedLicenses.ps1 new file mode 100644 index 000000000000..6ea65fb9f555 --- /dev/null +++ b/Modules/CIPPCore/Public/Tools/Initialize-CIPPExcludedLicenses.ps1 @@ -0,0 +1,93 @@ +function Initialize-CIPPExcludedLicenses { + <# + .SYNOPSIS + Initialize the ExcludedLicenses table from the default config file + + .DESCRIPTION + Reads the ExcludeSkuList.JSON config file and adds missing licenses to the ExcludedLicenses Azure Table. + Only adds licenses that don't already exist, preserving any manually added entries. + Use -Force to clear the table and reset to defaults. + + .FUNCTIONALITY + Internal + + .PARAMETER Force + If specified, clears existing entries before initializing from config + + .PARAMETER Headers + Request headers for logging + + .PARAMETER APIName + API name for logging purposes + + .EXAMPLE + Initialize-CIPPExcludedLicenses -Headers $Request.Headers -APIName 'ExecExcludeLicenses' + #> + [CmdletBinding()] + param( + [switch]$Force, + $Headers, + $APIName = 'Initialize-CIPPExcludedLicenses' + ) + + try { + $Table = Get-CIPPTable -TableName ExcludedLicenses + + # If Force is specified, clear existing entries first + if ($Force) { + $ExistingRows = Get-CIPPAzDataTableEntity @Table + foreach ($Row in $ExistingRows) { + Remove-AzDataTableEntity -Force @Table -Entity $Row + } + Write-LogMessage -API $APIName -headers $Headers -message 'Cleared existing excluded licenses' -Sev 'Info' + } + + # Get the config file path + $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase + $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent + $ConfigPath = Join-Path $CIPPRoot 'Config\ExcludeSkuList.JSON' + + if (-not (Test-Path $ConfigPath)) { + throw "Config file not found: $ConfigPath" + } + + $TableBaseData = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json -AsHashtable -Depth 10 + + # Get existing GUIDs to avoid overwriting manually added entries + $ExistingRows = Get-CIPPAzDataTableEntity @Table + $ExistingGUIDs = @($ExistingRows | ForEach-Object { $_.GUID }) + + $AddedCount = 0 + $SkippedCount = 0 + foreach ($Row in $TableBaseData) { + if ($Row.GUID -in $ExistingGUIDs) { + $SkippedCount++ + continue + } + $Row.PartitionKey = 'License' + $Row.RowKey = $Row.GUID + Add-CIPPAzDataTableEntity @Table -Entity ([pscustomobject]$Row) -Force | Out-Null + $AddedCount++ + } + + if ($Force) { + $Message = "Successfully performed full reset. Restored $AddedCount default licenses from config file" + } else { + $Message = "Successfully added $AddedCount missing licenses from config file ($SkippedCount already existed)" + } + Write-LogMessage -API $APIName -headers $Headers -message $Message -Sev 'Info' + + return @{ + Success = $true + Message = $Message + AddedCount = $AddedCount + SkippedCount = $SkippedCount + FullReset = [bool]$Force + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to initialize excluded licenses. $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage + throw $Result + } +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 index 480717d693bd..5de6978331df 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -10,8 +10,7 @@ function Sync-CippExtensionData { ) # Legacy cache system is deprecated - all extensions now use CippReportingDB - Write-Warning "Sync-CippExtensionData is deprecated. This scheduled task should be removed. Extensions now use Push-CIPPDBCacheData and Get-CippExtensionReportingData." - return + throw 'Sync-CippExtensionData is deprecated. This scheduled task should be removed. Extensions now use Push-CIPPDBCacheData and Get-CippExtensionReportingData.' $Table = Get-CIPPTable -TableName ExtensionSync $Extensions = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($SyncType)'" diff --git a/host.json b/host.json index ec9e853f1eed..fcd66850583e 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.1", + "defaultVersion": "10.0.3", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 1532420512a9..6a7144d3047f 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.1 +10.0.3