From 59a084a40502ac605ec8b0a47f80a39fb80458ab Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 15 Aug 2025 01:18:34 +0800 Subject: [PATCH 01/68] Add tenant offboarding defaults API and listing support Introduces Invoke-EditTenantOffboardingDefaults.ps1 to allow editing or clearing tenant offboarding defaults. Updates Invoke-ListTenants.ps1 to optionally include offboarding defaults in tenant listings when requested. --- .../Invoke-EditTenantOffboardingDefaults.ps1 | 81 +++++++++++++++++++ .../Tenant/Invoke-ListTenants.ps1 | 46 +++++++++-- 2 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 new file mode 100644 index 000000000000..9a6520f91c17 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 @@ -0,0 +1,81 @@ +using namespace System.Net + +function Invoke-EditTenantOffboardingDefaults { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.Config.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $customerId = $Request.Body.customerId + $offboardingDefaults = $Request.Body.offboardingDefaults + + if (!$customerId) { + $response = @{ + state = 'error' + resultText = 'Customer ID is required' + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = $response + }) + return + } + + $PropertiesTable = Get-CippTable -TableName 'TenantProperties' + + try { + # Convert the offboarding defaults to JSON string and ensure it's treated as a string + $jsonValue = [string]($offboardingDefaults | ConvertTo-Json -Compress) + + if ($jsonValue -and $jsonValue -ne '{}' -and $jsonValue -ne 'null' -and $jsonValue -ne '') { + # Save offboarding defaults + $offboardingEntity = @{ + PartitionKey = [string]$customerId + RowKey = [string]'OffboardingDefaults' + Value = [string]$jsonValue + } + $null = Add-CIPPAzDataTableEntity @PropertiesTable -Entity $offboardingEntity -Force + Write-LogMessage -headers $Headers -tenant $customerId -API $APIName -message "Updated tenant offboarding defaults" -Sev 'Info' + + $resultText = 'Tenant offboarding defaults updated successfully' + } else { + # Remove offboarding defaults if empty or null + $Existing = Get-CIPPAzDataTableEntity @PropertiesTable -Filter "PartitionKey eq '$customerId' and RowKey eq 'OffboardingDefaults'" + if ($Existing) { + Remove-AzDataTableEntity @PropertiesTable -Entity $Existing + Write-LogMessage -headers $Headers -tenant $customerId -API $APIName -message "Removed tenant offboarding defaults" -Sev 'Info' + } + + $resultText = 'Tenant offboarding defaults cleared successfully' + } + + $response = @{ + state = 'success' + resultText = $resultText + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $response + }) + } catch { + Write-LogMessage -headers $Headers -tenant $customerId -API $APINAME -message "Edit Tenant Offboarding Defaults failed. The error is: $($_.Exception.Message)" -Sev 'Error' + $response = @{ + state = 'error' + resultText = $_.Exception.Message + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = $response + }) + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 index 624e16be31c7..25b04748d5f3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 @@ -24,6 +24,8 @@ function Invoke-ListTenants { $AllTenantSelector = $Request.Query.AllTenantSelector } + $IncludeOffboardingDefaults = $Request.Query.IncludeOffboardingDefaults + # Clear Cache if ($Request.Body.ClearCache -eq $true) { $Results = Remove-CIPPCache -tenantsOnly $Request.Body.TenantsOnly @@ -75,16 +77,46 @@ function Invoke-ListTenants { $Tenants = $Tenants | Where-Object -Property customerId -In $TenantAccess } + # If offboarding defaults are requested, fetch them + if ($IncludeOffboardingDefaults -eq 'true' -and $Tenants) { + $PropertiesTable = Get-CippTable -TableName 'TenantProperties' + + # Get all offboarding defaults for all tenants in one query for performance + $AllOffboardingDefaults = Get-CIPPAzDataTableEntity @PropertiesTable -Filter "RowKey eq 'OffboardingDefaults'" + + # Add offboarding defaults to each tenant + foreach ($Tenant in $Tenants) { + $TenantDefaults = $AllOffboardingDefaults | Where-Object { $_.PartitionKey -eq $Tenant.customerId } + if ($TenantDefaults) { + try { + $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value ($TenantDefaults.Value | ConvertFrom-Json) -Force + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Failed to parse offboarding defaults for tenant $($Tenant.customerId): $($_.Exception.Message)" -Sev 'Warning' + $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value $null -Force + } + } else { + $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value $null -Force + } + } + } + if ($null -eq $TenantFilter -or $TenantFilter -eq 'null') { $TenantList = [system.collections.generic.list[object]]::new() if ($AllTenantSelector -eq $true) { - $TenantList.Add(@{ - customerId = 'AllTenants' - defaultDomainName = 'AllTenants' - displayName = '*All Tenants' - domains = 'AllTenants' - GraphErrorCount = 0 - }) | Out-Null + $AllTenantsObject = @{ + customerId = 'AllTenants' + defaultDomainName = 'AllTenants' + displayName = '*All Tenants' + domains = 'AllTenants' + GraphErrorCount = 0 + } + + # Add offboarding defaults to AllTenants object if requested + if ($IncludeOffboardingDefaults -eq 'true') { + $AllTenantsObject.offboardingDefaults = $null + } + + $TenantList.Add($AllTenantsObject) | Out-Null if (($Tenants).length -gt 1) { $TenantList.AddRange($Tenants) | Out-Null From cf728aa17baa36af868ec4f525ca45a2a3800dc0 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 Aug 2025 22:30:49 +0800 Subject: [PATCH 02/68] Add Power Platform and Power BI portal links Added Power Platform and Power BI portal URLs to tenant and user settings endpoints, and included them in NinjaOne tenant sync output. Also improved user-specific settings retrieval and error handling in Invoke-ListUserSettings.ps1. --- .../Users/Invoke-ListUserSettings.ps1 | 14 ++++++++++++++ .../Administration/Tenant/Invoke-ListTenants.ps1 | 4 +++- .../Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 index 1cfbd218703c..c0030f1135bc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 @@ -36,6 +36,15 @@ function Invoke-ListUserSettings { } } } + + try { + $UserSpecificSettings = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'UserSettings' and RowKey eq '$Username'" + $UserSpecificSettings = $UserSpecificSettings.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue + } + catch { + Write-Warning "Failed to convert UserSpecificSettings JSON: $($_.Exception.Message)" + } + #Get branding settings if ($UserSettings) { $brandingTable = Get-CippTable -tablename 'Config' @@ -44,6 +53,11 @@ function Invoke-ListUserSettings { $UserSettings | Add-Member -MemberType NoteProperty -Name 'customBranding' -Value $BrandingSettings -Force | Out-Null } } + + if ($UserSpecificSettings) { + $UserSettings | Add-Member -MemberType NoteProperty -Name 'UserSpecificSettings' -Value $UserSpecificSettings -Force | Out-Null + } + $StatusCode = [HttpStatusCode]::OK $Results = $UserSettings } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 index 624e16be31c7..096929785046 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 @@ -105,7 +105,9 @@ function Invoke-ListTenants { @{Name = 'portal_intune'; Expression = { "https://intune.microsoft.com/$($_.defaultDomainName)" } }, @{Name = 'portal_security'; Expression = { "https://security.microsoft.com/?tid=$($_.customerId)" } }, @{Name = 'portal_compliance'; Expression = { "https://purview.microsoft.com/?tid=$($_.customerId)" } }, - @{Name = 'portal_sharepoint'; Expression = { "/api/ListSharePointAdminUrl?tenantFilter=$($_.defaultDomainName)" } } + @{Name = 'portal_sharepoint'; Expression = { "/api/ListSharePointAdminUrl?tenantFilter=$($_.defaultDomainName)" } }, + @{Name = 'portal_platform'; Expression = { "https://admin.powerplatform.microsoft.com/account/login/$($_.customerId)" } }, + @{Name = 'portal_bi'; Expression = { "https://app.powerbi.com/admin-portal?ctid=$($_.customerId)" } } } } else { diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 155a400aea6a..493809046947 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -1565,6 +1565,16 @@ function Invoke-NinjaOneTenantSync { Name = 'Azure Portal' Link = "https://portal.azure.com/$($customer.defaultDomainName)" Icon = 'fas fa-server' + }, + @{ + Name = 'Power Platform Portal' + Link = "https://admin.powerplatform.microsoft.com/account/login/$($Customer.customerId)" + Icon = 'fa-solid fa-robot' + }, + @{ + Name = 'Power BI Portal' + Link = "https://app.powerbi.com/admin-portal?ctid=$($Customer.customerId)" + Icon = 'fas fa-bar-chart' } ) From 1ca079e232e690c88ad3c373745b0c44cedeeea2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:26:38 +0200 Subject: [PATCH 03/68] HVE and shared mailbox drawer. --- .../Administration/Invoke-ExecHVEUser.ps1 | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 new file mode 100644 index 000000000000..fca702d1c2e8 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 @@ -0,0 +1,108 @@ +using namespace System.Net + +function Invoke-ExecHVEUser { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $Results = [System.Collections.Generic.List[string]]::new() + $HVEUserObject = $Request.Body + $Tenant = $HVEUserObject.TenantFilter + + try { + # Check if Security Defaults are enabled + try { + $SecurityDefaults = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $Tenant + if ($SecurityDefaults.isEnabled -eq $true) { + $Results.Add('WARNING: Security Defaults are enabled for this tenant. HVE might not function.') + } + } catch { + $Results.Add('WARNING: Could not check Security Defaults status. Please verify authentication policies manually.') + } + + # Create the HVE user using New-MailUser + $BodyToShip = [pscustomobject] @{ + Name = $HVEUserObject.displayName + DisplayName = $HVEUserObject.displayName + PrimarySmtpAddress = $HVEUserObject.primarySMTPAddress + Password = $HVEUserObject.password + HVEAccount = $true + } + + $CreateHVERequest = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailUser' -cmdParams $BodyToShip + $Results.Add("Successfully created HVE user: $($HVEUserObject.primarySMTPAddress)") + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message "Created HVE user $($HVEUserObject.displayName) with email $($HVEUserObject.primarySMTPAddress)" -Sev 'Info' + + # Try to exclude from Conditional Access policies that block basic authentication + try { + # Get all Conditional Access policies + $CAPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $Tenant + + $BasicAuthPolicies = $CAPolicies | Where-Object { + $_.conditions.clientAppTypes -contains 'exchangeActiveSync' -or + $_.conditions.clientAppTypes -contains 'other' -or + $_.conditions.applications.includeApplications -contains 'All' -and + $_.grantControls.builtInControls -contains 'block' + } + + if ($BasicAuthPolicies) { + foreach ($Policy in $BasicAuthPolicies) { + try { + # Add the HVE user to the exclusions + $ExcludedUsers = @($Policy.conditions.users.excludeUsers) + if ($CreateHVERequest.ExternalDirectoryObjectId -notin $ExcludedUsers) { + + $ExcludeUsers = @($ExcludedUsers + $CreateHVERequest.ExternalDirectoryObjectId) + $UpdateBody = @{ + conditions = @{ + users = @{ + excludeUsers = @($ExcludeUsers | Sort-Object -Unique) + } + } + } + + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($Policy.id)" -type PATCH -body (ConvertTo-Json -InputObject $UpdateBody -Depth 10) -tenantid $Tenant + $Results.Add("Excluded HVE user from Conditional Access policy: $($Policy.displayName)") + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message "Excluded HVE user from CA policy: $($Policy.displayName)" -Sev 'Info' + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to exclude from CA policy '$($Policy.displayName)': $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Warning' -LogData $ErrorMessage + $Results.Add($Message) + } + } + } else { + $Results.Add('No Conditional Access policies blocking basic authentication found.') + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to check/update Conditional Access policies: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Warning' -LogData $ErrorMessage + $Results.Add($Message) + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to create HVE user: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Error' -LogData $ErrorMessage + $Results.Add($Message) + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = @($Results) } + }) +} From 73f75c8d95e8455a69e2b61b6cdccd6d67a5aa2f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:48:55 +0200 Subject: [PATCH 04/68] alert on licensed users with roles --- .../Get-CIPPAlertLicensedUsersWithRoles.ps1 | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLicensedUsersWithRoles.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLicensedUsersWithRoles.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLicensedUsersWithRoles.ps1 new file mode 100644 index 000000000000..d97625653c8e --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLicensedUsersWithRoles.ps1 @@ -0,0 +1,36 @@ +function Get-CIPPAlertLicensedUsersWithRoles { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + # Get all users with assigned licenses + $LicensedUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=userPrincipalName,assignedLicenses,displayName" -tenantid $TenantFilter | Where-Object { $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 } + if (-not $LicensedUsers -or $LicensedUsers.Count -eq 0) { + Write-Information "No licensed users found for tenant $TenantFilter" + return $true + } + # Get all directory roles with their members + $DirectoryRoles = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles?`$expand=members" -tenantid $TenantFilter + if (-not $DirectoryRoles -or $DirectoryRoles.Count -eq 0) { + Write-Information "No directory roles found for tenant $TenantFilter" + return + } + $UsersToAlertOn = $LicensedUsers | Where-Object { $_.userPrincipalName -in $DirectoryRoles.members.userPrincipalName } + + + if ($UsersToAlertOn.Count -gt 0) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $UsersToAlertOn + } else { + Write-Information "No licensed users with roles found for tenant $TenantFilter" + } + + +} From d3003b7b44df28c8fbc62c19a3b1e25f9a08a334 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 10:17:54 -0400 Subject: [PATCH 05/68] filter standardscompare list --- .../Tenant/Standards/Invoke-ListStandardsCompare.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 index 21b8b79272c7..ac4fea3c6f67 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 @@ -17,7 +17,8 @@ function Invoke-ListStandardsCompare { $Table.Filter = "PartitionKey eq '{0}'" -f $TenantFilter } - $Standards = Get-CIPPAzDataTableEntity @Table + $Tenants = Get-Tenants -IncludeErrors + $Standards = Get-CIPPAzDataTableEntity @Table | Where-Object { $_.PartitionKey -in $Tenants.defaultDomainName } #in the results we have objects starting with "standards." All these have to be converted from JSON. Do not do this is its a boolean <#$Results | ForEach-Object { @@ -57,7 +58,7 @@ function Invoke-ListStandardsCompare { $HexEncodedName = $Matches[2] $Chars = [System.Collections.Generic.List[char]]::new() for ($i = 0; $i -lt $HexEncodedName.Length; $i += 2) { - $Chars.Add([char][Convert]::ToInt32($HexEncodedName.Substring($i,2),16)) + $Chars.Add([char][Convert]::ToInt32($HexEncodedName.Substring($i, 2), 16)) } $FieldName = "$Prefix$(-join $Chars)" } From 6b3b9cb5757f9da392696bec35d7145f3c8b664a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 10:21:50 -0400 Subject: [PATCH 06/68] filter tenant alignment --- Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index 8d68ef26e5f3..b5c549f80db9 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -58,7 +58,8 @@ function Get-CIPPTenantAlignment { $Standards = if ($TenantFilter) { $AllStandards | Where-Object { $_.PartitionKey -eq $TenantFilter } } else { - $AllStandards + $Tenants = Get-Tenants -IncludeErrors + $AllStandards | Where-Object { $_.PartitionKey -in $Tenants.defaultDomainName } } # Build tenant standards data structure From 604e754d6c123c094bb6cdf83f76113e84279302 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 10:31:23 -0400 Subject: [PATCH 07/68] add ps version to version table --- profile.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/profile.ps1 b/profile.ps1 index 4c02022def7a..81fa396ebd19 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -43,11 +43,13 @@ if (!$LastStartup -or $CurrentVersion -ne $LastStartup.Version) { Write-Information "Version has changed from $($LastStartup.Version ?? 'None') to $CurrentVersion" if ($LastStartup) { $LastStartup.Version = $CurrentVersion + $LastStartup | Add-Member -MemberType NoteProperty -Name 'PSVersion' -Value $PSVersionTable.PSVersion.ToString() } else { $LastStartup = [PSCustomObject]@{ PartitionKey = 'Version' RowKey = $env:WEBSITE_SITE_NAME Version = $CurrentVersion + PSVersion = $PSVersionTable.PSVersion.ToString() } } Update-AzDataTableEntity @Table -Entity $LastStartup -Force -ErrorAction SilentlyContinue From 765395c88cb62685841679dec1c6ee513fa4db2a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 15:12:54 -0400 Subject: [PATCH 08/68] prevent one corrupted template from breaking list --- .../Tenant/Conditional/Invoke-ListCAtemplates.ps1 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 index 91a163298ce0..94a1f1bbeb1a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListCAtemplates.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListCAtemplates { +function Invoke-ListCAtemplates { <# .FUNCTIONALITY Entrypoint,AnyTenant @@ -39,9 +39,14 @@ Function Invoke-ListCAtemplates { $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'CATemplate'" $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { - $data = $_.JSON | ConvertFrom-Json -Depth 100 - $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.GUID -Force - $data + try { + $row = $_ + $data = $row.JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $row.GUID -Force + $data + } catch { + Write-Warning "Failed to process CA template: $($row.RowKey) - $($_.Exception.Message)" + } } | Sort-Object -Property displayName if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.query.id } From bb28ca81687580492be23c06effc1e2fd13381d6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 15:46:13 -0400 Subject: [PATCH 09/68] audit log search improvements --- .../AuditLogs/Get-CippAuditLogSearches.ps1 | 34 +++++++++++-------- .../AuditLogs/New-CippAuditLogSearch.ps1 | 30 +++++++++------- .../Alerts/Invoke-ExecAuditLogSearch.ps1 | 31 ++++++++++------- 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 index 693011c25aba..9301bb28aaba 100644 --- a/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/Get-CippAuditLogSearches.ps1 @@ -13,27 +13,31 @@ function Get-CippAuditLogSearches { [Parameter()] [switch]$ReadyToProcess ) - + $AuditLogSearchesTable = Get-CippTable -TableName 'AuditLogSearches' if ($ReadyToProcess.IsPresent) { - $AuditLogSearchesTable = Get-CippTable -TableName 'AuditLogSearches' $15MinutesAgo = (Get-Date).AddMinutes(-15).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $1DayAgo = (Get-Date).AddDays(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - $PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "Tenant eq '$TenantFilter' and (CippStatus eq 'Pending' or (CippStatus eq 'Processing' and Timestamp le datetime'$15MinutesAgo')) and Timestamp ge datetime'$1DayAgo'" | Sort-Object Timestamp + $PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "PartitionKey eq 'Search' and Tenant eq '$TenantFilter' and (CippStatus eq 'Pending' or (CippStatus eq 'Processing' and Timestamp le datetime'$15MinutesAgo')) and Timestamp ge datetime'$1DayAgo'" | Sort-Object Timestamp + } else { + $7DaysAgo = (Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $PendingQueries = Get-CIPPAzDataTableEntity @AuditLogSearchesTable -Filter "Tenant eq '$TenantFilter' and Timestamp ge datetime'$7DaysAgo'" + } - $BulkRequests = foreach ($PendingQuery in $PendingQueries) { - @{ - id = $PendingQuery.RowKey - url = 'security/auditLog/queries/' + $PendingQuery.RowKey - method = 'GET' - } + $BulkRequests = foreach ($PendingQuery in $PendingQueries) { + @{ + id = $PendingQuery.RowKey + url = 'security/auditLog/queries/' + $PendingQuery.RowKey + method = 'GET' } - if ($BulkRequests.Count -eq 0) { - return @() - } - $Queries = New-GraphBulkRequest -Requests @($BulkRequests) -AsApp $true -TenantId $TenantFilter | Select-Object -ExpandProperty body + } + if ($BulkRequests.Count -eq 0) { + return @() + } + $Queries = New-GraphBulkRequest -Requests @($BulkRequests) -AsApp $true -TenantId $TenantFilter | Select-Object -ExpandProperty body + + if ($ReadyToProcess.IsPresent) { $Queries = $Queries | Where-Object { $PendingQueries.RowKey -contains $_.id -and $_.status -eq 'succeeded' } - } else { - $Queries = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -AsApp $true -tenantid $TenantFilter } + return $Queries } diff --git a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 index 2cbbd76e9e8d..7b07fcfe8c0f 100644 --- a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 @@ -157,20 +157,26 @@ function New-CippAuditLogSearch { if ($PSCmdlet.ShouldProcess('Create a new audit log search for tenant ' + $TenantFilter)) { $Query = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/security/auditLog/queries' -body ($SearchParams | ConvertTo-Json -Compress) -tenantid $TenantFilter -AsApp $true + if ($ProcessLogs.IsPresent -and $Query.id) { - $Entity = [PSCustomObject]@{ - PartitionKey = [string]'Search' - RowKey = [string]$Query.id - Tenant = [string]$TenantFilter - DisplayName = [string]$DisplayName - StartTime = [datetime]$StartTime.ToUniversalTime() - EndTime = [datetime]$EndTime.ToUniversalTime() - Query = [string]($Query | ConvertTo-Json -Compress) - CippStatus = [string]'Pending' - } - $Table = Get-CIPPTable -TableName 'AuditLogSearches' - Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + $CippStatus = 'Pending' + } else { + $CippStatus = 'N/A' + } + + $Entity = [PSCustomObject]@{ + PartitionKey = [string]'Search' + RowKey = [string]$Query.id + Tenant = [string]$TenantFilter + DisplayName = [string]$DisplayName + StartTime = [datetime]$StartTime.ToUniversalTime() + EndTime = [datetime]$EndTime.ToUniversalTime() + Query = [string]($Query | ConvertTo-Json -Compress) + CippStatus = [string]$CippStatus } + $Table = Get-CIPPTable -TableName 'AuditLogSearches' + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + return $Query } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 index d0fb549f3ca5..db1bd87bf843 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 @@ -16,6 +16,8 @@ function Invoke-ExecAuditLogSearch { switch ($Action) { 'ProcessLogs' { + $Table = Get-CIPPTable -TableName 'AuditLogSearches' + $SearchId = $Request.Query.SearchId ?? $Request.Body.SearchId $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter if (!$SearchId) { @@ -26,20 +28,25 @@ function Invoke-ExecAuditLogSearch { return } - $Search = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/security/auditLog/queries/$SearchId" -AsApp $true -TenantId $TenantFilter - Write-Information ($Search | ConvertTo-Json -Depth 10) + $Existing = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'Search' and RowKey eq '$SearchId' and Tenant eq '$TenantFilter'" + if (!$Existing) { + $Search = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/security/auditLog/queries/$SearchId" -AsApp $true -TenantId $TenantFilter + Write-Information ($Search | ConvertTo-Json -Depth 10) - $Entity = [PSCustomObject]@{ - PartitionKey = [string]'Search' - RowKey = [string]$SearchId - Tenant = [string]$TenantFilter - DisplayName = [string]$Search.displayName - StartTime = [datetime]$Search.filterStartDateTime - EndTime = [datetime]$Search.filterEndDateTime - Query = [string]($Search | ConvertTo-Json -Compress) - CippStatus = [string]'Pending' + $Entity = [PSCustomObject]@{ + PartitionKey = [string]'Search' + RowKey = [string]$SearchId + Tenant = [string]$TenantFilter + DisplayName = [string]$Search.displayName + StartTime = [datetime]$Search.filterStartDateTime + EndTime = [datetime]$Search.filterEndDateTime + Query = [string]($Search | ConvertTo-Json -Compress) + CippStatus = [string]'Pending' + } + } else { + $Existing.CippStatus = 'Pending' } - $Table = Get-CIPPTable -TableName 'AuditLogSearches' + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null Write-LogMessage -headers $Headers -API $APIName -message "Queued search for processing: $($Search.displayName)" -Sev 'Info' -tenant $TenantFilter From 88c0ff01a8c721acbbcd4d21051d563341770560 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 15:47:21 -0400 Subject: [PATCH 10/68] fix pagination issues with alltenants queries --- .../Push-ListGraphRequestQueue.ps1 | 1 + .../CIPP/Core/Invoke-ListGraphRequest.ps1 | 4 ++-- .../GraphRequests/Get-GraphRequestList.ps1 | 21 ++++++++++++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 index dc7c33e94bd5..9c838711e42c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 @@ -35,6 +35,7 @@ function Push-ListGraphRequestQueue { ReverseTenantLookupProperty = $Item.ReverseTenantLookupProperty ReverseTenantLookup = $Item.ReverseTenantLookup AsApp = $Item.AsApp ?? $false + Caller = 'Push-ListGraphRequestQueue' SkipCache = $true } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 index 54ad46f0d529..9b7c58ae4b68 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListGraphRequest.ps1 @@ -81,7 +81,7 @@ function Invoke-ListGraphRequest { } if ($Request.Query.manualPagination) { - $GraphRequestParams.NoPagination = [System.Boolean]$Request.Query.manualPagination + $GraphRequestParams.ManualPagination = [System.Boolean]$Request.Query.manualPagination } if ($Request.Query.nextLink) { @@ -139,7 +139,7 @@ function Invoke-ListGraphRequest { if ($Results.Queued -eq $true) { $Metadata.Queued = $Results.Queued $Metadata.QueueMessage = $Results.QueueMessage - $Metadata.QueuedId = $Results.QueueId + $Metadata.QueueId = $Results.QueueId $Results = @() } } diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index 1370345fe9a1..1e94f51a74ff 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -33,6 +33,9 @@ function Get-GraphRequestList { .PARAMETER NoPagination Disable pagination + .PARAMETER ManualPagination + Enable manual pagination using nextLink + .PARAMETER CountOnly Only return count of results @@ -45,6 +48,12 @@ function Get-GraphRequestList { .PARAMETER ReverseTenantLookupProperty Property to perform reverse tenant lookup + .PARAMETER AsApp + Run the request as an application + + .PARAMETER Caller + Name of the calling function + #> [CmdletBinding()] param( @@ -61,11 +70,13 @@ function Get-GraphRequestList { [switch]$SkipCache, [switch]$ClearCache, [switch]$NoPagination, + [switch]$ManualPagination, [switch]$CountOnly, [switch]$NoAuthCheck, [switch]$ReverseTenantLookup, [string]$ReverseTenantLookupProperty = 'tenantId', - [boolean]$AsApp = $false + [boolean]$AsApp = $false, + [string]$Caller = 'Get-GraphRequestList' ) $SingleTenantThreshold = 8000 @@ -104,8 +115,8 @@ function Get-GraphRequestList { tenantid = $TenantFilter ComplexFilter = $true } - if ($NoPagination.IsPresent) { - $GraphRequest.noPagination = $NoPagination.IsPresent + if ($NoPagination.IsPresent -or $ManualPagination.IsPresent) { + $GraphRequest.noPagination = $true } if ($CountOnly.IsPresent) { $GraphRequest.CountOnly = $CountOnly.IsPresent @@ -297,9 +308,9 @@ function Get-GraphRequestList { if (!$QueueThresholdExceeded) { #nextLink should ONLY be used in direct calls with manual pagination. It should not be used in queueing - if ($NoPagination.IsPresent -and $nextLink -match '^https://.+') { $GraphRequest.uri = $nextLink } + if ($ManualPagination.IsPresent -and $nextLink -match '^https://.+') { $GraphRequest.uri = $nextLink } - $GraphRequestResults = New-GraphGetRequest @GraphRequest -Caller 'Get-GraphRequestList' -ErrorAction Stop + $GraphRequestResults = New-GraphGetRequest @GraphRequest -Caller $Caller -ErrorAction Stop $GraphRequestResults = $GraphRequestResults | Select-Object *, @{n = 'Tenant'; e = { $TenantFilter } }, @{n = 'CippStatus'; e = { 'Good' } } if ($ReverseTenantLookup -and $GraphRequestResults) { From 7a60c9da92b317904c00a0263c6b2cb58c07c39a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 15:47:36 -0400 Subject: [PATCH 11/68] add support for querying specific queue id --- .../Public/CippQueue/Invoke-ListCippQueue.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 index e9c33daf0ebf..b88c94dfb98c 100644 --- a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 @@ -12,10 +12,19 @@ function Invoke-ListCippQueue { Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' } + $QueueId = $Request.Query.QueueId + $CippQueue = Get-CippTable -TableName 'CippQueue' $CippQueueTasks = Get-CippTable -TableName 'CippQueueTasks' $3HoursAgo = (Get-Date).ToUniversalTime().AddHours(-3).ToString('yyyy-MM-ddTHH:mm:ssZ') - $CippQueueData = Get-CIPPAzDataTableEntity @CippQueue -Filter "PartitionKey eq 'CippQueue' and Timestamp ge datetime'$3HoursAgo'" | Sort-Object -Property Timestamp -Descending + + if ($QueueId) { + $Filter = "PartitionKey eq 'CippQueue' and RowKey eq '$QueueId'" + } else { + $Filter = "PartitionKey eq 'CippQueue' and Timestamp ge datetime'$3HoursAgo'" + } + + $CippQueueData = Get-CIPPAzDataTableEntity @CippQueue -Filter $Filter | Sort-Object -Property Timestamp -Descending $QueueData = foreach ($Queue in $CippQueueData) { $Tasks = Get-CIPPAzDataTableEntity @CippQueueTasks -Filter "PartitionKey eq 'Task' and QueueId eq '$($Queue.RowKey)'" | Where-Object { $_.Name } | Select-Object @{n = 'Timestamp'; exp = { $_.Timestamp.DateTime.ToUniversalTime() } }, Name, Status From 48644705edbe789750c789d1b5aac535af1c63d8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 17:57:55 -0400 Subject: [PATCH 12/68] fix timestamps --- Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 index b88c94dfb98c..7712bdabcfb8 100644 --- a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 @@ -27,7 +27,7 @@ function Invoke-ListCippQueue { $CippQueueData = Get-CIPPAzDataTableEntity @CippQueue -Filter $Filter | Sort-Object -Property Timestamp -Descending $QueueData = foreach ($Queue in $CippQueueData) { - $Tasks = Get-CIPPAzDataTableEntity @CippQueueTasks -Filter "PartitionKey eq 'Task' and QueueId eq '$($Queue.RowKey)'" | Where-Object { $_.Name } | Select-Object @{n = 'Timestamp'; exp = { $_.Timestamp.DateTime.ToUniversalTime() } }, Name, Status + $Tasks = Get-CIPPAzDataTableEntity @CippQueueTasks -Filter "PartitionKey eq 'Task' and QueueId eq '$($Queue.RowKey)'" | Where-Object { $_.Name } | Select-Object @{n = 'Timestamp'; exp = { $_.Timestamp } }, Name, Status $TaskStatus = @{} $Tasks | Group-Object -Property Status | ForEach-Object { $TaskStatus.$($_.Name) = $_.Count @@ -65,7 +65,7 @@ function Invoke-ListCippQueue { PercentRunning = [math]::Round((($TotalRunning / $Queue.TotalTasks) * 100), 1) Tasks = @($Tasks) Status = $Queue.Status - Timestamp = $Queue.Timestamp.DateTime.ToUniversalTime() + Timestamp = $Queue.Timestamp } } From 506411881232c953f29813b42f6ab13854b8a1fe Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 18:02:16 -0400 Subject: [PATCH 13/68] Update Invoke-ListCippQueue.ps1 --- Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 index 7712bdabcfb8..b89468291fa0 100644 --- a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 @@ -63,7 +63,7 @@ function Invoke-ListCippQueue { PercentComplete = [math]::Round(((($TotalCompleted + $TotalFailed) / $Queue.TotalTasks) * 100), 1) PercentFailed = [math]::Round((($TotalFailed / $Queue.TotalTasks) * 100), 1) PercentRunning = [math]::Round((($TotalRunning / $Queue.TotalTasks) * 100), 1) - Tasks = @($Tasks) + Tasks = @($Tasks | Sort-Object -Descending Timestamp) Status = $Queue.Status Timestamp = $Queue.Timestamp } From be2db2f9e9719ea4f43f7b3ced2ab5b889fa156e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 18:29:34 -0400 Subject: [PATCH 14/68] fix forever loading bug by not waiting for all tenants before reporting data not fully fixed, just allows partial results on alltenant query Also add queue id for tracking problematic ones --- .../Administration/Invoke-ListMailboxRules.ps1 | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 index 0c0b1c954322..8bf0e01b33ee 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListMailboxRules { +function Invoke-ListMailboxRules { <# .FUNCTIONALITY Entrypoint @@ -28,18 +28,15 @@ Function Invoke-ListMailboxRules { $Metadata = @{} # If a queue is running, we will not start a new one - if ($RunningQueue) { + if ($RunningQueue -and !$Rows) { $Metadata = [PSCustomObject]@{ QueueMessage = "Still loading data for $TenantFilter. Please check back in a few more minutes" + QueueId = $RunningQueue.RowKey } [PSCustomObject]@{ Waiting = $true } } elseif ((!$Rows -and !$RunningQueue) -or ($TenantFilter -eq 'AllTenants' -and ($Rows | Measure-Object).Count -eq 1)) { - # If no rows are found and no queue is running, we will start a new one - $Metadata = [PSCustomObject]@{ - QueueMessage = "Loading data for $TenantFilter. Please check back in 1 minute" - } if ($TenantFilter -eq 'AllTenants') { $Tenants = Get-Tenants -IncludeErrors | Select-Object defaultDomainName @@ -49,6 +46,12 @@ Function Invoke-ListMailboxRules { $Type = $TenantFilter } $Queue = New-CippQueueEntry -Name "Mailbox Rules ($Type)" -Reference $QueueReference -TotalTasks ($Tenants | Measure-Object).Count + # If no rows are found and no queue is running, we will start a new one + $Metadata = [PSCustomObject]@{ + QueueMessage = "Loading data for $TenantFilter. Please check back in 1 minute" + QueueId = $Queue.RowKey + } + $Batch = $Tenants | Select-Object defaultDomainName, @{Name = 'FunctionName'; Expression = { 'ListMailboxRulesQueue' } }, @{Name = 'QueueName'; Expression = { $_.defaultDomainName } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } } if (($Batch | Measure-Object).Count -gt 0) { $InputObject = [PSCustomObject]@{ @@ -66,6 +69,9 @@ Function Invoke-ListMailboxRules { $Rows = $Rows | Where-Object -Property Tenant -EQ $TenantFilter $Rows = $Rows } + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } $GraphRequest = $Rows | ForEach-Object { $NewObj = $_.Rules | ConvertFrom-Json -ErrorAction SilentlyContinue $NewObj | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $_.Tenant -Force From dd5778518711cb91da9826c0b98d4a042751adfd Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 21 Aug 2025 18:37:56 -0400 Subject: [PATCH 15/68] add queue id to other long running api calls --- .../Spamfilter/Invoke-ListMailQuarantine.ps1 | 5 +++++ .../Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 | 5 +++++ .../HTTP Functions/Security/Invoke-ExecAlertsList.ps1 | 8 +++++++- .../HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 | 7 ++++++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 index a3d2f2629e95..a403fa79168a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 @@ -29,6 +29,7 @@ function Invoke-ListMailQuarantine { if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey } [PSCustomObject]@{ Waiting = $true @@ -39,6 +40,7 @@ function Invoke-ListMailQuarantine { $Queue = New-CippQueueEntry -Name 'Mail Quarantine - All Tenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'MailQuarantineOrchestrator' @@ -57,6 +59,9 @@ function Invoke-ListMailQuarantine { Waiting = $true } } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } $messages = $Rows foreach ($message in $messages) { $messageObj = $message.QuarantineMessage | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index 2b06bf09a7c7..13389253bd3c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -75,6 +75,7 @@ function Invoke-ExecJITAdmin { if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading JIT Admin data for all tenants. Please check back in a few more minutes.' + QueueId = $RunningQueue.RowKey } } elseif (!$Rows -and !$RunningQueue) { $TenantList = Get-Tenants -IncludeErrors @@ -82,6 +83,7 @@ function Invoke-ExecJITAdmin { $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading JIT Admin data for all tenants. Please check back in a few minutes.' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'JITAdminOrchestrator' @@ -97,6 +99,9 @@ function Invoke-ExecJITAdmin { } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } # There is data in the cache, so we will use that Write-Information "Found $($Rows.Count) rows in the cache" foreach ($row in $Rows) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 index 04f876786b21..07eb65d497db 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecAlertsList { +function Invoke-ExecAlertsList { <# .FUNCTIONALITY Entrypoint @@ -68,6 +68,7 @@ Function Invoke-ExecAlertsList { if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey } [PSCustomObject]@{ Waiting = $true @@ -78,6 +79,7 @@ Function Invoke-ExecAlertsList { $Queue = New-CippQueueEntry -Name 'Alerts List - All Tenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'AlertsOrchestrator' @@ -97,6 +99,10 @@ Function Invoke-ExecAlertsList { InstanceId = $InstanceId } } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } + $Alerts = $Rows $AlertsObj = foreach ($Alert in $Alerts) { $AlertInfo = $Alert.Alert | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 index 5a3991b4df41..bf26894a4e85 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecIncidentsList { +function Invoke-ExecIncidentsList { <# .FUNCTIONALITY Entrypoint @@ -52,6 +52,7 @@ Function Invoke-ExecIncidentsList { if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey } } elseif (!$Rows -and !$RunningQueue) { # If no rows are found and no queue is running, we will start a new one @@ -59,6 +60,7 @@ Function Invoke-ExecIncidentsList { $Queue = New-CippQueueEntry -Name 'Incidents - All Tenants' -Link '/security/reports/incident-report?customerId=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'IncidentOrchestrator' @@ -74,6 +76,9 @@ Function Invoke-ExecIncidentsList { } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } $Incidents = $Rows foreach ($incident in $Incidents) { $IncidentObj = $incident.Incident | ConvertFrom-Json From 95ddb009e25fc57e28f199be55c205ebbe21a6ca Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:15:26 +0200 Subject: [PATCH 16/68] WAAttachmentRestrictions --- ...-CIPPStandardOWAAttachmentRestrictions.ps1 | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 new file mode 100644 index 000000000000..c498716a5a04 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 @@ -0,0 +1,137 @@ +function Invoke-CIPPStandardOWAAttachmentRestrictions { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) OWAAttachmentRestrictions + .SYNOPSIS + (Label) Restrict Email Attachments on Unmanaged Devices + .DESCRIPTION + (Helptext) Restricts how users on unmanaged devices can interact with email attachments in Outlook on the web and new Outlook for Windows. Prevents downloading attachments or blocks viewing them entirely. + (DocsDescription) This standard configures the OWA mailbox policy to restrict access to email attachments on unmanaged devices. Users can be prevented from downloading attachments (but can view/edit via Office Online) or blocked from seeing attachments entirely. This helps prevent data exfiltration through email attachments on devices not managed by the organization. + .NOTES + CAT + Exchange Standards + TAG + "zero_trust" + "unmanaged_devices" + "attachment_restrictions" + "data_loss_prevention" + ADDEDCOMPONENT + {"type":"select","name":"standards.OWAAttachmentRestrictions.ConditionalAccessPolicy","label":"Attachment Restriction Policy","options":[{"label":"Read Only (View/Edit via Office Online, no download)","value":"ReadOnly"},{"label":"Read Only Plus Attachments Blocked (Cannot see attachments)","value":"ReadOnlyPlusAttachmentsBlocked"}],"defaultValue":"ReadOnlyPlusAttachmentsBlocked"} + IMPACT + Medium Impact + ADDEDDATE + 2025-08-22 + POWERSHELLEQUIVALENT + Set-OwaMailboxPolicy -Identity "OwaMailboxPolicy-Default" -ConditionalAccessPolicy ReadOnlyPlusAttachmentsBlocked + RECOMMENDEDBY + "Microsoft Zero Trust" + "CIPP" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + https://learn.microsoft.com/en-us/security/zero-trust/zero-trust-identity-device-access-policies-workloads#exchange-online-recommendations-for-zero-trust + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'OWAAttachmentRestrictions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + + if ($TestResult -eq $false) { + Write-Host "We're exiting as the correct license is not present for this standard." + return $true + } #we're done. + + # Input validation + $ValidPolicies = @('ReadOnly', 'ReadOnlyPlusAttachmentsBlocked') + if ($Settings.ConditionalAccessPolicy.value -notin $ValidPolicies) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "OWAAttachmentRestrictions: Invalid ConditionalAccessPolicy parameter set. Must be one of: $($ValidPolicies -join ', ')" -sev Error + return + } + + try { + # Get the default OWA mailbox policy + $CurrentPolicy = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OwaMailboxPolicy' -cmdParams @{ Identity = 'OwaMailboxPolicy-Default' } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the OWA Attachment Restrictions state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + + $StateIsCorrect = $CurrentPolicy.ConditionalAccessPolicy -eq $Settings.ConditionalAccessPolicy.value + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "OWA attachment restrictions are already set to $($Settings.ConditionalAccessPolicy)" -sev Info + } else { + try { + $cmdParams = @{ + Identity = 'OwaMailboxPolicy-Default' + ConditionalAccessPolicy = $Settings.ConditionalAccessPolicy.value + } + + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OwaMailboxPolicy' -cmdParams $cmdParams + + $PolicyDescription = switch ($Settings.ConditionalAccessPolicy.value) { + 'ReadOnly' { 'Read Only (users can view/edit attachments via Office Online but cannot download)' } + 'ReadOnlyPlusAttachmentsBlocked' { 'Read Only Plus Attachments Blocked (users cannot see attachments at all)' } + } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set OWA attachment restrictions to: $PolicyDescription" -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set OWA attachment restrictions. $($ErrorMessage.NormalizedError)" -sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + $PolicyDescription = switch ($Settings.ConditionalAccessPolicy.value) { + 'ReadOnly' { 'Read Only (view/edit via Office Online, no download)' } + 'ReadOnlyPlusAttachmentsBlocked' { 'Read Only Plus Attachments Blocked (cannot see attachments)' } + } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "OWA attachment restrictions are correctly set to: $PolicyDescription" -sev Info + } else { + $CurrentDescription = switch ($CurrentPolicy.ConditionalAccessPolicy) { + 'ReadOnly' { 'Read Only (view/edit via Office Online, no download)' } + 'ReadOnlyPlusAttachmentsBlocked' { 'Read Only Plus Attachments Blocked (cannot see attachments)' } + $null { 'Not configured (full access to attachments)' } + default { $CurrentPolicy.ConditionalAccessPolicy } + } + + $RequiredDescription = switch ($Settings.ConditionalAccessPolicy.value) { + 'ReadOnly' { 'Read Only (view/edit via Office Online, no download)' } + 'ReadOnlyPlusAttachmentsBlocked' { 'Read Only Plus Attachments Blocked (cannot see attachments)' } + } + + $AlertMessage = "OWA attachment restrictions are set to '$CurrentDescription' but should be '$RequiredDescription'" + Write-StandardsAlert -message $AlertMessage -object @{ + CurrentPolicy = $CurrentPolicy.ConditionalAccessPolicy + RequiredPolicy = $Settings.ConditionalAccessPolicy + PolicyName = $CurrentPolicy.Name + CurrentDescription = $CurrentDescription + RequiredDescription = $RequiredDescription + } -tenant $Tenant -standardName 'OWAAttachmentRestrictions' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info + } + } + + if ($Settings.report -eq $true) { + if ($StateIsCorrect) { + Set-CIPPStandardsCompareField -FieldName 'standards.OWAAttachmentRestrictions' -FieldValue $true -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'OWAAttachmentRestrictions' -FieldValue $true -StoreAs bool -Tenant $Tenant + } else { + $ReportData = @{ + CurrentPolicy = $CurrentPolicy.ConditionalAccessPolicy + RequiredPolicy = $Settings.ConditionalAccessPolicy.value + PolicyName = $CurrentPolicy.Name + IsCompliant = $false + Description = 'OWA attachment restrictions not properly configured for unmanaged devices' + } + Set-CIPPStandardsCompareField -FieldName 'standards.OWAAttachmentRestrictions' -FieldValue $ReportData -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'OWAAttachmentRestrictions' -FieldValue $ReportData -StoreAs json -Tenant $Tenant + } + } +} From 8b38f85d4e4f26bd10bd452bbdc12e3bcb8f7c9e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 22 Aug 2025 09:44:20 -0400 Subject: [PATCH 17/68] info logging tweak --- Modules/CippEntrypoints/CippEntrypoints.psm1 | 4 ++-- profile.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 524a3f6fd0ad..5c17e4cb8a90 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -40,7 +40,7 @@ function Receive-CippHttpTrigger { $Request = $Request | ConvertTo-Json -Depth 100 | ConvertFrom-Json Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName $FunctionName = 'Invoke-{0}' -f $Request.Params.CIPPEndpoint - Write-Information "Function: $($Request.Params.CIPPEndpoint)" + Write-Information "API: $($Request.Params.CIPPEndpoint)" $HttpTrigger = @{ Request = [pscustomobject]($Request) @@ -61,7 +61,7 @@ function Receive-CippHttpTrigger { }) return } - + try { Write-Information "Access: $Access" if ($Access) { diff --git a/profile.ps1 b/profile.ps1 index 81fa396ebd19..fbbcb0ed9df0 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -37,7 +37,7 @@ try { Set-Location -Path $PSScriptRoot $CurrentVersion = (Get-Content .\version_latest.txt).trim() $Table = Get-CippTable -tablename 'Version' -Write-Information "Function: $($env:WEBSITE_SITE_NAME) Version: $CurrentVersion" +Write-Information "Function App: $($env:WEBSITE_SITE_NAME) Version: $CurrentVersion" $LastStartup = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'Version' and RowKey eq '$($env:WEBSITE_SITE_NAME)'" if (!$LastStartup -or $CurrentVersion -ne $LastStartup.Version) { Write-Information "Version has changed from $($LastStartup.Version ?? 'None') to $CurrentVersion" From af8f675d2b718b222ff25c3654fcfca6f6e12921 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 22 Aug 2025 09:49:20 -0400 Subject: [PATCH 18/68] optimize partition keys --- .../Administration/Invoke-ListMailboxRules.ps1 | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 index 8bf0e01b33ee..394c0b810f82 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 @@ -19,8 +19,12 @@ function Invoke-ListMailboxRules { $Table = Get-CIPPTable -TableName cachembxrules if ($TenantFilter -ne 'AllTenants') { - $Table.Filter = "Tenant eq '$TenantFilter'" + $Table.Filter = "PartitionKey eq 'MailboxRules' and Tenant eq '$TenantFilter'" + } else { + $Table.Filter = "PartitionKey eq 'MailboxRules'" } + + Write-Information 'Getting cached mailbox rules' $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-1) $PartitionKey = 'MailboxRules' $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey @@ -29,6 +33,7 @@ function Invoke-ListMailboxRules { $Metadata = @{} # If a queue is running, we will not start a new one if ($RunningQueue -and !$Rows) { + Write-Information "Queue is already running for $TenantFilter" $Metadata = [PSCustomObject]@{ QueueMessage = "Still loading data for $TenantFilter. Please check back in a few more minutes" QueueId = $RunningQueue.RowKey @@ -37,7 +42,7 @@ function Invoke-ListMailboxRules { Waiting = $true } } elseif ((!$Rows -and !$RunningQueue) -or ($TenantFilter -eq 'AllTenants' -and ($Rows | Measure-Object).Count -eq 1)) { - + Write-Information "No cached mailbox rules found for $TenantFilter, starting new orchestration" if ($TenantFilter -eq 'AllTenants') { $Tenants = Get-Tenants -IncludeErrors | Select-Object defaultDomainName $Type = 'All Tenants' @@ -65,10 +70,6 @@ function Invoke-ListMailboxRules { } } else { - if ($TenantFilter -ne 'AllTenants') { - $Rows = $Rows | Where-Object -Property Tenant -EQ $TenantFilter - $Rows = $Rows - } $Metadata = [PSCustomObject]@{ QueueId = $RunningQueue.RowKey ?? $null } From b817b4f6f9921e6dc6d5a3b1b6eebe82936ad1fe Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 22 Aug 2025 10:12:52 -0400 Subject: [PATCH 19/68] add better queue filtering --- .../CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 | 7 +++++-- .../Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 | 2 +- .../Administration/Invoke-ListMailboxRules.ps1 | 2 +- .../Transport/Invoke-ListTransportRules.ps1 | 9 +++++++-- .../Administration/Users/Invoke-ExecJITAdmin.ps1 | 2 +- .../HTTP Functions/Security/Invoke-ExecAlertsList.ps1 | 2 +- .../HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 | 2 +- .../Public/GraphRequests/Get-GraphRequestList.ps1 | 4 ++-- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 index b89468291fa0..34e9fbfa191a 100644 --- a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 @@ -5,14 +5,15 @@ function Invoke-ListCippQueue { .ROLE CIPP.Core.Read #> - param($Request = $null, $TriggerMetadata = $null) + param($Request = $null, $TriggerMetadata = $null, $Reference = $null, $QueueId = $null) if ($Request) { $APIName = $Request.Params.CIPPEndpoint Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' } - $QueueId = $Request.Query.QueueId + $QueueId = $Request.Query.QueueId ?? $QueueId + $Reference = $Request.Query.Reference ?? $Reference $CippQueue = Get-CippTable -TableName 'CippQueue' $CippQueueTasks = Get-CippTable -TableName 'CippQueueTasks' @@ -20,6 +21,8 @@ function Invoke-ListCippQueue { if ($QueueId) { $Filter = "PartitionKey eq 'CippQueue' and RowKey eq '$QueueId'" + } elseif ($Reference) { + $Filter = "PartitionKey eq 'CippQueue' and Reference eq '$Reference' and Timestamp ge datetime'$3HoursAgo'" } else { $Filter = "PartitionKey eq 'CippQueue' and Timestamp ge datetime'$3HoursAgo'" } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 index d3ef1e2711c8..d4674c69038b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdateTenants.ps1 @@ -5,7 +5,7 @@ function Push-UpdateTenants { #> Param($Item) $QueueReference = 'UpdateTenants' - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } $Queue = New-CippQueueEntry -Name 'Update Tenants' -Reference $QueueReference -TotalTasks 1 try { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 index 394c0b810f82..165a06a82e21 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 @@ -28,7 +28,7 @@ function Invoke-ListMailboxRules { $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-1) $PartitionKey = 'MailboxRules' $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } $Metadata = @{} # If a queue is running, we will not start a new one diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 index 3f4b5c65e019..f9166f317897 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListTransportRules { +function Invoke-ListTransportRules { <# .FUNCTIONALITY Entrypoint @@ -28,12 +28,13 @@ Function Invoke-ListTransportRules { $Filter = "PartitionKey eq '$PartitionKey'" $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-60) $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } # If a queue is running, we will not start a new one if ($RunningQueue) { $Metadata = [PSCustomObject]@{ QueueMessage = 'Still loading transport rules for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey } } elseif (!$Rows -and !$RunningQueue) { # If no rows are found and no queue is running, we will start a new one @@ -41,6 +42,7 @@ Function Invoke-ListTransportRules { $Queue = New-CippQueueEntry -Name 'Transport Rules - All Tenants' -Link '/email/transport/list-rules?tenantFilter=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count $Metadata = [PSCustomObject]@{ QueueMessage = 'Loading transport rules for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey } $InputObject = [PSCustomObject]@{ OrchestratorName = 'TransportRuleOrchestrator' @@ -57,6 +59,9 @@ Function Invoke-ListTransportRules { Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null } else { # Return cached data + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } $Rules = $Rows foreach ($rule in $Rules) { $RuleObj = $rule.TransportRule | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index 13389253bd3c..82ffd30f340d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -70,7 +70,7 @@ function Invoke-ExecJITAdmin { $QueueReference = '{0}-{1}' -f $Request.Query.TenantFilter, $PartitionKey # $TenantFilter is 'AllTenants' Write-Information "QueueReference: $QueueReference" - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } if ($RunningQueue) { $Metadata = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 index 07eb65d497db..3671f073f2f1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 @@ -63,7 +63,7 @@ function Invoke-ExecAlertsList { $Filter = "PartitionKey eq '$PartitionKey'" $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-30) $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } # If a queue is running, we will not start a new one if ($RunningQueue) { $Metadata = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 index bf26894a4e85..0a848352323d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 @@ -47,7 +47,7 @@ function Invoke-ExecIncidentsList { $Filter = "PartitionKey eq '$PartitionKey'" $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-30) $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } # If a queue is running, we will not start a new one if ($RunningQueue) { $Metadata = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index 1e94f51a74ff..60d0795734a6 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -157,7 +157,7 @@ function Get-GraphRequestList { $Type = 'Queue' Write-Information "Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } } elseif (!$SkipCache.IsPresent -and !$ClearCache.IsPresent -and !$CountOnly.IsPresent) { if ($TenantFilter -eq 'AllTenants' -or $Count -gt $SingleTenantThreshold) { $Table = Get-CIPPTable -TableName $TableName @@ -171,7 +171,7 @@ function Get-GraphRequestList { $Type = 'Cache' Write-Information "Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq $QueueReference -and $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } } } } catch { From a9ed4003623f3c9cd07179d27bc8054a359315cb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 22 Aug 2025 10:41:32 -0400 Subject: [PATCH 20/68] nested column selection for graph explorer --- .../Public/GraphRequests/Get-GraphRequestList.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index 60d0795734a6..e85303b8a640 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -100,6 +100,14 @@ function Get-GraphRequestList { $Item.Value = $Item.Value.ToString().ToLower() } if ($Item.Value) { + if ($Item.Key -eq '$select' -or $Item.Key -eq 'select') { + $Columns = $Item.Value -split ',' + $ActualCols = foreach ($Col in $Columns) { + $Col -split '\.' | Select-Object -First 1 + } + $Item.Value = ($ActualCols | Sort-Object -Unique) -join ',' + } + $ParamCollection.Add($Item.Key, $Item.Value) } } From d367e169feab9a576efd5f4a432c505e6e8fbd98 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 22 Aug 2025 12:17:10 -0400 Subject: [PATCH 21/68] variable expansion improvements --- .../Public/Get-CIPPTextReplacement.ps1 | 8 +++++++- .../GraphRequests/Get-GraphRequestList.ps1 | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 index 68e801215b8a..1a6355d444bc 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 @@ -36,7 +36,8 @@ function Get-CIPPTextReplacement { '%windir%', '%programfiles%', '%programfiles(x86)%', - '%programdata%' + '%programdata%', + '%cippuserschema%' ) $Tenant = Get-Tenants -TenantFilter $TenantFilter @@ -77,5 +78,10 @@ function Get-CIPPTextReplacement { # Partner specific replacements $Text = $Text -replace '%partnertenantid%', $env:TenantID $Text = $Text -replace '%samappid%', $env:ApplicationID + + if ($Text -match '%cippuserschema%') { + $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1 + $Text = $Text -replace '%cippuserschema%', $Schema.id + } return $Text } diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index e85303b8a640..62a90e29616e 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -105,10 +105,11 @@ function Get-GraphRequestList { $ActualCols = foreach ($Col in $Columns) { $Col -split '\.' | Select-Object -First 1 } - $Item.Value = ($ActualCols | Sort-Object -Unique) -join ',' + $Value = ($ActualCols | Sort-Object -Unique) -join ',' + } else { + $Value = $Item.Value } - - $ParamCollection.Add($Item.Key, $Item.Value) + $ParamCollection.Add($Item.Key, $Value) } } $GraphQuery.Query = $ParamCollection.ToString() @@ -142,7 +143,16 @@ function Get-GraphRequestList { $GraphQuery = [System.UriBuilder]('https://graph.microsoft.com/{0}/{1}' -f $Version, $Endpoint) $ParamCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) foreach ($Item in ($Parameters.GetEnumerator() | Sort-Object -CaseSensitive -Property Key)) { - $Value = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $Item.Value + if ($Item.Key -eq '$select' -or $Item.Key -eq 'select') { + $Columns = $Item.Value -split ',' + $ActualCols = foreach ($Col in $Columns) { + $Col -split '\.' | Select-Object -First 1 + } + $Value = ($ActualCols | Sort-Object -Unique) -join ',' + } else { + $Value = $Item.Value + } + $Value = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $Value $ParamCollection.Add($Item.Key, $Value) } $GraphQuery.Query = $ParamCollection.ToString() From 46f5fd2813b46b2cdf39092988419fea515e95e6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 22 Aug 2025 12:47:47 -0400 Subject: [PATCH 22/68] add cippurl replacement --- Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 index 1a6355d444bc..2b6740fa9111 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 @@ -37,7 +37,8 @@ function Get-CIPPTextReplacement { '%programfiles%', '%programfiles(x86)%', '%programdata%', - '%cippuserschema%' + '%cippuserschema%', + '%cippurl%' ) $Tenant = Get-Tenants -TenantFilter $TenantFilter @@ -83,5 +84,13 @@ function Get-CIPPTextReplacement { $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1 $Text = $Text -replace '%cippuserschema%', $Schema.id } + + if ($Text -match '%cippurl%') { + $ConfigTable = Get-CIPPTable -tablename 'Config' + $Config = Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'InstanceProperties' and RowKey eq 'CIPPURL'" + if ($Config) { + $Text = $Text -replace '%cippurl%', $Config.Value + } + } return $Text } From 60fea48fb4cc9b8417eb0fb779e933e8e4ed73cb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 22 Aug 2025 12:49:13 -0400 Subject: [PATCH 23/68] add defaultdomain --- Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 index 2b6740fa9111..54e7e11e0923 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 @@ -38,7 +38,8 @@ function Get-CIPPTextReplacement { '%programfiles(x86)%', '%programdata%', '%cippuserschema%', - '%cippurl%' + '%cippurl%', + '%defaultdomain%' ) $Tenant = Get-Tenants -TenantFilter $TenantFilter @@ -73,6 +74,7 @@ function Get-CIPPTextReplacement { #default replacements for all tenants: %tenantid% becomes $tenant.customerId, %tenantfilter% becomes $tenant.defaultDomainName, %tenantname% becomes $tenant.displayName $Text = $Text -replace '%tenantid%', $Tenant.customerId $Text = $Text -replace '%tenantfilter%', $Tenant.defaultDomainName + $Text = $Text -replace '%defaultdomain%', $Tenant.defaultDomainName $Text = $Text -replace '%initialdomain%', $Tenant.initialDomainName $Text = $Text -replace '%tenantname%', $Tenant.displayName From 846df85a4d34b5fd84162fdef6c8404ef9e3004d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 22 Aug 2025 19:59:35 +0200 Subject: [PATCH 24/68] Improvements to rule management for tenants --- .../Push-ListMailboxRulesQueue.ps1 | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListMailboxRulesQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListMailboxRulesQueue.ps1 index 1c826fbfa0cb..b26603bd13e0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListMailboxRulesQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListMailboxRulesQueue.ps1 @@ -12,14 +12,20 @@ function Push-ListMailboxRulesQueue { $Table = Get-CIPPTable -TableName cachembxrules try { - $Rules = New-ExoRequest -tenantid $domainName -cmdlet 'Get-Mailbox' -Select 'userPrincipalName,GUID' | ForEach-Object -Parallel { - Import-Module CIPPCore - $MbxRules = New-ExoRequest -Anchor $_.UserPrincipalName -tenantid $using:domainName -cmdlet 'Get-InboxRule' -cmdParams @{Mailbox = $_.GUID; IncludeHidden = $true } | Where-Object { $_.Name -ne 'Junk E-Mail Rule' -and $_.Name -notlike 'Microsoft.Exchange.OOF.*' } - foreach ($Rule in $MbxRules) { - $Rule | Add-Member -NotePropertyName 'UserPrincipalName' -NotePropertyValue $_.userPrincipalName - $Rule + $Mailboxes = New-ExoRequest -tenantid $domainName -cmdlet 'Get-Mailbox' -Select 'userPrincipalName,GUID' + $Request = $Mailboxes | ForEach-Object { + @{ + OperationGuid = $_.UserPrincipalName + CmdletInput = @{ + CmdletName = 'Get-InboxRule' + Parameters = @{ + Mailbox = $_.UserPrincipalName + } + } } } + + $Rules = New-ExoBulkRequest -tenantid $domainName -cmdletArray @($Request) | Where-Object { $_.Identity } if (($Rules | Measure-Object).Count -gt 0) { $GraphRequest = foreach ($Rule in $Rules) { [PSCustomObject]@{ From 3842bad72b9ef50d1978ac56adbc4bc44ecffe65 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 Aug 2025 18:25:29 +0800 Subject: [PATCH 25/68] Feat: Contact Permissions Management --- .../Invoke-ExecModifyContactPerms.ps1 | 116 ++++++++++++++++++ .../Invoke-ListContactPermissions.ps1 | 41 +++++++ .../Public/Set-CIPPContactPermission.ps1 | 64 ++++++++++ 3 files changed, 221 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPContactPermission.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 new file mode 100644 index 000000000000..894765475b17 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 @@ -0,0 +1,116 @@ +using namespace System.Net + +function Invoke-ExecModifyContactPerms { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $Username = $Request.Body.userID + $TenantFilter = $Request.Body.tenantFilter + $Permissions = $Request.Body.permissions + + Write-LogMessage -headers $Headers -API $APIName -message "Processing request for user: $Username, tenant: $TenantFilter" -Sev 'Debug' + + if ($null -eq $Username) { + Write-LogMessage -headers $Headers -API $APIName -message 'Username is null' -Sev 'Error' + $body = [pscustomobject]@{'Results' = @('Username is required') } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = $Body + }) + return + } + + try { + $UserId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)" -tenantid $TenantFilter).id + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved user ID: $UserId" -Sev 'Debug' + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Failed to get user ID: $($_.Exception.Message)" -Sev 'Error' + $body = [pscustomobject]@{'Results' = @("Failed to get user ID: $($_.Exception.Message)") } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::NotFound + Body = $Body + }) + return + } + + $Results = [System.Collections.Generic.List[string]]::new() + $HasErrors = $false + + # Convert permissions to array format if it's an object with numeric keys + if ($Permissions -is [PSCustomObject]) { + if ($Permissions.PSObject.Properties.Name -match '^\d+$') { + $Permissions = $Permissions.PSObject.Properties.Value + } else { + $Permissions = @($Permissions) + } + } + + Write-LogMessage -headers $Headers -API $APIName -message "Processing $($Permissions.Count) permission entries" -Sev 'Debug' + + foreach ($Permission in $Permissions) { + Write-LogMessage -headers $Headers -API $APIName -message "Processing permission: $($Permission | ConvertTo-Json)" -Sev 'Debug' + + $PermissionLevel = $Permission.PermissionLevel.value ?? $Permission.PermissionLevel + $Modification = $Permission.Modification + $CanViewPrivateItems = $Permission.CanViewPrivateItems ?? $false + $FolderName = $Permission.FolderName ?? 'Contact' + $SendNotificationToUser = $Permission.SendNotificationToUser ?? $false + + Write-LogMessage -headers $Headers -API $APIName -message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems, FolderName: $FolderName" -Sev 'Debug' + + # Handle UserID as array or single value + $TargetUsers = @($Permission.UserID | ForEach-Object { $_.value ?? $_ }) + + Write-LogMessage -headers $Headers -API $APIName -message "Target Users: $($TargetUsers -join ', ')" -Sev 'Debug' + + foreach ($TargetUser in $TargetUsers) { + try { + Write-LogMessage -headers $Headers -API $APIName -message "Processing target user: $TargetUser" -Sev 'Debug' + $Params = @{ + APIName = $APIName + Headers = $Headers + RemoveAccess = if ($Modification -eq 'Remove') { $TargetUser } else { $null } + TenantFilter = $TenantFilter + UserID = $UserId + folderName = $FolderName + UserToGetPermissions = $TargetUser + LoggingName = $TargetUser + Permissions = $PermissionLevel + SendNotificationToUser = $SendNotificationToUser + } + + # Write-Host "Request params: $($Params | ConvertTo-Json)" + $Result = Set-CIPPContactPermission @Params + + $null = $Results.Add($Result) + } catch { + $HasErrors = $true + $null = $Results.Add("$($_.Exception.Message)") + } + } + } + + if ($Results.Count -eq 0) { + Write-LogMessage -headers $Headers -API $APIName -message 'No results were generated from the operation' -Sev 'Warning' + $null = $Results.Add('No results were generated from the operation. Please check the logs for more details.') + $HasErrors = $true + } + + $Body = [pscustomobject]@{'Results' = @($Results) } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = if ($HasErrors) { [HttpStatusCode]::InternalServerError } else { [HttpStatusCode]::OK } + Body = $Body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 new file mode 100644 index 000000000000..32162c24e3f8 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContactPermissions.ps1 @@ -0,0 +1,41 @@ +using namespace System.Net + +Function Invoke-ListContactPermissions { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + $UserID = $Request.Query.UserID + $TenantFilter = $Request.Query.tenantFilter + + try { + $GetContactParam = @{Identity = $UserID; FolderScope = 'Contacts' } + $ContactFolder = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -anchor $UserID -cmdParams $GetContactParam | Select-Object -First 1 -ExcludeProperty *data.type* + $ContactParam = @{Identity = "$($UserID):\$($ContactFolder.name)" } + $Mailbox = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Mailbox' -cmdParams @{Identity = $UserID } + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderPermission' -anchor $UserID -cmdParams $ContactParam -UseSystemMailbox $true | Select-Object Identity, User, AccessRights, FolderName, @{ Name = 'MailboxInfo'; Expression = { $Mailbox } } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Contact permissions listed for $($TenantFilter)" -sev Debug + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPCore/Public/Set-CIPPContactPermission.ps1 b/Modules/CIPPCore/Public/Set-CIPPContactPermission.ps1 new file mode 100644 index 000000000000..ae61cc3b3458 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPContactPermission.ps1 @@ -0,0 +1,64 @@ +function Set-CIPPContactPermission { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + $APIName = 'Set Contact Permissions', + $Headers, + $RemoveAccess, + $TenantFilter, + $UserID, + $FolderName, + $UserToGetPermissions, + $LoggingName, + $Permissions, + [bool]$SendNotificationToUser = $false + ) + + try { + # If a pretty logging name is not provided, use the ID instead + if ([string]::IsNullOrWhiteSpace($LoggingName) -and $RemoveAccess) { + $LoggingName = $RemoveAccess + } elseif ([string]::IsNullOrWhiteSpace($LoggingName) -and $UserToGetPermissions) { + $LoggingName = $UserToGetPermissions + } + + $ContactParam = [PSCustomObject]@{ + Identity = "$($UserID):\$FolderName" + AccessRights = @($Permissions) + User = $UserToGetPermissions + SendNotificationToUser = $SendNotificationToUser + } + + if ($RemoveAccess) { + if ($PSCmdlet.ShouldProcess("$UserID\$FolderName", "Remove permissions for $LoggingName")) { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{Identity = "$($UserID):\$FolderName"; User = $RemoveAccess } + $Result = "Successfully removed access for $LoggingName from contact folder $($ContactParam.Identity)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info + } + } else { + if ($PSCmdlet.ShouldProcess("$UserID\$FolderName", "Set permissions for $LoggingName to $Permissions")) { + try { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxFolderPermission' -cmdParams $ContactParam -Anchor $UserID + } catch { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-MailboxFolderPermission' -cmdParams $ContactParam -Anchor $UserID + } + + $Result = "Successfully set permissions on contact folder $($ContactParam.Identity). The user $LoggingName now has $Permissions permissions on this folder." + + if ($SendNotificationToUser) { + $Result += ' A notification has been sent to the user.' + } + + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Warning "Error changing contact permissions $($_.Exception.Message)" + Write-Information $_.InvocationInfo.PositionMessage + $Result = "Failed to set contact permissions for $LoggingName on $UserID : $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error -LogData $ErrorMessage + throw $Result + } + + return $Result +} From 3921be4b5addf926479d0ddbf1cf1b5331b35331 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 Aug 2025 23:00:08 +0800 Subject: [PATCH 26/68] Feat: Remove deprecated add-ins for "Report Phishing" and "Report Message" --- ...ke-CIPPStandardLegacyEmailReportAddins.ps1 | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 new file mode 100644 index 000000000000..6b800db9a986 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 @@ -0,0 +1,155 @@ +function Invoke-CIPPStandardLegacyEmailReportAddins { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) LegacyEmailReportAddins + .SYNOPSIS + (Label) Remove Legacy Email Report Add-ins + .DESCRIPTION + (Helptext) This standard removes the legacy "Report Message" and "Report Phishing" add-ins from Outlook. These have been superseded by newer reporting mechanisms. + (DocsDescription) This standard removes the legacy "Report Message" and "Report Phishing" add-ins from Outlook. These have been superseded by newer reporting mechanisms. + .NOTES + CAT + Exchange Standards + TAG + ADDEDCOMPONENT + IMPACT + Low Impact + ADDEDDATE + 2025-08-26 + POWERSHELLEQUIVALENT + Admin Center API + 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) + + # Define the legacy add-ins to remove + $LegacyAddins = @( + @{ + AssetId = 'WA200002469' + ProductId = '3f32746a-0586-4c54-b8ce-d3b611c5b6c8' + Name = 'Report Phishing' + }, + @{ + AssetId = 'WA104381180' + ProductId = '6046742c-3aee-485e-a4ac-92ab7199db2e' + Name = 'Report Message' + } + ) + + try { + $CurrentApps = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/fd/addins/api/apps?workloads=AzureActiveDirectory,WXPO,MetaOS,Teams,SharePoint' + $InstalledApps = $CurrentApps.apps + } + catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the installed add-ins for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + + # Check which legacy add-ins are currently installed + $AddinsToRemove = [System.Collections.Generic.List[PSCustomObject]]::new() + $InstalledLegacyAddins = [System.Collections.Generic.List[string]]::new() + + foreach ($LegacyAddin in $LegacyAddins) { + $InstalledAddin = $InstalledApps | Where-Object { $_.assetId -eq $LegacyAddin.AssetId -or $_.productId -eq $LegacyAddin.ProductId } + if ($InstalledAddin) { + $InstalledLegacyAddins.Add($LegacyAddin.Name) + $AddinsToRemove.Add([PSCustomObject]@{ + AppsourceAssetID = $LegacyAddin.AssetId + ProductID = $LegacyAddin.ProductId + Command = 'UNDEPLOY' + Workload = 'WXPO' + }) + } + } + + $StateIsCorrect = ($AddinsToRemove.Count -eq 0) + $RemediationPerformed = $false + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Legacy Email Report Add-ins are already removed.' -Sev Info + } else { + foreach ($AddinToRemove in $AddinsToRemove) { + try { + $Body = @{ + Locale = 'en-US' + WorkloadManagementList = @($AddinToRemove) + } | ConvertTo-Json -Depth 10 -Compress + + $GraphRequest = @{ + tenantID = $Tenant + uri = 'https://admin.microsoft.com/fd/addins/api/apps' + scope = 'https://admin.microsoft.com/.default' + AsApp = $false + Type = 'POST' + ContentType = 'application/json; charset=utf-8' + Body = $Body + } + + $Response = New-GraphPostRequest @GraphRequest + $AddinName = ($LegacyAddins | Where-Object { $_.AssetId -eq $AddinToRemove.AppsourceAssetID }).Name + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Successfully initiated removal of $AddinName add-in" -Sev Info + $RemediationPerformed = $true + } catch { + $AddinName = ($LegacyAddins | Where-Object { $_.AssetId -eq $AddinToRemove.AppsourceAssetID }).Name + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to remove $AddinName add-in" -Sev Error -LogData $_ + } + } + } + } + + # If we performed remediation and need to report/alert, get fresh state + if ($RemediationPerformed -and ($Settings.alert -eq $true -or $Settings.report -eq $true)) { + try { + $FreshApps = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/fd/addins/api/apps?workloads=AzureActiveDirectory,WXPO,MetaOS,Teams,SharePoint' + $FreshInstalledApps = $FreshApps.apps + + # Check fresh state + $FreshInstalledLegacyAddins = [System.Collections.Generic.List[string]]::new() + foreach ($LegacyAddin in $LegacyAddins) { + $InstalledAddin = $FreshInstalledApps | Where-Object { $_.assetId -eq $LegacyAddin.AssetId -or $_.productId -eq $LegacyAddin.ProductId } + if ($InstalledAddin) { + $FreshInstalledLegacyAddins.Add($LegacyAddin.Name) + } + } + + # Use fresh state for reporting/alerting + $StateIsCorrect = ($FreshInstalledLegacyAddins.Count -eq 0) + $InstalledLegacyAddins = $FreshInstalledLegacyAddins + } + catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get fresh add-in state after remediation for $Tenant" -Sev Warning + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Legacy Email Report Add-ins are not installed.' -sev Info + } else { + $InstalledAddinsText = ($InstalledLegacyAddins -join ', ') + Write-StandardsAlert -message "Legacy Email Report Add-ins are still installed: $InstalledAddinsText" -tenant $tenant -standardName 'LegacyEmailReportAddins' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message "Legacy Email Report Add-ins are still installed: $InstalledAddinsText" -sev Alert + } + } + + if ($Settings.report -eq $true) { + $ReportData = if ($StateIsCorrect) { + $true + } else { + @{ + InstalledLegacyAddins = $InstalledLegacyAddins + Status = 'Legacy add-ins still installed' + } + } + Set-CIPPStandardsCompareField -FieldName 'standards.LegacyEmailReportAddins' -FieldValue $ReportData -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'LegacyEmailReportAddins' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + } +} From 314d9006060bfca80fc0904cf7f170fb0b62475f Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 Aug 2025 23:07:30 +0800 Subject: [PATCH 27/68] Update openapi spec --- openapi.json | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/openapi.json b/openapi.json index c94c00948d65..013d331ba238 100644 --- a/openapi.json +++ b/openapi.json @@ -3640,6 +3640,259 @@ } } }, + "/ListContactPermissions": { + "get": { + "description": "ListContactPermissions - Retrieves contact folder permissions for a specified user", + "summary": "ListContactPermissions", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "UserID", + "in": "query", + "description": "The user ID to retrieve contact permissions for" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query", + "description": "The tenant filter to specify which tenant" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Identity": { + "type": "string", + "description": "The identity of the contact folder" + }, + "User": { + "type": "string", + "description": "The user who has permissions" + }, + "AccessRights": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The access rights granted to the user" + }, + "FolderName": { + "type": "string", + "description": "The name of the contact folder" + }, + "MailboxInfo": { + "type": "object", + "description": "Information about the mailbox" + } + } + } + } + } + }, + "description": "Successfully retrieved contact permissions" + }, + "403": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + }, + "description": "Forbidden - insufficient permissions" + } + } + } + }, + "/ExecModifyContactPerms": { + "post": { + "description": "ExecModifyContactPerms - Modifies contact folder permissions for a specified user", + "summary": "ExecModifyContactPerms", + "tags": [ + "POST" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "userID", + "tenantFilter", + "permissions" + ], + "properties": { + "userID": { + "type": "string", + "description": "The user ID whose contact permissions will be modified" + }, + "tenantFilter": { + "type": "string", + "description": "The tenant filter to specify which tenant" + }, + "permissions": { + "type": "array", + "description": "Array of permission objects to apply", + "items": { + "type": "object", + "properties": { + "PermissionLevel": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Permission level (e.g., Owner, PublishingEditor, Editor, PublishingAuthor, Author, NonEditingAuthor, Reviewer, Contributor, AvailabilityOnly, LimitedDetails)" + } + } + }, + "Modification": { + "type": "string", + "description": "Type of modification (Add, Remove)", + "enum": ["Add", "Remove"] + }, + "UserID": { + "type": "array", + "description": "Array of target users to grant/remove permissions", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "User ID or email address" + } + } + } + }, + "CanViewPrivateItems": { + "type": "boolean", + "description": "Whether the user can view private items", + "default": false + }, + "FolderName": { + "type": "string", + "description": "Name of the contact folder", + "default": "Contact" + }, + "SendNotificationToUser": { + "type": "boolean", + "description": "Whether to send notification to the user", + "default": false + } + }, + "required": [ + "PermissionLevel", + "Modification", + "UserID" + ] + } + } + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of result messages for each permission operation" + } + } + } + } + }, + "description": "Successfully processed contact permission modifications" + }, + "400": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Error messages" + } + } + } + } + }, + "description": "Bad Request - Missing required parameters" + }, + "404": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Error messages" + } + } + } + } + }, + "description": "Not Found - User ID not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Error messages" + } + } + } + } + }, + "description": "Internal Server Error - Operation failed" + } + } + } + }, "/ListMailboxRules": { "get": { "description": "ListMailboxRules", From f8d63a2035c9ac7b1ff86e05de2581efbcd2c20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 28 Aug 2025 22:25:34 +0200 Subject: [PATCH 28/68] Feat: Add Invoke-ExecSyncVPP function for VPP token synchronization --- .../Applications/Invoke-ExecSyncVPP.ps1 | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 new file mode 100644 index 000000000000..d1bf225814cf --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 @@ -0,0 +1,47 @@ +using namespace System.Net + +function Invoke-ExecSyncVPP { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.Application.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev Debug + + $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter + try { + # Get all VPP tokens and sync them + $VppTokens = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/vppTokens' -tenantid $TenantFilter | Where-Object { $_.state -eq 'valid' } + + if ($null -eq $VppTokens -or $VppTokens.Count -eq 0) { + $Result = 'No VPP tokens found' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + } else { + $SyncCount = 0 + foreach ($Token in $VppTokens) { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/vppTokens/$($Token.id)/syncLicenses" -tenantid $TenantFilter + $SyncCount++ + } + $Result = "Successfully started VPP sync for $SyncCount tokens" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = 'Failed to start VPP sync' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) + +} From 53248f3709eb06aba3ce05ceeee094008df75dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 29 Aug 2025 15:05:14 +0200 Subject: [PATCH 29/68] Feat: add MDO/Email & collaboration alerts API simpler Refactor API request in Invoke-ExecOffice365AlertsList to simplify URI construction Add Invoke-ExecSetOffice365Alert function add AllTenants support rename to MDO --- .../Push-ExecMdoAlertsListAllTenants.ps1 | 48 +++++++++++ .../Security/Invoke-ExecMdoAlertsList.ps1 | 84 +++++++++++++++++++ .../Security/Invoke-ExecSetMdoAlert.ps1 | 74 ++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecMdoAlertsListAllTenants.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecSetMdoAlert.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecMdoAlertsListAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecMdoAlertsListAllTenants.ps1 new file mode 100644 index 000000000000..15cab5352f97 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecMdoAlertsListAllTenants.ps1 @@ -0,0 +1,48 @@ +function Push-ExecMdoAlertsListAllTenants { + <# + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + $Tenant = Get-Tenants -TenantFilter $Item.customerId + $domainName = $Tenant.defaultDomainName + $Table = Get-CIPPTable -TableName 'cachealertsandincidents' + + try { + # Get MDO alerts using the specific endpoint and filter + $Alerts = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/security/alerts_v2?`$filter=serviceSource eq 'microsoftDefenderForOffice365'" -tenantid $domainName + + foreach ($Alert in $Alerts) { + $GUID = (New-Guid).Guid + $GraphRequest = @{ + MdoAlert = [string]($Alert | ConvertTo-Json -Depth 10) + RowKey = [string]$GUID + PartitionKey = 'MdoAlert' + Tenant = [string]$domainName + } + Add-CIPPAzDataTableEntity @Table -Entity $GraphRequest -Force | Out-Null + } + + } catch { + $GUID = (New-Guid).Guid + $AlertText = ConvertTo-Json -InputObject @{ + Tenant = $domainName + displayName = "Could not connect to Tenant: $($_.Exception.Message)" + id = '' + severity = 'CIPP' + status = 'Failed' + createdDateTime = (Get-Date).ToString('s') + category = 'Unknown' + description = 'Could not connect' + serviceSource = 'microsoftDefenderForOffice365' + } + $GraphRequest = @{ + MdoAlert = [string]$AlertText + RowKey = [string]$GUID + PartitionKey = 'MdoAlert' + Tenant = [string]$domainName + } + Add-CIPPAzDataTableEntity @Table -Entity $GraphRequest -Force | Out-Null + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 new file mode 100644 index 000000000000..0427a09ab5df --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 @@ -0,0 +1,84 @@ +using namespace System.Net + +function Invoke-ExecMDOAlertsList { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.Alert.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter + + try { + $GraphRequest = if ($TenantFilter -ne 'AllTenants') { + # Single tenant functionality + New-GraphGetRequest -uri "https://graph.microsoft.com/beta/security/alerts_v2?`$filter=serviceSource eq 'microsoftDefenderForOffice365'" -tenantid $TenantFilter + } else { + # AllTenants functionality + $Table = Get-CIPPTable -TableName cachealertsandincidents + $PartitionKey = 'MdoAlert' + $Filter = "PartitionKey eq '$PartitionKey'" + $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-30) + $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + # If a queue is running, we will not start a new one + if ($RunningQueue) { + $Metadata = [PSCustomObject]@{ + QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey + } + } elseif (!$Rows -and !$RunningQueue) { + # If no rows are found and no queue is running, we will start a new one + $TenantList = Get-Tenants -IncludeErrors + $Queue = New-CippQueueEntry -Name 'MDO Alerts - All Tenants' -Link '/security/reports/mdo-alerts?customerId=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count + $Metadata = [PSCustomObject]@{ + QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey + } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'MdoAlertsOrchestrator' + QueueFunction = @{ + FunctionName = 'GetTenants' + QueueId = $Queue.RowKey + TenantParams = @{ + IncludeErrors = $true + } + DurableName = 'ExecMdoAlertsListAllTenants' + } + SkipLog = $true + } + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null + } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } + $Alerts = $Rows + foreach ($alert in $Alerts) { + ConvertFrom-Json -InputObject $alert.MdoAlert -Depth 10 + } + } + } + } catch { + $Body = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + } + if (!$Body) { + $StatusCode = [HttpStatusCode]::OK + $Body = [PSCustomObject]@{ + Results = @($GraphRequest) + Metadata = $Metadata + } + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecSetMdoAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecSetMdoAlert.ps1 new file mode 100644 index 000000000000..2da36eadb959 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecSetMdoAlert.ps1 @@ -0,0 +1,74 @@ +using namespace System.Net + +function Invoke-ExecSetMdoAlert { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.Incident.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $AlertId = $Request.Query.GUID ?? $Request.Body.GUID + $Status = $Request.Query.Status ?? $Request.Body.Status + $Assigned = $Request.Query.Assigned ?? $Request.Body.Assigned ?? ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json).userDetails + $Classification = $Request.Query.Classification ?? $Request.Body.Classification + $Determination = $Request.Query.Determination ?? $Request.Body.Determination + $Result = '' + $AssignBody = @{} + + try { + # Set received status + if ($null -ne $Status) { + $AssignBody.status = $Status + $Result += 'Set status for incident ' + $AlertId + ' to ' + $Status + } + + # Set received classification and determination + if ($null -ne $Classification) { + if ($null -eq $Determination) { + # Maybe some poindexter tries to send a classification without a determination + throw + } + + $AssignBody.classification = $Classification + $AssignBody.determination = $Determination + $Result += 'Set classification & determination for incident ' + $AlertId + ' to ' + $Classification + ' ' + $Determination + } + + # Set received assignee + if ($null -ne $Assigned) { + $AssignBody.assignedTo = $Assigned + if ($null -eq $Status) { + $Result += 'Set assigned for incident ' + $AlertId + ' to ' + $Assigned + } + } + + # Convert hashtable to JSON + $AssignBodyJson = $AssignBody | ConvertTo-Json -Compress + + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/security/alerts_v2/$AlertId" -type PATCH -tenantid $TenantFilter -body $AssignBodyJson -asApp $true + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to update incident $AlertId : $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} From fb6d624bfb712e5cd53f965514a552068e86a982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 30 Aug 2025 23:04:25 +0200 Subject: [PATCH 30/68] Refactor BitLocker key functions for to return ID and implement logging --- .../MEM/Invoke-ExecGetRecoveryKey.ps1 | 4 ++-- .../CIPPCore/Public/Get-CIPPBitlockerKey.ps1 | 24 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 index 2ab268e9a852..e62aa8b30293 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecGetRecoveryKey { +function Invoke-ExecGetRecoveryKey { <# .FUNCTIONALITY Entrypoint @@ -19,7 +19,7 @@ Function Invoke-ExecGetRecoveryKey { $GUID = $Request.Query.GUID ?? $Request.Body.GUID try { - $Result = Get-CIPPBitLockerKey -device $GUID -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers + $Result = Get-CIPPBitLockerKey -Device $GUID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers $StatusCode = [HttpStatusCode]::OK } catch { $Result = $_.Exception.Message diff --git a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 index 291d5b06f63e..6035975ad1d2 100644 --- a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 @@ -1,22 +1,34 @@ -function Get-CIPPBitlockerKey { +function Get-CIPPBitLockerKey { [CmdletBinding()] param ( - $device, + $Device, $TenantFilter, $APIName = 'Get BitLocker key', $Headers ) try { - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/informationProtection/bitlocker/recoveryKeys?`$filter=deviceId eq '$($device)'" -tenantid $TenantFilter | ForEach-Object { - (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/informationProtection/bitlocker/recoveryKeys/$($_.id)?`$select=key" -tenantid $TenantFilter).key + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/informationProtection/bitlocker/recoveryKeys?`$filter=deviceId eq '$($Device)'" -tenantid $TenantFilter | + ForEach-Object { + $BitLockerKeyObject = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/informationProtection/bitlocker/recoveryKeys/$($_.id)?`$select=key" -tenantid $TenantFilter) + [PSCustomObject]@{ + resultText = "Id: $($_.id) Key: $($BitLockerKeyObject.key)" + copyField = $BitLockerKeyObject.key + state = 'success' + } + } + + if ($GraphRequest.Count -eq 0) { + Write-LogMessage -headers $Headers -API $APIName -message "No BitLocker recovery keys found for $($Device)" -Sev Info -tenant $TenantFilter + return "No BitLocker recovery keys found for $($Device)" } + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved BitLocker recovery keys for $($Device)" -Sev Info -tenant $TenantFilter return $GraphRequest } catch { $ErrorMessage = Get-CippException -Exception $_ - $Result = "Could not retrieve BitLocker recovery key for $($device). Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + $Result = "Could not retrieve BitLocker recovery key for $($Device). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev Error -tenant $TenantFilter -LogData $ErrorMessage throw $Result } } From ec1042644c14793526e8ec433c6488211a2a4f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 30 Aug 2025 23:51:29 +0200 Subject: [PATCH 31/68] Fix: Add ExternalAudience param based on input --- Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 index ced3fad0a5af..19f7fae9e445 100644 --- a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 @@ -18,8 +18,9 @@ function Set-CIPPOutOfOffice { try { $CmdParams = @{ - Identity = $UserID - AutoReplyState = $State + Identity = $UserID + AutoReplyState = $State + ExternalAudience = 'None' } if ($PSBoundParameters.ContainsKey('InternalMessage')) { @@ -28,6 +29,7 @@ function Set-CIPPOutOfOffice { if ($PSBoundParameters.ContainsKey('ExternalMessage')) { $CmdParams.ExternalMessage = $ExternalMessage + $CmdParams.ExternalAudience = 'All' } if ($State -eq 'Scheduled') { From be8b9c8a6d963096fe9188409be4fa0d59401a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 31 Aug 2025 00:24:22 +0200 Subject: [PATCH 32/68] Fix: Add GOV exchange licenses to the standards license check --- .../Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 | 2 +- .../Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 | 6 +++--- .../Public/Standards/Invoke-CIPPStandardAuditLog.ps1 | 4 ++-- .../Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardBookings.ps1 | 2 +- .../Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 | 2 +- .../Standards/Invoke-CIPPStandardDelegateSentItems.ps1 | 2 +- .../Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 | 2 +- .../Standards/Invoke-CIPPStandardDeployMailContact.ps1 | 2 +- ...Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 | 2 +- .../Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 2 +- .../Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 | 2 +- .../Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 | 2 +- .../Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 | 2 +- .../Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 | 2 +- .../Invoke-CIPPStandardEXODisableAutoForwarding.ps1 | 2 +- .../Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 | 4 ++-- .../Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 | 2 +- .../Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 | 2 +- .../Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 | 2 +- .../Invoke-CIPPStandardExchangeConnectorTemplate.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 | 2 +- .../Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 | 2 +- .../Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 | 2 +- .../Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 | 2 +- .../Standards/Invoke-CIPPStandardMessageExpiration.ps1 | 2 +- .../Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 | 2 +- .../Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 | 2 +- .../Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 | 2 +- .../Standards/Invoke-CIPPStandardPhishingSimulations.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 | 2 +- .../Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 | 2 +- .../Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 | 2 +- .../Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 | 2 +- .../Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 | 2 +- .../Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 | 2 +- .../Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 | 2 +- .../Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 | 2 +- .../Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 | 2 +- .../Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 | 2 +- .../Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 | 2 +- .../Invoke-CIPPStandardTwoClickEmailProtection.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardcalDefault.ps1 | 2 +- 53 files changed, 57 insertions(+), 57 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index 49e881023c41..f03d6d1f35d6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardAddDKIM { param($Tenant, $Settings) #$Rerun -Type Standard -Tenant $Tenant -API 'AddDKIM' -Settings $Settings - $TestResult = Test-CIPPStandardLicense -StandardName 'AddDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AddDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index 5952765a17b5..0ea8bfd4f03a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -50,7 +50,7 @@ function Invoke-CIPPStandardAntiPhishPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AntiPhishPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AntiPhishPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 index 4b2750be7fa0..855c2685624b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardAntiSpamSafeList { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AntiSpamSafeList' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AntiSpamSafeList' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -41,7 +41,7 @@ function Invoke-CIPPStandardAntiSpamSafeList { $State = [System.Convert]::ToBoolean($Settings.EnableSafeList) } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'AntiSpamSafeList: Failed to convert the EnableSafeList parameter to a boolean' -sev Error - Return + return } try { @@ -49,7 +49,7 @@ function Invoke-CIPPStandardAntiSpamSafeList { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to get the Anti-Spam Connection Filter Safe List. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Return + return } $WantedState = $State -eq $true ? $true : $false $StateIsCorrect = if ($CurrentState -eq $WantedState) { $true } else { $false } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 index 81ae65729879..79e6269c7d97 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardAuditLog { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AuditLog' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AuditLog' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -43,7 +43,7 @@ function Invoke-CIPPStandardAuditLog { Write-Host ($Settings | ConvertTo-Json) $AuditLogEnabled = [bool](New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AdminAuditLogConfig' -Select UnifiedAuditLogIngestionEnabled).UnifiedAuditLogIngestionEnabled - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' $DehydratedTenant = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' -Select IsDehydrated).IsDehydrated diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 index c0ee4bc5bdf3..049f9a8a1dc8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 @@ -28,7 +28,7 @@ function Invoke-CIPPStandardAutoExpandArchive { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'AutoExpandArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'AutoExpandArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 index 6c8f529b2de0..f1a4b6727ccf 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardBookings { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'Bookings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'Bookings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 index c7ff49f9cc94..97a8bd07448a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardCloudMessageRecall { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'CloudMessageRecall' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'CloudMessageRecall' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index e3686fec0076..561f2919db2e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardDelegateSentItems { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DelegateSentItems' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DelegateSentItems' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index 63f77574f974..a37a37b6190c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardDeployContactTemplates { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DeployContactTemplates' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DeployContactTemplates' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index aa419c59f095..6f7798fd86b6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardDeployMailContact { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DeployMailContact' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DeployMailContact' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 index f0275a9eb92c..1ebc05b7c365 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAdditionalStorageProviders' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAdditionalStorageProviders' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index 37a64f99a319..72585d50ec4d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableBasicAuthSMTP' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableBasicAuthSMTP' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 index 82d25184ccaf..a83b5033a821 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExchangeOnlinePowerShell' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExchangeOnlinePowerShell' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 index 6b4c29c2be3b..6e8cb00c8a49 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExternalCalendarSharing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExternalCalendarSharing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 index 11e3db4739cf..047374531541 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableOutlookAddins { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableOutlookAddins' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableOutlookAddins' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index 4f284d19857f..fcfaa7653f87 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableResourceMailbox { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableResourceMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableResourceMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 index e6bd16f501ba..ce70d89078e1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardDisableTNEF { param ($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableTNEF' - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableTNEF' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableTNEF' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 index b98f28fe2fd3..8d4011a60bd8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardEXODisableAutoForwarding { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EXODisableAutoForwarding' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EXODisableAutoForwarding' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 index d29e2af9634f..67dab06c3bdd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 @@ -7,7 +7,7 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { .SYNOPSIS (Label) Set Exchange Outbound Spam Limits .DESCRIPTION - (Helptext) Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. + (Helptext) Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. (DocsDescription) Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. .NOTES CAT @@ -35,7 +35,7 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EXOOutboundSpamLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EXOOutboundSpamLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 1e1df95f041d..f959049e9ffa 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardEnableLitigationHold { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableLitigationHold' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableLitigationHold' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 index 4e251944fc17..e1a58fd11da9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardEnableMailTips { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailTips' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailTips' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 index 2676d7577a04..cf538a561585 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardEnableMailboxAuditing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailboxAuditing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailboxAuditing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 index 6710fce40846..b98ea70abcc5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 @@ -28,7 +28,7 @@ function Invoke-CIPPStandardEnableOnlineArchiving { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'EnableOnlineArchiving' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'EnableOnlineArchiving' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 index b57860ecf55b..ca28aaed9122 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 @@ -4,7 +4,7 @@ function Invoke-CIPPStandardExchangeConnectorTemplate { Internal #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ExConnector' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ExConnector' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 index 268a521c020c..93a2d324ddd7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardFocusedInbox { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'FocusedInbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'FocusedInbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 index ea0284c0e2ba..1eff1e59ac2f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { param ($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GlobalQuarantineNotifications' - $TestResult = Test-CIPPStandardLicense -StandardName 'GlobalQuarantineNotifications' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'GlobalQuarantineNotifications' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 29c84354828e..fdf82451dce2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -28,7 +28,7 @@ function Invoke-CIPPStandardGroupTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index cf2459c7071c..95b345a2a534 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardMailboxRecipientLimits { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MailboxRecipientLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MailboxRecipientLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index dfc4135d4fa8..5825478c3f96 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MalwareFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MalwareFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 index af905f43d0a3..b3d2e22adb6d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 @@ -28,7 +28,7 @@ function Invoke-CIPPStandardMessageExpiration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'MessageExpiration' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'MessageExpiration' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 index c498716a5a04..1e2c2a925123 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardOWAAttachmentRestrictions { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'OWAAttachmentRestrictions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'OWAAttachmentRestrictions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 index 51049bddc07d..789c300c6233 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardOutBoundSpamAlert { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'OutBoundSpamAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'OutBoundSpamAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 index 1cf8c915b826..23691f879c08 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'PhishSimSpoofIntelligence' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishSimSpoofIntelligence' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 index 0fa3af87bf61..6cc3a314ea55 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardPhishingSimulations { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'PhishingSimulations' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishingSimulations' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 index 979f690a1222..c484f2006a17 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardProfilePhotos { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ProfilePhotos' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ProfilePhotos' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 index 9a61e0478eb2..17778b5bf697 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardQuarantineRequestAlert { #> param ($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineRequestAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineRequestAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 index 02bf724072d4..1b821dbf7bd1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardQuarantineTemplate { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 index a1135e0dc558..8cbdd779e730 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardRetentionPolicyTag { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'RetentionPolicyTag' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'RetentionPolicyTag' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index 4756d667c395..f3760a9bfdfa 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardRotateDKIM { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'RotateDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'RotateDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index f3f0dc8c5ce5..459e13013550 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index 6ae32ddd6a39..8abf1df83ace 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 index db323964664e..7de23f78f2a4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 index d246ed95746d..182449f8ab18 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardSafeSendersDisable { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SafeSendersDisable' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SafeSendersDisable' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 index 28692ae363c5..536f0ce8afd6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardSendFromAlias { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SendFromAlias' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SendFromAlias' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 index 958e822a9c9d..82d75d8bafff 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SendReceiveLimitTenant' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SendReceiveLimitTenant' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 index 2d567060ab95..2b74ec1f1ad5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardShortenMeetings { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ShortenMeetings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'ShortenMeetings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index 3b811a9a712e..3604327ff5bc 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -51,7 +51,7 @@ function Invoke-CIPPStandardSpamFilterPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SpamFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SpamFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index 15a49616c884..763bd36054d3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardSpoofWarn { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SpoofWarn' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'SpoofWarn' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 index bcb2881ab356..569d97f465cf 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingsByDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingsByDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index 3f85ccbc01d5..ed82eeba51f9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -26,7 +26,7 @@ function Invoke-CIPPStandardTransportRuleTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TransportRuleTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TransportRuleTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 index b43fa7fdb673..b1bb5ff5c84c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardTwoClickEmailProtection { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TwoClickEmailProtection' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'TwoClickEmailProtection' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 index 66833d43dd9c..b5f9dc674954 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 @@ -30,7 +30,7 @@ function Invoke-CIPPStandardUserSubmissions { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'UserSubmissions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'UserSubmissions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index 2c490636c5d6..88ee44f103d7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardcalDefault { param($Tenant, $Settings, $QueueItem) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'calDefault' - $TestResult = Test-CIPPStandardLicense -StandardName 'calDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'calDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." From 6e39967d4a4e7fb3e79306fecdf9dc7b6d098420 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 1 Sep 2025 21:54:18 -0400 Subject: [PATCH 33/68] batch $expand support limited use functionality (e.g. scheduler) due to the length of time this may take to complete --- .../GraphRequests/Get-GraphRequestList.ps1 | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index 62a90e29616e..d7f1e082d794 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -54,6 +54,9 @@ function Get-GraphRequestList { .PARAMETER Caller Name of the calling function + .PARAMETER UseBatchExpand + Perform a batch lookup using the $expand query parameter to avoid 20 item max + #> [CmdletBinding()] param( @@ -76,7 +79,8 @@ function Get-GraphRequestList { [switch]$ReverseTenantLookup, [string]$ReverseTenantLookupProperty = 'tenantId', [boolean]$AsApp = $false, - [string]$Caller = 'Get-GraphRequestList' + [string]$Caller = 'Get-GraphRequestList', + [switch]$UseBatchExpand ) $SingleTenantThreshold = 8000 @@ -109,7 +113,12 @@ function Get-GraphRequestList { } else { $Value = $Item.Value } - $ParamCollection.Add($Item.Key, $Value) + + if ($UseBatchExpand.IsPresent -and ($Item.Key -eq '$expand' -or $Item.Key -eq 'expand')) { + $BatchExpandQuery = $Item.Value + } else { + $ParamCollection.Add($Item.Key, $Value) + } } } $GraphQuery.Query = $ParamCollection.ToString() @@ -331,6 +340,34 @@ function Get-GraphRequestList { $GraphRequestResults = New-GraphGetRequest @GraphRequest -Caller $Caller -ErrorAction Stop $GraphRequestResults = $GraphRequestResults | Select-Object *, @{n = 'Tenant'; e = { $TenantFilter } }, @{n = 'CippStatus'; e = { 'Good' } } + if ($UseBatchExpand.IsPresent -and ![string]::IsNullOrEmpty($BatchExpandQuery)) { + if ($BatchExpandQuery -match '' -and ![string]::IsNullOrEmpty($GraphRequestResults.id)) { + # Convert $expand format to actual batch query e.g. members($select=id,displayName) to members?$select=id,displayName + $BatchExpandQuery = $BatchExpandQuery -replace '\(\$?([^=]+)=([^)]+)\)', '?$$$1=$2' -replace ';', '&' + + # Extract property name from expand + $Property = $BatchExpandQuery -replace '\?.*$', '' -replace '^.*\/', '' + Write-Information "Performing batch expansion for property '$Property'..." + + $Uri = "$Endpoint/{0}/$BatchExpandQuery" + + $Requests = foreach ($Result in $GraphRequestResults) { + @{ + id = $Result.id + url = $Uri -f $Result.id + method = 'GET' + } + } + $BatchResults = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter -NoAuthCheck $NoAuthCheck.IsPresent -asapp $AsApp + + $GraphRequestResults = foreach ($Result in $GraphRequestResults) { + $PropValue = $BatchResults | Where-Object { $_.id -eq $Result.id } | Select-Object -ExpandProperty body + $Result | Add-Member -MemberType NoteProperty -Name $Property -Value ($PropValue.value ?? $PropValue) + $Result + } + } + } + if ($ReverseTenantLookup -and $GraphRequestResults) { $ReverseLookupRequests = $GraphRequestResults.$ReverseTenantLookupProperty | Sort-Object -Unique | ForEach-Object { @{ From 123f280440e38801de6b71fefd08e244b5fce60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 2 Sep 2025 19:03:29 +0200 Subject: [PATCH 34/68] Fix: Fix AP profile assignment bug from bad comparison Casing and other fun little fixes WORD --- .../Autopilot/Invoke-AddAutopilotConfig.ps1 | 44 +++++------- .../Set-CIPPDefaultAPDeploymentProfile.ps1 | 67 ++++++++++--------- cspell.json | 1 + 3 files changed, 53 insertions(+), 59 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAutopilotConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAutopilotConfig.ps1 index 5112dc52d091..a95125b9eeaa 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAutopilotConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddAutopilotConfig.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-AddAutopilotConfig { +function Invoke-AddAutopilotConfig { <# .FUNCTIONALITY Entrypoint @@ -14,42 +14,34 @@ Function Invoke-AddAutopilotConfig { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - - # Input bindings are passed in via param block. - $Tenants = $Request.body.selectedTenants.value - $AssignTo = if ($request.body.Assignto -ne 'on') { $request.body.Assignto } - $Profbod = [pscustomobject]$Request.body - $usertype = if ($Profbod.NotLocalAdmin -eq 'true') { 'standard' } else { 'administrator' } - $DeploymentMode = if ($profbod.DeploymentMode -eq 'true') { 'shared' } else { 'singleUser' } + $Tenants = $Request.Body.selectedTenants.value + $Profbod = [pscustomobject]$Request.Body + $UserType = if ($Profbod.NotLocalAdmin -eq 'true') { 'standard' } else { 'administrator' } + $DeploymentMode = if ($Profbod.DeploymentMode -eq 'true') { 'shared' } else { 'singleUser' } $profileParams = @{ - displayname = $request.body.Displayname - description = $request.body.Description - usertype = $usertype + DisplayName = $Request.Body.DisplayName + Description = $Request.Body.Description + UserType = $UserType DeploymentMode = $DeploymentMode - assignto = $AssignTo - devicenameTemplate = $Profbod.deviceNameTemplate - allowWhiteGlove = $Profbod.allowWhiteGlove - CollectHash = $Profbod.collectHash - hideChangeAccount = $Profbod.hideChangeAccount - hidePrivacy = $Profbod.hidePrivacy - hideTerms = $Profbod.hideTerms + AssignTo = $Request.Body.Assignto + DeviceNameTemplate = $Profbod.DeviceNameTemplate + AllowWhiteGlove = $Profbod.allowWhiteGlove + CollectHash = $Profbod.CollectHash + HideChangeAccount = $Profbod.HideChangeAccount + HidePrivacy = $Profbod.HidePrivacy + HideTerms = $Profbod.HideTerms Autokeyboard = $Profbod.Autokeyboard Language = $ProfBod.languages.value } - $results = foreach ($Tenant in $tenants) { - $profileParams['tenantFilter'] = $Tenant + $Results = foreach ($tenant in $Tenants) { + $profileParams['tenantFilter'] = $tenant Set-CIPPDefaultAPDeploymentProfile @profileParams } - $body = [pscustomobject]@{'Results' = $results } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = $body + Body = @{'Results' = $Results } }) - - - } diff --git a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 index 502b663199ff..05f1aefffabd 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 @@ -1,18 +1,18 @@ function Set-CIPPDefaultAPDeploymentProfile { [CmdletBinding(SupportsShouldProcess = $true)] param( - $tenantFilter, - $displayName, - $description, - $devicenameTemplate, - $allowWhiteGlove, + $TenantFilter, + $DisplayName, + $Description, + $DeviceNameTemplate, + $AllowWhiteGlove, $CollectHash, - $userType, + $UserType, $DeploymentMode, - $hideChangeAccount, + $HideChangeAccount, $AssignTo, - $hidePrivacy, - $hideTerms, + $HidePrivacy, + $HideTerms, $AutoKeyboard, $Headers, $Language = 'os-default', @@ -24,65 +24,66 @@ function Set-CIPPDefaultAPDeploymentProfile { try { $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.azureADWindowsAutopilotDeploymentProfile' - 'displayName' = "$($displayName)" - 'description' = "$($description)" - 'deviceNameTemplate' = "$($devicenameTemplate)" + 'displayName' = "$($DisplayName)" + 'description' = "$($Description)" + 'deviceNameTemplate' = "$($DeviceNameTemplate)" 'language' = "$($Language)" - 'enableWhiteGlove' = $([bool]($allowWhiteGlove)) + 'enableWhiteGlove' = $([bool]($AllowWhiteGlove)) 'deviceType' = 'windowsPc' 'extractHardwareHash' = $([bool]($CollectHash)) 'roleScopeTagIds' = @() 'hybridAzureADJoinSkipConnectivityCheck' = $false - 'outOfBoxExperienceSetting' = @{ - 'deviceUsageType' = "$DeploymentMode" - 'escapeLinkHidden' = $([bool]($hideChangeAccount)) - 'privacySettingsHidden' = $([bool]($hidePrivacy)) - 'eulaHidden' = $([bool]($hideTerms)) - 'userType' = "$userType" + 'outOfBoxExperienceSetting' = @{ + 'deviceUsageType' = "$DeploymentMode" + 'escapeLinkHidden' = $([bool]($HideChangeAccount)) + 'privacySettingsHidden' = $([bool]($HidePrivacy)) + 'eulaHidden' = $([bool]($HideTerms)) + 'userType' = "$UserType" 'keyboardSelectionPageSkipped' = $([bool]($AutoKeyboard)) } } $Body = ConvertTo-Json -InputObject $ObjBody - $Profiles = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -tenantid $tenantFilter | Where-Object -Property displayName -EQ $displayName + $Profiles = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -tenantid $TenantFilter | Where-Object -Property displayName -EQ $DisplayName if ($Profiles.count -gt 1) { $Profiles | ForEach-Object { if ($_.id -ne $Profiles[0].id) { if ($PSCmdlet.ShouldProcess($_.displayName, 'Delete duplicate Autopilot profile')) { - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($_.id)" -tenantid $tenantFilter -type DELETE - Write-LogMessage -Headers $User -API $APIName -tenant $($tenantFilter) -message "Deleted duplicate Autopilot profile $($displayName)" -Sev 'Info' + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($_.id)" -tenantid $TenantFilter -type DELETE + Write-LogMessage -Headers $User -API $APIName -tenant $($TenantFilter) -message "Deleted duplicate Autopilot profile $($DisplayName)" -Sev 'Info' } } } $Profiles = $Profiles[0] } if (!$Profiles) { - if ($PSCmdlet.ShouldProcess($displayName, 'Add Autopilot profile')) { + if ($PSCmdlet.ShouldProcess($DisplayName, 'Add Autopilot profile')) { $Type = 'Add' - $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -body $body -tenantid $tenantFilter - Write-LogMessage -Headers $User -API $APIName -tenant $($tenantFilter) -message "Added Autopilot profile $($displayName)" -Sev 'Info' + $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -body $Body -tenantid $TenantFilter + Write-LogMessage -Headers $User -API $APIName -tenant $($TenantFilter) -message "Added Autopilot profile $($DisplayName)" -Sev 'Info' } } else { $Type = 'Edit' - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($Profiles.id)" -tenantid $tenantFilter -body $body -type PATCH + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($Profiles.id)" -tenantid $TenantFilter -body $Body -type PATCH $GraphRequest = $Profiles | Select-Object -Last 1 } if ($AssignTo -eq $true) { $AssignBody = '{"target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}}' - if ($PSCmdlet.ShouldProcess($AssignTo, "Assign Autopilot profile $displayName")) { + if ($PSCmdlet.ShouldProcess($AssignTo, "Assign Autopilot profile $DisplayName")) { #Get assignments - $Assignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $tenantFilter + $Assignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $TenantFilter if (!$Assignments) { - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $tenantFilter -type POST -body $AssignBody + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $TenantFilter -type POST -body $AssignBody } - Write-LogMessage -Headers $User -API $APIName -tenant $tenantFilter -message "Assigned autopilot profile $($displayName) to $AssignTo" -Sev 'Info' + Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Assigned autopilot profile $($DisplayName) to $AssignTo" -Sev 'Info' } } - "Successfully $($Type)ed profile for $tenantFilter" + "Successfully $($Type)ed profile for $TenantFilter" } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -Headers $User -API $APIName -tenant $tenantFilter -message "Failed $($Type)ing Autopilot Profile $($displayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage - throw "Failed to add profile for $($tenantFilter): $($ErrorMessage.NormalizedError)" + $Result = "Failed $($Type)ing Autopilot Profile $($DisplayName). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' -LogData $ErrorMessage + throw $Result } } diff --git a/cspell.json b/cspell.json index 56a59eca16f4..5e6ccfb08f36 100644 --- a/cspell.json +++ b/cspell.json @@ -20,6 +20,7 @@ "endswith", "entra", "Entra", + "eula", "exploitability", "gdap", "GDAP", From 93efece187a84728f7adbd0b316b08707cedfced Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 2 Sep 2025 17:08:42 -0400 Subject: [PATCH 35/68] prevent null from being returned --- .../Activity Triggers/BEC/Push-BECRun.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 index b23079f413bb..9c1ffb9b5ab8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 @@ -50,7 +50,7 @@ function Push-BECRun { $ExtractResult = 'Successfully extracted logs from auditlog' } Write-Information 'Getting last sign-in' - Try { + try { $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$SuspectUser')&`$top=1&`$orderby=createdDateTime desc" $LastSignIn = New-GraphGetRequest -uri $URI -tenantid $TenantFilter -noPagination $true -verbose | Select-Object @{ Name = 'CreatedDateTime'; Expression = { $(($_.createdDateTime | Out-String) -replace '\r\n') } }, id, @@ -69,7 +69,7 @@ function Push-BECRun { #List all users devices $Bytes = [System.Text.Encoding]::UTF8.GetBytes($SuspectUser) $base64IdentityParam = [Convert]::ToBase64String($Bytes) - Try { + try { $Devices = New-GraphGetRequest -uri "https://outlook.office365.com:443/adminapi/beta/$($TenantFilter)/mailbox('$($base64IdentityParam)')/MobileDevice/Exchange.GetMobileDeviceStatistics()/?IsEncoded=True" -Tenantid $TenantFilter -scope ExchangeOnline } catch { $Devices = $null @@ -143,10 +143,10 @@ function Push-BECRun { Write-Information 'Getting bulk requests' $GraphResults = New-GraphBulkRequest -Requests $Requests -tenantid $TenantFilter -asapp $true - $PasswordChanges = ($GraphResults | Where-Object { $_.id -eq 'Users' }).body.value | Where-Object { $_.lastPasswordChangeDateTime -ge $startDate } - $NewUsers = ($GraphResults | Where-Object { $_.id -eq 'Users' }).body.value | Where-Object { $_.createdDateTime -ge $startDate } - $MFADevices = ($GraphResults | Where-Object { $_.id -eq 'MFADevices' }).body.value - $NewSPs = ($GraphResults | Where-Object { $_.id -eq 'NewSPs' }).body.value + $PasswordChanges = ($GraphResults | Where-Object { $_.id -eq 'Users' }).body.value | Where-Object { $_.lastPasswordChangeDateTime -ge $startDate } ?? @() + $NewUsers = ($GraphResults | Where-Object { $_.id -eq 'Users' }).body.value | Where-Object { $_.createdDateTime -ge $startDate } ?? @() + $MFADevices = ($GraphResults | Where-Object { $_.id -eq 'MFADevices' }).body.value ?? @() + $NewSPs = ($GraphResults | Where-Object { $_.id -eq 'NewSPs' }).body.value ?? @() $Results = [PSCustomObject]@{ From 7b469c18c3132790f44d88345f225922cb157e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 2 Sep 2025 23:25:38 +0200 Subject: [PATCH 36/68] Feat: Add Invoke-RemoveAutopilotConfig and Remove-CIPPAutopilotProfile functions for deleting Autopilot profiles --- .../Invoke-RemoveAutopilotConfig.ps1 | 56 ++++++++++++++++ .../Public/Remove-CIPPAutopilotProfile.ps1 | 67 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-RemoveAutopilotConfig.ps1 create mode 100644 Modules/CIPPCore/Public/Remove-CIPPAutopilotProfile.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-RemoveAutopilotConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-RemoveAutopilotConfig.ps1 new file mode 100644 index 000000000000..ba09de16a199 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-RemoveAutopilotConfig.ps1 @@ -0,0 +1,56 @@ +using namespace System.Net + +function Invoke-RemoveAutopilotConfig { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.Autopilot.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Body.tenantFilter + $ProfileId = $Request.Body.ID + $DisplayName = $Request.Body.displayName + $Assignments = $Request.Body.assignments + + try { + # Validate required parameters + if ([string]::IsNullOrEmpty($ProfileId)) { + throw 'Profile ID is required' + } + + if ([string]::IsNullOrEmpty($TenantFilter)) { + throw 'Tenant filter is required' + } + + # Call the helper function to delete the autopilot profile + $params = @{ + ProfileId = $ProfileId + DisplayName = $DisplayName + TenantFilter = $TenantFilter + Assignments = $Assignments + Headers = $Headers + APIName = $APIName + } + $Result = Remove-CIPPAutopilotProfile @params + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = $_.Exception.Message + $Result = $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = "$Result" } + }) +} diff --git a/Modules/CIPPCore/Public/Remove-CIPPAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Remove-CIPPAutopilotProfile.ps1 new file mode 100644 index 000000000000..f1a06bbf1e46 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPAutopilotProfile.ps1 @@ -0,0 +1,67 @@ +function Remove-CIPPAutopilotProfile { + param( + $ProfileId, + $DisplayName, + $TenantFilter, + $Assignments, + $Headers, + $APIName = 'Remove Autopilot Profile' + ) + + + try { + + try { + $DisplayName = $null -eq $DisplayName ? $ProfileId : $DisplayName + if ($Assignments.Count -gt 0) { + Write-Host "Profile $ProfileId has $($Assignments.Count) assignments, removing them first" + throw + } + + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId" -tenantid $TenantFilter -type DELETE + $Result = "Successfully deleted Autopilot profile '$($DisplayName)'" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' + return $Result + } catch { + + # Profile could not be deleted, there is probably an assignment still referencing it. The error is bloody useless here, and we just need to try some stuff + if ($null -eq $Assignments) { + $Assignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId/assignments" -tenantid $TenantFilter + } + + # Remove all assignments + if ($Assignments -and $Assignments.Count -gt 0) { + foreach ($Assignment in $Assignments) { + try { + # Use the assignment ID directly as provided by the API + $AssignmentId = $Assignment.id + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId/assignments/$AssignmentId" -tenantid $TenantFilter -type DELETE + + } catch { + # Handle the case where the assignment might reference a deleted group + try { + if ($Assignment.target -and $Assignment.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget') { + $GroupId = $Assignment.target.groupId + $AlternativeAssignmentId = "${ProfileId}_${GroupId}" + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId/assignments/$AlternativeAssignmentId" -tenantid $TenantFilter -type DELETE + } + } catch { + throw "Could not remove assignment $AssignmentId" + } + } + } + } + # Retry deleting the profile after removing assignments + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$ProfileId" -tenantid $TenantFilter -type DELETE + $Result = "Successfully deleted Autopilot profile '$($DisplayName)' " + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' + return $Result + } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $ErrorText = "Failed to delete Autopilot profile $ProfileId. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorText -Sev 'Error' -LogData $ErrorMessage + throw $ErrorText + } +} From 1e0a3d6ce6463f50663f255e181507288bd9a7e1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 2 Sep 2025 18:42:25 -0400 Subject: [PATCH 37/68] prevent malformed audit log rule from breaking processing --- .../Webhooks/Test-CIPPAuditLogRules.ps1 | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index 1e13a1d5f25c..04fcdfb76c9b 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -372,22 +372,26 @@ function Test-CIPPAuditLogRules { $MatchedRules = [System.Collections.Generic.List[string]]::new() $DataToProcess = foreach ($clause in $Where) { - $ClauseStartTime = Get-Date - Write-Warning "Webhook: Processing clause: $($clause.clause)" - $ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause } - if ($ReturnedData) { - Write-Warning "Webhook: There is matching data: $(($ReturnedData.operation | Select-Object -Unique) -join ', ')" - $ReturnedData = foreach ($item in $ReturnedData) { - $item.CIPPAction = $clause.expectedAction - $item.CIPPClause = $clause.CIPPClause -join ' and ' - $MatchedRules.Add($clause.CIPPClause -join ' and ') - $item + try { + $ClauseStartTime = Get-Date + Write-Warning "Webhook: Processing clause: $($clause.clause)" + $ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause } + if ($ReturnedData) { + Write-Warning "Webhook: There is matching data: $(($ReturnedData.operation | Select-Object -Unique) -join ', ')" + $ReturnedData = foreach ($item in $ReturnedData) { + $item.CIPPAction = $clause.expectedAction + $item.CIPPClause = $clause.CIPPClause -join ' and ' + $MatchedRules.Add($clause.CIPPClause -join ' and ') + $item + } } + $ClauseEndTime = Get-Date + $ClauseSeconds = ($ClauseEndTime - $ClauseStartTime).TotalSeconds + Write-Warning "Task took $ClauseSeconds seconds for clause: $($clause.clause)" + $ReturnedData + } catch { + Write-Warning "Error processing clause: $($clause.clause): $($_.Exception.Message)" } - $ClauseEndTime = Get-Date - $ClauseSeconds = ($ClauseEndTime - $ClauseStartTime).TotalSeconds - Write-Warning "Task took $ClauseSeconds seconds for clause: $($clause.clause)" - $ReturnedData } $Results.MatchedRules = @($MatchedRules | Select-Object -Unique) $Results.MatchedLogs = ($DataToProcess | Measure-Object).Count From 2aa5f781bb060752abd3c664b99a4726dfcbbfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 3 Sep 2025 21:07:14 +0200 Subject: [PATCH 38/68] Fix: Add logic handling setting MFA for guests --- .../Administration/Users/Invoke-ExecPerUserMFA.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 index c22b0945e193..46f0e717da95 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 @@ -6,17 +6,21 @@ function Invoke-ExecPerUserMFA { .ROLE Identity.User.ReadWrite #> - Param($Request, $TriggerMetadata) + param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + # Guest user handling + $UserId = $Request.Body.userPrincipalName -match '#EXT#' ? $Request.Body.userId : $Request.Body.userPrincipalName + $TenantFilter = $Request.Body.tenantFilter + $State = $Request.Body.State.value ? $Request.Body.State.value : $Request.Body.State $Request = @{ - userId = $Request.Body.userId - TenantFilter = $Request.Body.tenantFilter - State = $Request.Body.State.value ? $Request.Body.State.value : $Request.Body.State + userId = $UserId + TenantFilter = $TenantFilter + State = $State Headers = $Headers APIName = $APIName } From 90940faee5ef37cabe0d3d4d4835ec1d13ca32fe Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Sep 2025 11:19:13 -0400 Subject: [PATCH 39/68] batch expand for assignedLicenses --- .../GraphRequests/Get-GraphRequestList.ps1 | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index d7f1e082d794..0f31743a02bf 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -349,21 +349,37 @@ function Get-GraphRequestList { $Property = $BatchExpandQuery -replace '\?.*$', '' -replace '^.*\/', '' Write-Information "Performing batch expansion for property '$Property'..." - $Uri = "$Endpoint/{0}/$BatchExpandQuery" + if ($Property -eq 'assignedLicenses') { + $LicenseDetails = Get-CIPPLicenseOverview -TenantFilter $TenantFilter + $GraphRequestResults = foreach ($GraphRequestResult in $GraphRequestResults) { + $NewLicenses = [system.collections.generic.list[string]]::new() + foreach ($License in $GraphRequestResult.assignedLicenses) { + $LicenseInfo = $LicenseDetails | Where-Object { $_.skuId -eq $License.skuId } | Select-Object -First 1 + if ($LicenseInfo) { + $NewLicenses.Add($LicenseInfo.License) + } + } + $GraphRequestResult | Add-Member -MemberType NoteProperty -Name $Property -Value @($NewLicenses) -Force + $GraphRequestResult + } + } else { + + $Uri = "$Endpoint/{0}/$BatchExpandQuery" - $Requests = foreach ($Result in $GraphRequestResults) { - @{ - id = $Result.id - url = $Uri -f $Result.id - method = 'GET' + $Requests = foreach ($Result in $GraphRequestResults) { + @{ + id = $Result.id + url = $Uri -f $Result.id + method = 'GET' + } } - } - $BatchResults = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter -NoAuthCheck $NoAuthCheck.IsPresent -asapp $AsApp + $BatchResults = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter -NoAuthCheck $NoAuthCheck.IsPresent -asapp $AsApp - $GraphRequestResults = foreach ($Result in $GraphRequestResults) { - $PropValue = $BatchResults | Where-Object { $_.id -eq $Result.id } | Select-Object -ExpandProperty body - $Result | Add-Member -MemberType NoteProperty -Name $Property -Value ($PropValue.value ?? $PropValue) - $Result + $GraphRequestResults = foreach ($Result in $GraphRequestResults) { + $PropValue = $BatchResults | Where-Object { $_.id -eq $Result.id } | Select-Object -ExpandProperty body + $Result | Add-Member -MemberType NoteProperty -Name $Property -Value ($PropValue.value ?? $PropValue) + $Result + } } } } From fbe5f33d2373d0fa77cf5bbaa1ef5279469cefab Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Sep 2025 12:06:34 -0400 Subject: [PATCH 40/68] only skip exchange groups if not licensed Fixes https://github.com/KelvinTegelaar/CIPP/issues/4569 Does not block group creation when security or other non-exchange types --- .../Invoke-CIPPStandardGroupTemplate.ps1 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index fdf82451dce2..aa71fb08be65 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -28,12 +28,8 @@ function Invoke-CIPPStandardGroupTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access + $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') -SkipLog - if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." - return $true - } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GroupTemplate' $existingGroups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $tenant if ($Settings.remediate -eq $true) { @@ -67,6 +63,11 @@ function Invoke-CIPPStandardGroupTemplate { if ($groupobj.groupType -in 'Generic', 'azurerole', 'dynamic', 'Security') { $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenant -type POST -body (ConvertTo-Json -InputObject $BodyToship -Depth 10) -verbose } else { + if (!$TestResult) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot create group $($groupobj.displayname) as the tenant is not licensed for Exchange." -Sev 'Error' + continue + } + if ($groupobj.groupType -eq 'dynamicdistribution') { $Params = @{ Name = $groupobj.Displayname @@ -92,6 +93,10 @@ function Invoke-CIPPStandardGroupTemplate { if ($groupobj.groupType -in 'Generic', 'azurerole', 'dynamic') { $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($CheckExististing.id)" -tenantid $tenant -type PATCH -body (ConvertTo-Json -InputObject $BodyToship -Depth 10) -verbose } else { + if (!$TestResult) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot update group $($groupobj.displayname) as the tenant is not licensed for Exchange." -Sev 'Error' + continue + } if ($groupobj.groupType -eq 'dynamicdistribution') { $Params = @{ Name = $groupobj.Displayname From 5bc11f1c1a135d2cbce2713366e9de538a04cd7a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Sep 2025 19:12:07 -0400 Subject: [PATCH 41/68] force orchestrator id to be string --- Modules/CippEntrypoints/CippEntrypoints.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 5c17e4cb8a90..da0fd7d643c6 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -291,7 +291,7 @@ function Receive-CIPPTimerTrigger { $Results = Invoke-Command -ScriptBlock { & $Function.Command @Parameters } if ($Results -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { - $FunctionStatus.OrchestratorId = $Results + $FunctionStatus.OrchestratorId = $Results -join ',' $Status = 'Started' } else { $Status = 'Completed' From d7e8d8c5493b994ae30b1f4da4b6adfe3a996aaf Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Sep 2025 22:06:49 -0400 Subject: [PATCH 42/68] fix member query --- .../Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 index 5da36dcb859d..ca258aed65ea 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPAccessAssignments.ps1 @@ -32,7 +32,7 @@ function Invoke-ListGDAPAccessAssignments { 'method' = 'GET' } } - $Members = New-GraphBulkRequest -Requests $ContainerMembers -tenantid $TenantFilter -asApp $true -NoAuthCheck $true + $Members = New-GraphBulkRequest -Requests @($ContainerMembers) -tenantid $TenantFilter -asApp $true -NoAuthCheck $true $Results = foreach ($AccessAssignment in $AccessAssignments) { [PSCustomObject]@{ From 898ab21b3de58fa56f6f354a65c2432c0a890683 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:12:12 +0800 Subject: [PATCH 43/68] Add null checks to mailbox quota alert --- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 index 1e61ffaa01b6..d4e8c651e490 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1 @@ -17,7 +17,7 @@ function Get-CIPPAlertQuotaUsed { return } $OverQuota = $AlertData | ForEach-Object { - if ($_.StorageUsedInBytes -eq 0 -or $_.prohibitSendReceiveQuotaInBytes -eq 0) { return } + if ([string]::IsNullOrEmpty($_.StorageUsedInBytes) -or [string]::IsNullOrEmpty($_.prohibitSendReceiveQuotaInBytes) -or $_.StorageUsedInBytes -eq 0 -or $_.prohibitSendReceiveQuotaInBytes -eq 0) { return } try { $PercentLeft = [math]::round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100) } catch { $PercentLeft = 100 } From d4bf4a69c61c530ada75b52f50b13762649e0d28 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Sep 2025 15:04:30 -0400 Subject: [PATCH 44/68] add follow up command to update dynamic group for sender auth --- .../Identity/Administration/Groups/Invoke-AddGroup.ps1 | 9 +++++++++ .../Standards/Invoke-CIPPStandardGroupTemplate.ps1 | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 index 8a14f5d0928a..8a8a9e3906cf 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 @@ -62,6 +62,15 @@ function Invoke-AddGroup { PrimarySmtpAddress = $Email } $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DynamicDistributionGroup' -cmdParams $ExoParams + + if (!$GroupObject.allowExternal) { + $SetParams = @{ + RequireSenderAuthenticationEnabled = [bool]!$GroupObject.allowExternal + Name = $GroupObject.displayName + PrimarySmtpAddress = $Email + } + $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $SetParams + } } else { $ExoParams = @{ Name = $GroupObject.displayName diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index aa71fb08be65..0b5decdbbb8e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -75,6 +75,15 @@ function Invoke-CIPPStandardGroupTemplate { PrimarySmtpAddress = $email } $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DynamicDistributionGroup' -cmdParams $params + + if (!$groupobj.AllowExternal) { + $SetParams = @{ + RequireSenderAuthenticationEnabled = [bool]!$groupobj.AllowExternal + Name = $groupobj.Displayname + PrimarySmtpAddress = $email + } + $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $SetParams + } } else { $Params = @{ Name = $groupobj.Displayname From 4d1e3386c45cab481b6d0aaa35569d3c8cb48faa Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 6 Sep 2025 14:58:55 -0400 Subject: [PATCH 45/68] add initialDomainName to tenantfilter --- Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 index c231eb916100..bb98a0b77825 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 @@ -42,8 +42,8 @@ function Get-Tenants { $IncludedTenantFilter = [scriptblock]::Create("`$_.customerId -eq '$TenantFilter'") $RelationshipFilter = " and customer/tenantId eq '$TenantFilter'" } else { - $Filter = "{0} and defaultDomainName eq '{1}'" -f $Filter, $TenantFilter - $IncludedTenantFilter = [scriptblock]::Create("`$_.defaultDomainName -eq '$TenantFilter'") + $Filter = "{0} and defaultDomainName eq '{1}' -or initialDomainName eq '{1}'" -f $Filter, $TenantFilter + $IncludedTenantFilter = [scriptblock]::Create("`$_.defaultDomainName -eq '$TenantFilter' -or `$_.initialDomainName -eq '$TenantFilter'") $RelationshipFilter = '' } } else { From daa1ca9ca4fe22a88d0df533523ebae59eda80ce Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 6 Sep 2025 15:06:26 -0400 Subject: [PATCH 46/68] handle new alert format for phishing check --- .../Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 | 4 ++++ Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 index 58976b122f30..1c8d4c61afda 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-PublicPhishingCheck.ps1 @@ -16,6 +16,10 @@ function Invoke-PublicPhishingCheck { if ($Request.body.Cloned -and $Tenant.customerId -eq $Request.body.TenantId) { Write-AlertMessage -message $Request.body.AlertMessage -sev 'Alert' -tenant $Request.body.TenantId + } elseif ($Request.Body.source -and $Tenant) { + $Message = "Alert received from $($Request.Body.source) for $($Request.body.TenantId)" + Write-Information ($Request.Body | ConvertTo-Json) + Write-AlertMessage -message $Message -sev 'Alert' -tenant $Tenant.customerId -LogData $Request.body } # Associate values to output bindings by calling 'Push-OutputBinding'. diff --git a/Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 b/Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 index a7a19d9b6340..30eea9247a42 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Write-AlertMessage.ps1 @@ -1,4 +1,4 @@ -function Write-AlertMessage($message, $tenant = 'None', $tenantId = $null) { +function Write-AlertMessage($message, $tenant = 'None', $tenantId = $null, $LogData = @{}) { <# .FUNCTIONALITY Internal @@ -10,7 +10,7 @@ function Write-AlertMessage($message, $tenant = 'None', $tenantId = $null) { $ExistingMessage = Get-CIPPAzDataTableEntity @Table -Filter $Filter if (!$ExistingMessage) { Write-Host 'No duplicate message found, writing to log' - Write-LogMessage -message $message -tenant $tenant -sev 'Alert' -tenantId $tenantId -API 'Alerts' + Write-LogMessage -message $message -tenant $tenant -sev 'Alert' -tenantId $tenantId -API 'Alerts' -LogData $LogData } else { Write-Host 'Alerts: Duplicate entry found, not writing to log' From eff059a02d748a534a654ee66f6457de9b2f377e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 6 Sep 2025 15:28:30 -0400 Subject: [PATCH 47/68] fix odata --- Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 index bb98a0b77825..b2ae8f4555cc 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 @@ -42,7 +42,7 @@ function Get-Tenants { $IncludedTenantFilter = [scriptblock]::Create("`$_.customerId -eq '$TenantFilter'") $RelationshipFilter = " and customer/tenantId eq '$TenantFilter'" } else { - $Filter = "{0} and defaultDomainName eq '{1}' -or initialDomainName eq '{1}'" -f $Filter, $TenantFilter + $Filter = "{0} and defaultDomainName eq '{1}' or initialDomainName eq '{1}'" -f $Filter, $TenantFilter $IncludedTenantFilter = [scriptblock]::Create("`$_.defaultDomainName -eq '$TenantFilter' -or `$_.initialDomainName -eq '$TenantFilter'") $RelationshipFilter = '' } From baa198da70db2c98e5d6c14ad1245abba5756fdf Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 7 Sep 2025 15:06:10 +0200 Subject: [PATCH 48/68] Deployment group templates fix --- .../Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 0b5decdbbb8e..4a82d249bd69 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -28,7 +28,6 @@ function Invoke-CIPPStandardGroupTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') -SkipLog ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GroupTemplate' $existingGroups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $tenant From ce08eb404caf58ce47cb8f1649286ea0cd39e96c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 7 Sep 2025 15:26:20 +0200 Subject: [PATCH 49/68] backoff logic --- .../GraphHelper/New-GraphGetRequest.ps1 | 139 +++++++++++------- 1 file changed, 88 insertions(+), 51 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index e7a3ad3bdf9b..f2c5eaf0a16f 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -56,71 +56,108 @@ function New-GraphGetRequest { } $ReturnedData = do { - try { - $GraphRequest = @{ - Uri = $nextURL - Method = 'GET' - Headers = $headers - ContentType = 'application/json; charset=utf-8' - } - if ($IncludeResponseHeaders) { - $GraphRequest.ResponseHeadersVariable = 'ResponseHeaders' - } + $RetryCount = 0 + $MaxRetries = 3 + $RequestSuccessful = $false - if ($ReturnRawResponse) { - $GraphRequest.SkipHttpErrorCheck = $true - $Data = Invoke-WebRequest @GraphRequest - } else { - $Data = (Invoke-RestMethod @GraphRequest) - } + do { + try { + $GraphRequest = @{ + Uri = $nextURL + Method = 'GET' + Headers = $headers + ContentType = 'application/json; charset=utf-8' + } + if ($IncludeResponseHeaders) { + $GraphRequest.ResponseHeadersVariable = 'ResponseHeaders' + } - if ($ReturnRawResponse) { - if (Test-Json -Json $Data.Content) { - $Content = $Data.Content | ConvertFrom-Json + if ($ReturnRawResponse) { + $GraphRequest.SkipHttpErrorCheck = $true + $Data = Invoke-WebRequest @GraphRequest } else { - $Content = $Data.Content + $Data = (Invoke-RestMethod @GraphRequest) } - $Data | Select-Object -Property StatusCode, StatusDescription, @{Name = 'Content'; Expression = { $Content }} - $nextURL = $null - } elseif ($CountOnly) { - $Data.'@odata.count' - $NextURL = $null - } else { - if ($Data.PSObject.Properties.Name -contains 'value') { $data.value } else { $Data } - if ($noPagination -eq $true) { - if ($Caller -eq 'Get-GraphRequestList') { - @{ 'nextLink' = $data.'@odata.nextLink' } + # If we reach here, the request was successful + $RequestSuccessful = $true + + if ($ReturnRawResponse) { + if (Test-Json -Json $Data.Content) { + $Content = $Data.Content | ConvertFrom-Json + } else { + $Content = $Data.Content } + + $Data | Select-Object -Property StatusCode, StatusDescription, @{Name = 'Content'; Expression = { $Content }} $nextURL = $null + } elseif ($CountOnly) { + $Data.'@odata.count' + $NextURL = $null } else { - $NextPageUriFound = $false - if ($IncludeResponseHeaders) { - if ($ResponseHeaders.NextPageUri) { - $NextURL = $ResponseHeaders.NextPageUri - $NextPageUriFound = $true + if ($Data.PSObject.Properties.Name -contains 'value') { $data.value } else { $Data } + if ($noPagination -eq $true) { + if ($Caller -eq 'Get-GraphRequestList') { + @{ 'nextLink' = $data.'@odata.nextLink' } + } + $nextURL = $null + } else { + $NextPageUriFound = $false + if ($IncludeResponseHeaders) { + if ($ResponseHeaders.NextPageUri) { + $NextURL = $ResponseHeaders.NextPageUri + $NextPageUriFound = $true + } } + if (!$NextPageUriFound) { + $nextURL = $data.'@odata.nextLink' + } + } + } + } catch { + $ShouldRetry = $false + $WaitTime = 0 + + try { + $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message + } catch { $Message = $null } + if ($Message -eq $null) { $Message = $($_.Exception.Message) } + + # Check for 429 Too Many Requests + if ($_.Exception.Response.StatusCode -eq 429) { + $RetryAfterHeader = $_.Exception.Response.Headers['Retry-After'] + if ($RetryAfterHeader) { + $WaitTime = [int]$RetryAfterHeader + Write-Warning "Rate limited (429). Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries" + $ShouldRetry = $true } - if (!$NextPageUriFound) { - $nextURL = $data.'@odata.nextLink' + } + # Check for "Resource temporarily unavailable" + elseif ($Message -like "*Resource temporarily unavailable*") { + if ($RetryCount -lt $MaxRetries) { + $WaitTime = Get-Random -Minimum 1 -Maximum 10 # Random sleep between 1-10 seconds + Write-Warning "Resource temporarily unavailable. Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries" + $ShouldRetry = $true } } - } - } catch { - try { - $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message - } catch { $Message = $null } - if ($Message -eq $null) { $Message = $($_.Exception.Message) } - if ($Message -ne 'Request not applicable to target tenant.' -and $Tenant) { - $Tenant.LastGraphError = $Message - if ($Tenant.PSObject.Properties.Name -notcontains 'GraphErrorCount') { - $Tenant | Add-Member -MemberType NoteProperty -Name 'GraphErrorCount' -Value 0 -Force + + if ($ShouldRetry -and $RetryCount -lt $MaxRetries) { + $RetryCount++ + Start-Sleep -Seconds $WaitTime + } else { + # Final failure - update tenant error tracking and throw + if ($Message -ne 'Request not applicable to target tenant.' -and $Tenant) { + $Tenant.LastGraphError = $Message + if ($Tenant.PSObject.Properties.Name -notcontains 'GraphErrorCount') { + $Tenant | Add-Member -MemberType NoteProperty -Name 'GraphErrorCount' -Value 0 -Force + } + $Tenant.GraphErrorCount++ + Update-AzDataTableEntity -Force @TenantsTable -Entity $Tenant + } + throw $Message } - $Tenant.GraphErrorCount++ - Update-AzDataTableEntity -Force @TenantsTable -Entity $Tenant } - throw $Message - } + } while (-not $RequestSuccessful -and $RetryCount -le $MaxRetries) } until ([string]::IsNullOrEmpty($NextURL) -or $NextURL -is [object[]] -or ' ' -eq $NextURL) if ($Tenant.PSObject.Properties.Name -notcontains 'LastGraphError') { $Tenant | Add-Member -MemberType NoteProperty -Name 'LastGraphError' -Value '' -Force From bffce6ce53c9d8a0f7c590fcd4838e7bc6cf1770 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 7 Sep 2025 21:04:15 +0200 Subject: [PATCH 50/68] Changes --- Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index f2c5eaf0a16f..e1281bff1c28 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -59,7 +59,7 @@ function New-GraphGetRequest { $RetryCount = 0 $MaxRetries = 3 $RequestSuccessful = $false - + Write-Host "This is attempt $($RetryCount + 1) of $MaxRetries" do { try { $GraphRequest = @{ @@ -89,7 +89,7 @@ function New-GraphGetRequest { $Content = $Data.Content } - $Data | Select-Object -Property StatusCode, StatusDescription, @{Name = 'Content'; Expression = { $Content }} + $Data | Select-Object -Property StatusCode, StatusDescription, @{Name = 'Content'; Expression = { $Content } } $nextURL = $null } elseif ($CountOnly) { $Data.'@odata.count' @@ -133,7 +133,7 @@ function New-GraphGetRequest { } } # Check for "Resource temporarily unavailable" - elseif ($Message -like "*Resource temporarily unavailable*") { + elseif ($Message -like '*Resource temporarily unavailable*') { if ($RetryCount -lt $MaxRetries) { $WaitTime = Get-Random -Minimum 1 -Maximum 10 # Random sleep between 1-10 seconds Write-Warning "Resource temporarily unavailable. Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries" From 2ae506846a5fd85c37c4f88cd5fe3a4f923c99df Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 7 Sep 2025 16:27:30 -0400 Subject: [PATCH 51/68] rate limiting in user tasks --- Config/SchedulerRateLimits.json | 10 ++ .../Start-UserTasksOrchestrator.ps1 | 94 ++++++++++++++++--- 2 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 Config/SchedulerRateLimits.json diff --git a/Config/SchedulerRateLimits.json b/Config/SchedulerRateLimits.json new file mode 100644 index 000000000000..3d2c65716af0 --- /dev/null +++ b/Config/SchedulerRateLimits.json @@ -0,0 +1,10 @@ +[ + { + "Command": "Sync-CIPPExtensionData", + "MaxRequests": 50 + }, + { + "Command": "Push-CIPPExtensionData", + "MaxRequests": 30 + } +] \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index b887e41d3366..87404edca459 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -10,10 +10,39 @@ function Start-UserTasksOrchestrator { $1HourAgo = (Get-Date).AddHours(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Running' and Timestamp lt datetime'$1HourAgo'))" $tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + $RateLimitTable = Get-CIPPTable -tablename 'SchedulerRateLimits' + $RateLimits = Get-CIPPAzDataTableEntity @RateLimitTable -Filter "PartitionKey eq 'SchedulerRateLimits'" + + $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase + $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent + $DefaultRateLimits = Get-Content -Path "$CIPPRoot/Config/SchedulerRateLimits.json" | ConvertFrom-Json + $NewRateLimits = foreach ($Limit in $DefaultRateLimits) { + if ($Limit.Command -notin $RateLimits.RowKey) { + @{ + PartitionKey = 'SchedulerRateLimits' + RowKey = $Limit.Command + MaxRequests = $Limit.MaxRequests + } + } + } + + if ($NewRateLimits) { + $null = Add-CIPPAzDataTableEntity @RateLimitTable -Entity $NewRateLimits -Force + $RateLimits = Get-CIPPAzDataTableEntity @RateLimitTable -Filter "PartitionKey eq 'SchedulerRateLimits'" + } + + # Create a hashtable for quick rate limit lookups + $RateLimitLookup = @{} + foreach ($limit in $RateLimits) { + $RateLimitLookup[$limit.RowKey] = $limit.MaxRequests + } + $Batch = [System.Collections.Generic.List[object]]::new() $TenantList = Get-Tenants -IncludeErrors foreach ($task in $tasks) { $tenant = $task.Tenant + $currentUnixTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds if ($currentUnixTime -ge $task.ScheduledTime) { try { @@ -113,21 +142,62 @@ function Start-UserTasksOrchestrator { } } } + + Write-Information 'Batching tasks for execution...' + Write-Information "Total tasks to process: $($Batch.Count)" + if (($Batch | Measure-Object).Count -gt 0) { - # Create queue entry - $Queue = New-CippQueueEntry -Name 'Scheduled Tasks' -TotalTasks ($Batch | Measure-Object).Count - $QueueId = $Queue.RowKey - $Batch = $Batch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, ($_.TaskInfo.Tenant -ne 'AllTenants' ? $_.TaskInfo.Tenant : $_.Parameters.TenantFilter) } } - - $InputObject = [PSCustomObject]@{ - OrchestratorName = 'UserTaskOrchestrator' - Batch = @($Batch) - SkipLog = $true + # Group commands by type and apply rate limits + $CommandGroups = $Batch | Group-Object -Property Command + $ProcessedBatches = [System.Collections.Generic.List[object]]::new() + + foreach ($CommandGroup in $CommandGroups) { + $CommandName = $CommandGroup.Name + $Commands = [System.Collections.Generic.List[object]]::new($CommandGroup.Group) + + # Get rate limit for this command (default to 100 if not found) + $MaxItemsPerBatch = if ($RateLimitLookup.ContainsKey($CommandName)) { + $RateLimitLookup[$CommandName] + } else { + 100 + } + + # Split into batches based on rate limit + while ($Commands.Count -gt 0) { + $BatchSize = [Math]::Min($Commands.Count, $MaxItemsPerBatch) + $CommandBatch = [System.Collections.Generic.List[object]]::new() + + for ($i = 0; $i -lt $BatchSize; $i++) { + $CommandBatch.Add($Commands[0]) + $Commands.RemoveAt(0) + } + + $ProcessedBatches.Add($CommandBatch) + } } - #Write-Host ($InputObject | ConvertTo-Json -Depth 10) - if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting User Tasks Orchestrator')) { - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + # Process each batch separately + foreach ($ProcessedBatch in $ProcessedBatches) { + Write-Information "Processing batch with $($ProcessedBatch.Count) tasks..." + Write-Information 'Tasks by command:' + $ProcessedBatch | Group-Object -Property Command | ForEach-Object { + Write-Information " - $($_.Name): $($_.Count)" + } + + # Create queue entry for each batch + $Queue = New-CippQueueEntry -Name "Scheduled Tasks - Batch #$($ProcessedBatches.IndexOf($ProcessedBatch) + 1) of $($ProcessedBatches.Count)" + $QueueId = $Queue.RowKey + $BatchWithQueue = $ProcessedBatch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, ($_.TaskInfo.Tenant -ne 'AllTenants' ? $_.TaskInfo.Tenant : $_.Parameters.TenantFilter) } } + + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'UserTaskOrchestrator' + Batch = @($BatchWithQueue) + SkipLog = $true + } + + if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting User Tasks Orchestrator')) { + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + } } } } From 37483879aeeb1817da2731c51034c24d760a7474 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 8 Sep 2025 00:41:23 +0200 Subject: [PATCH 52/68] fix display name retrieval --- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 57 +++++++++++++---------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index f08cd5e573ff..bb32924cc8c4 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -29,6 +29,24 @@ function Get-CIPPDrift { [switch]$AllTenants ) + + $IntuneTable = Get-CippTable -tablename 'templates' + $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" + $RawIntuneTemplates = (Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter) + $AllIntuneTemplates = $RawIntuneTemplates | ForEach-Object { + try { + $JSONData = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force + $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force + $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data + } catch { + # Skip invalid templates + } + } | Sort-Object -Property displayName + try { $AlignmentData = Get-CIPPTenantAlignment -TenantFilter $TenantFilter -TemplateId $TemplateId | Where-Object -Property standardType -EQ 'drift' if (-not $AlignmentData) { @@ -64,16 +82,24 @@ function Get-CIPPDrift { } else { 'New' } + #if the $ComparisonItem.StandardName contains *intuneTemplate*, then it's an Intune policy deviation, and we need to grab the correct displayname from the template table + if ($ComparisonItem.StandardName -like '*intuneTemplate*') { + $CompareGuid = $ComparisonItem.StandardName.Split('.') | Select-Object -Index 2 + Write-Host "Extracted GUID: $CompareGuid" + $Template = $AllIntuneTemplates | Where-Object { $_.GUID -like "*$CompareGuid*" } + if ($Template) { $displayName = $Template.displayName } + } $reason = if ($ExistingDriftStates.ContainsKey($ComparisonItem.StandardName)) { $ExistingDriftStates[$ComparisonItem.StandardName].Reason } $User = if ($ExistingDriftStates.ContainsKey($ComparisonItem.StandardName)) { $ExistingDriftStates[$ComparisonItem.StandardName].User } $StandardsDeviations.Add([PSCustomObject]@{ - standardName = $ComparisonItem.StandardName - expectedValue = 'Compliant' - receivedValue = $ComparisonItem.StandardValue - state = 'current' - Status = $Status - Reason = $reason - lastChangedByUser = $User + standardName = $ComparisonItem.StandardName + standardDisplayName = $displayName + expectedValue = 'Compliant' + receivedValue = $ComparisonItem.StandardValue + state = 'current' + Status = $Status + Reason = $reason + lastChangedByUser = $User }) } } @@ -194,22 +220,6 @@ function Get-CIPPDrift { # Get actual Intune templates from templates table if ($IntuneTemplateIds.Count -gt 0) { try { - $IntuneTable = Get-CippTable -tablename 'templates' - $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" - $RawIntuneTemplates = (Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter) - $AllIntuneTemplates = $RawIntuneTemplates | ForEach-Object { - try { - $JSONData = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force - $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force - $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force - $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force - $data - } catch { - # Skip invalid templates - } - } | Sort-Object -Property displayName $TemplateIntuneTemplates = $AllIntuneTemplates | Where-Object { $_.GUID -in $IntuneTemplateIds } } catch { @@ -222,7 +232,6 @@ function Get-CIPPDrift { $PolicyFound = $false $tenantPolicy.policy | Add-Member -MemberType NoteProperty -Name 'URLName' -Value $TenantPolicy.Type -Force $TenantPolicyName = if ($TenantPolicy.Policy.displayName) { $TenantPolicy.Policy.displayName } else { $TenantPolicy.Policy.name } - foreach ($TemplatePolicy in $TemplateIntuneTemplates) { $TemplatePolicyName = if ($TemplatePolicy.displayName) { $TemplatePolicy.displayName } else { $TemplatePolicy.name } From 3e6d1ba2386ba5c0ca38d9a21f0391c907e4c1c6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 8 Sep 2025 00:59:19 +0200 Subject: [PATCH 53/68] allow array --- .../CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 index 41a0ae5a2854..8edf5c9f2681 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAddTrustedIP.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecAddTrustedIP { +function Invoke-ExecAddTrustedIP { <# .FUNCTIONALITY Entrypoint @@ -11,12 +11,13 @@ Function Invoke-ExecAddTrustedIP { param($Request, $TriggerMetadata) $Table = Get-CippTable -tablename 'trustedIps' - Add-CIPPAzDataTableEntity @Table -Entity @{ - PartitionKey = $Request.Body.tenantfilter - RowKey = $Request.Body.IP - state = $Request.Body.State - } -Force - + foreach ($IP in $Request.body.IP) { + Add-CIPPAzDataTableEntity @Table -Entity @{ + PartitionKey = $Request.Body.tenantfilter + RowKey = $IP + state = $Request.Body.State + } -Force + } Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = @{ results = "Added $($Request.Body.IP) to database with state $($Request.Body.State) for $($Request.Body.tenantfilter)" } From e55f664f9e28f99fc5b9a59f33ae8abff30bdaf6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:23:21 +0200 Subject: [PATCH 54/68] CIS Templates --- CommunityRepos.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CommunityRepos.json b/CommunityRepos.json index e34aafb8c821..bf4d51d000be 100644 --- a/CommunityRepos.json +++ b/CommunityRepos.json @@ -1,4 +1,22 @@ [ + { + "Id": "1041442982", + "Name": "CISTemplates", + "Description": "CIPP CIS Templates", + "URL": "https://github.com/CyberDrain/CyberDrain-CIS-Templates", + "FullName": "CyberDrain/CyberDrain-CIS-Templates", + "Owner": "CyberDrain", + "Visibility": "public", + "WriteAccess": false, + "DefaultBranch": "main", + "RepoPermissions": { + "admin": false, + "maintain": false, + "push": false, + "triage": false, + "pull": true + } + }, { "Id": "930523724", "Name": "CIPP-Templates", From 9c13efdece61c0759413193fa1885f3175364087 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:51:56 +0200 Subject: [PATCH 55/68] updated community repos --- CommunityRepos.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CommunityRepos.json b/CommunityRepos.json index bf4d51d000be..9d6d702d0161 100644 --- a/CommunityRepos.json +++ b/CommunityRepos.json @@ -70,5 +70,23 @@ "triage": false, "pull": true } + }, + { + "Id": "863076113", + "Name": "IntuneBaseLines", + "Description": "In this repo, you will find Intune profiles in JSON format, which can be used in setting up your Modern Workplace. All policies were created in Microsoft Intune and exported to share with the community.", + "URL": "https://github.com/IntuneAdmin/IntuneBaselines", + "FullName": "IntuneAdmin/IntuneBaselines", + "Owner": "IntuneAdmin", + "Visibility": "public", + "WriteAccess": false, + "DefaultBranch": "main", + "RepoPermissions": { + "admin": false, + "maintain": false, + "push": false, + "triage": false, + "pull": true + } } ] From 8567bf99907fe3853ab21415d476006302e5a409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 8 Sep 2025 16:20:45 +0200 Subject: [PATCH 56/68] refactor: use splatting Fix: Remove invalid value and unused ID variable Feat: Add windows updates option Refactor: readabillity dawg --- .../Autopilot/Invoke-AddEnrollment.ps1 | 29 ++++++++++++------- .../Autopilot/Invoke-ListAutopilotconfig.ps1 | 12 ++++---- .../Public/Set-CIPPDefaultAPEnrollment.ps1 | 8 ++--- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddEnrollment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddEnrollment.ps1 index b8f8a4c34fe2..a21767f47bae 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddEnrollment.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-AddEnrollment.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-AddEnrollment { +function Invoke-AddEnrollment { <# .FUNCTIONALITY Entrypoint @@ -14,22 +14,29 @@ Function Invoke-AddEnrollment { $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - - # Input bindings are passed in via param block. - $Tenants = $Request.body.selectedTenants.value - $Profbod = $Request.body - $results = foreach ($Tenant in $tenants) { - Set-CIPPDefaultAPEnrollment -TenantFilter $Tenant -ShowProgress $Profbod.ShowProgress -BlockDevice $Profbod.blockDevice -AllowReset $Profbod.AllowReset -EnableLog $Profbod.EnableLog -ErrorMessage $Profbod.ErrorMessage -TimeOutInMinutes $Profbod.TimeOutInMinutes -AllowFail $Profbod.AllowFail -OBEEOnly $Profbod.OBEEOnly + $Tenants = $Request.Body.selectedTenants.value + $Profbod = $Request.Body + $Results = foreach ($Tenant in $Tenants) { + $ParamSplat = @{ + TenantFilter = $Tenant + ShowProgress = $Profbod.ShowProgress + BlockDevice = $Profbod.blockDevice + AllowReset = $Profbod.AllowReset + EnableLog = $Profbod.EnableLog + ErrorMessage = $Profbod.ErrorMessage + TimeOutInMinutes = $Profbod.TimeOutInMinutes + AllowFail = $Profbod.AllowFail + OBEEOnly = $Profbod.OBEEOnly + InstallWindowsUpdates = $Profbod.InstallWindowsUpdates + } + Set-CIPPDefaultAPEnrollment @ParamSplat } - $body = [pscustomobject]@{'Results' = $results } - # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = $body + Body = @{'Results' = $Results } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 index a9a7bfdc0717..f79ed07ab422 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Autopilot/Invoke-ListAutopilotconfig.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListAutopilotconfig { +function Invoke-ListAutopilotconfig { <# .FUNCTIONALITY Entrypoint @@ -15,18 +15,16 @@ Function Invoke-ListAutopilotconfig { Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - - # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter - $userid = $Request.Query.UserID try { - if ($request.query.type -eq 'ApProfile') { + if ($Request.Query.type -eq 'ApProfile') { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles?`$expand=assignments" -tenantid $TenantFilter } - if ($request.query.type -eq 'ESP') { - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments" -tenantid $TenantFilter | Where-Object -Property '@odata.type' -EQ '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' + if ($Request.Query.type -eq 'ESP') { + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments" -tenantid $TenantFilter | + Where-Object -Property '@odata.type' -EQ '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' } $StatusCode = [HttpStatusCode]::OK } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDefaultAPEnrollment.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefaultAPEnrollment.ps1 index d02794c16c0a..171e1d732695 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDefaultAPEnrollment.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDefaultAPEnrollment.ps1 @@ -8,14 +8,13 @@ function Set-CIPPDefaultAPEnrollment { $EnableLog, $ErrorMessage, $TimeOutInMinutes, + $InstallWindowsUpdates, $AllowFail, $OBEEOnly, $Headers, $APIName = 'Add Default Enrollment Status Page' ) - $User = $Request.Headers - try { $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' @@ -28,6 +27,7 @@ function Set-CIPPDefaultAPEnrollment { 'allowLogCollectionOnInstallFailure' = [bool]$EnableLog 'customErrorMessage' = "$ErrorMessage" 'installProgressTimeoutInMinutes' = $TimeOutInMinutes + 'installQualityUpdates' = [bool]$InstallWindowsUpdates 'allowDeviceUseOnInstallFailure' = [bool]$AllowFail 'selectedMobileAppIds' = @() 'trackInstallProgressForAutopilotOnly' = [bool]$OBEEOnly @@ -40,11 +40,11 @@ function Set-CIPPDefaultAPEnrollment { if ($PSCmdlet.ShouldProcess($ExistingStatusPage.ID, 'Set Default Enrollment Status Page')) { $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/$($ExistingStatusPage.ID)" -body $Body -Type PATCH -tenantid $TenantFilter "Successfully changed default enrollment status page for $TenantFilter" - Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Added Autopilot Enrollment Status Page $($ExistingStatusPage.displayName)" -Sev 'Info' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Added Autopilot Enrollment Status Page $($ExistingStatusPage.displayName)" -Sev 'Info' } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Failed adding Autopilot Enrollment Status Page $($ExistingStatusPage.displayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Failed adding Autopilot Enrollment Status Page $($ExistingStatusPage.displayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage throw "Failed to change default enrollment status page for $($TenantFilter): $($ErrorMessage.NormalizedError)" } } From 12b53e9ef8ec28ea358c579960bcee8c52de7cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 8 Sep 2025 16:39:38 +0200 Subject: [PATCH 57/68] feat: enhance Autopilot Status Page settings with new options and compatibility improvements --- ...Invoke-CIPPStandardAutopilotStatusPage.ps1 | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 index 97851eefeeb0..33038ccdc207 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 @@ -15,14 +15,16 @@ function Invoke-CIPPStandardAutopilotStatusPage { TAG DISABLEDFEATURES {"report":false,"warn":false,"remediate":false} + EXECUTIVETEXT + Provides employees with a visual progress indicator during automated device setup, improving the user experience when receiving new computers. This reduces IT support calls and helps ensure successful device deployment by guiding users through the setup process. ADDEDCOMPONENT {"type":"number","name":"standards.AutopilotStatusPage.TimeOutInMinutes","label":"Timeout in minutes","defaultValue":60} {"type":"textField","name":"standards.AutopilotStatusPage.ErrorMessage","label":"Custom Error Message","required":false} {"type":"switch","name":"standards.AutopilotStatusPage.ShowProgress","label":"Show progress to users","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.EnableLog","label":"Turn on log collection","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.OBEEOnly","label":"Show status page only with OOBE setup","defaultValue":true} + {"type":"switch","name":"standards.AutopilotStatusPage.InstallWindowsUpdates","label":"Install Windows Updates during setup","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.BlockDevice","label":"Block device usage during setup","defaultValue":true} - {"type":"switch","name":"standards.AutopilotStatusPage.AllowRetry","label":"Allow retry","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.AllowReset","label":"Allow reset","defaultValue":true} {"type":"switch","name":"standards.AutopilotStatusPage.AllowFail","label":"Allow users to use device if setup fails","defaultValue":true} IMPACT @@ -46,7 +48,10 @@ function Invoke-CIPPStandardAutopilotStatusPage { } #we're done. try { $CurrentConfig = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments&orderBy=priority&`$filter=deviceEnrollmentConfigurationType eq 'windows10EnrollmentCompletionPageConfiguration' and priority eq 0" -tenantid $Tenant | - Select-Object -Property id, displayName, priority, showInstallationProgress, blockDeviceSetupRetryByUser, allowDeviceResetOnInstallFailure, allowLogCollectionOnInstallFailure, customErrorMessage, installProgressTimeoutInMinutes, allowDeviceUseOnInstallFailure, trackInstallProgressForAutopilotOnly + Select-Object -Property id, displayName, priority, showInstallationProgress, blockDeviceSetupRetryByUser, allowDeviceResetOnInstallFailure, allowLogCollectionOnInstallFailure, customErrorMessage, installProgressTimeoutInMinutes, allowDeviceUseOnInstallFailure, trackInstallProgressForAutopilotOnly, installQualityUpdates + + # Compatibility for standards made in v8.3.0 or before, which did not have the InstallWindowsUpdates setting + $InstallWindowsUpdates = $Settings.InstallWindowsUpdates ?? $false $StateIsCorrect = ($CurrentConfig.installProgressTimeoutInMinutes -eq $Settings.TimeOutInMinutes) -and ($CurrentConfig.customErrorMessage -eq $Settings.ErrorMessage) -and @@ -54,6 +59,7 @@ function Invoke-CIPPStandardAutopilotStatusPage { ($CurrentConfig.allowLogCollectionOnInstallFailure -eq $Settings.EnableLog) -and ($CurrentConfig.trackInstallProgressForAutopilotOnly -eq $Settings.OBEEOnly) -and ($CurrentConfig.blockDeviceSetupRetryByUser -eq !$Settings.BlockDevice) -and + ($CurrentConfig.installQualityUpdates -eq $InstallWindowsUpdates) -and ($CurrentConfig.allowDeviceResetOnInstallFailure -eq $Settings.AllowReset) -and ($CurrentConfig.allowDeviceUseOnInstallFailure -eq $Settings.AllowFail) } catch { @@ -66,15 +72,16 @@ function Invoke-CIPPStandardAutopilotStatusPage { if ($Settings.remediate -eq $true) { try { $Parameters = @{ - TenantFilter = $Tenant - ShowProgress = $Settings.ShowProgress - BlockDevice = $Settings.BlockDevice - AllowReset = $Settings.AllowReset - EnableLog = $Settings.EnableLog - ErrorMessage = $Settings.ErrorMessage - TimeOutInMinutes = $Settings.TimeOutInMinutes - AllowFail = $Settings.AllowFail - OBEEOnly = $Settings.OBEEOnly + TenantFilter = $Tenant + ShowProgress = $Settings.ShowProgress + BlockDevice = $Settings.BlockDevice + InstallWindowsUpdates = $InstallWindowsUpdates + AllowReset = $Settings.AllowReset + EnableLog = $Settings.EnableLog + ErrorMessage = $Settings.ErrorMessage + TimeOutInMinutes = $Settings.TimeOutInMinutes + AllowFail = $Settings.AllowFail + OBEEOnly = $Settings.OBEEOnly } Set-CIPPDefaultAPEnrollment @Parameters From 8e7f84719b98f16b54b2f362ab09a02ae1ba44f0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 8 Sep 2025 18:07:47 -0400 Subject: [PATCH 58/68] chore: refactor groups/group templates --- .../Administration/Groups/Invoke-AddGroup.ps1 | 77 +----- .../Groups/Invoke-AddGroupTemplate.ps1 | 45 ++-- .../Groups/Invoke-ListGroupTemplates.ps1 | 18 +- Modules/CIPPCore/Public/New-CIPPGroup.ps1 | 236 ++++++++++++++++ .../Invoke-CIPPStandardGroupTemplate.ps1 | 253 ++++++++++++------ 5 files changed, 464 insertions(+), 165 deletions(-) create mode 100644 Modules/CIPPCore/Public/New-CIPPGroup.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 index 8a8a9e3906cf..ee0de43333b1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 @@ -19,80 +19,15 @@ function Invoke-AddGroup { $Results = foreach ($tenant in $SelectedTenants) { try { - $Email = if ($GroupObject.primDomain.value) { "$($GroupObject.username)@$($GroupObject.primDomain.value)" } else { "$($GroupObject.username)@$($tenant)" } - if ($GroupObject.groupType -in 'Generic', 'azurerole', 'dynamic', 'm365') { + # Use the centralized New-CIPPGroup function + $Result = New-CIPPGroup -GroupObject $GroupObject -TenantFilter $tenant -APIName $APIName -ExecutingUser $Request.Headers.'x-ms-client-principal-name' - $BodyParams = [pscustomobject] @{ - 'displayName' = $GroupObject.displayName - 'description' = $GroupObject.description - 'mailNickname' = $GroupObject.username - mailEnabled = [bool]$false - securityEnabled = [bool]$true - isAssignableToRole = [bool]($GroupObject | Where-Object -Property groupType -EQ 'AzureRole') - } - if ($GroupObject.membershipRules) { - $BodyParams | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue ($GroupObject.membershipRules) - $BodyParams | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' - if ($GroupObject.groupType -eq 'm365') { - $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified', 'DynamicMembership') - $BodyParams.mailEnabled = $true - } else { - $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') - } - # Skip adding static members if we're using dynamic membership - $SkipStaticMembers = $true - } elseif ($GroupObject.groupType -eq 'm365') { - $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified') - $BodyParams.mailEnabled = $true - } - if ($GroupObject.owners) { - $BodyParams | Add-Member -NotePropertyName 'owners@odata.bind' -NotePropertyValue (($GroupObject.owners) | ForEach-Object { "https://graph.microsoft.com/v1.0/users/$($_.value)" }) - $BodyParams.'owners@odata.bind' = @($BodyParams.'owners@odata.bind') - } - if ($GroupObject.members -and -not $SkipStaticMembers) { - $BodyParams | Add-Member -NotePropertyName 'members@odata.bind' -NotePropertyValue (($GroupObject.members) | ForEach-Object { "https://graph.microsoft.com/v1.0/users/$($_.value)" }) - $BodyParams.'members@odata.bind' = @($BodyParams.'members@odata.bind') - } - $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenant -type POST -body (ConvertTo-Json -InputObject $BodyParams -Depth 10) -Verbose + if ($Result.Success) { + "Successfully created group $($GroupObject.displayName) for $($tenant)" + $StatusCode = [HttpStatusCode]::OK } else { - if ($GroupObject.groupType -eq 'dynamicDistribution') { - $ExoParams = @{ - Name = $GroupObject.displayName - RecipientFilter = $GroupObject.membershipRules - PrimarySmtpAddress = $Email - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DynamicDistributionGroup' -cmdParams $ExoParams - - if (!$GroupObject.allowExternal) { - $SetParams = @{ - RequireSenderAuthenticationEnabled = [bool]!$GroupObject.allowExternal - Name = $GroupObject.displayName - PrimarySmtpAddress = $Email - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $SetParams - } - } else { - $ExoParams = @{ - Name = $GroupObject.displayName - Alias = $GroupObject.username - Description = $GroupObject.description - PrimarySmtpAddress = $Email - Type = $GroupObject.groupType - RequireSenderAuthenticationEnabled = [bool]!$GroupObject.allowExternal - } - if ($GroupObject.owners) { - $ExoParams.ManagedBy = @($GroupObject.owners.value) - } - if ($GroupObject.members) { - $ExoParams.Members = @($GroupObject.members.value) - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DistributionGroup' -cmdParams $ExoParams - } + throw $Result.Message } - - "Successfully created group $($GroupObject.displayName) for $($tenant)" - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $tenant -message "Created group $($GroupObject.displayName) with id $($GraphRequest.id)" -Sev Info - $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Request.Headers -API $APIName -tenant $tenant -message "Group creation API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 index 51380b38dc68..d04461e38d22 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 @@ -15,26 +15,41 @@ function Invoke-AddGroupTemplate { $GUID = $Request.Body.GUID ?? (New-Guid).GUID try { - if (!$Request.Body.displayname) { throw 'You must enter a displayname' } - $groupType = switch -wildcard ($Request.Body.groupType) { - '*dynamic*' { 'dynamic' } - '*azurerole*' { 'azurerole' } - '*unified*' { 'm365' } - '*Microsoft*' { 'm365' } - '*generic*' { 'generic' } - '*mail*' { 'mailenabledsecurity' } - '*Distribution*' { 'distribution' } - '*security*' { 'security' } + if (!$Request.Body.displayName) { + throw 'You must enter a displayname' + } + + # Normalize group type to match New-CIPPGroup expectations (handle both camelCase and lowercase) + $groupType = switch -wildcard ($Request.Body.groupType.ToLower()) { + '*dynamicdistribution*' { 'dynamicDistribution'; break } # Check this first before *dynamic* and *distribution* + '*dynamic*' { 'dynamic'; break } + '*azurerole*' { 'azureRole'; break } + '*unified*' { 'm365'; break } + '*microsoft*' { 'm365'; break } + '*m365*' { 'm365'; break } + '*generic*' { 'generic'; break } + '*security*' { 'security'; break } + '*distribution*' { 'distribution'; break } + '*mail*' { 'distribution'; break } default { $Request.Body.groupType } } - if ($Request.body.membershipRules) { $groupType = 'dynamic' } + + # Override to dynamic if membership rules are provided (for backward compatibility) + # but only if it's not already a dynamicDistribution group + if ($Request.body.membershipRules -and $groupType -notin @('dynamicDistribution')) { + $groupType = 'dynamic' + } + # Normalize field names to handle different casing from various forms + $displayName = $Request.Body.displayName ?? $Request.Body.Displayname ?? $Request.Body.displayname + $description = $Request.Body.description ?? $Request.Body.Description + $object = [PSCustomObject]@{ - displayName = $Request.Body.displayName - description = $Request.Body.description + displayName = $displayName + description = $description groupType = $groupType membershipRules = $Request.Body.membershipRules allowExternal = $Request.Body.allowExternal - username = $Request.Body.username + username = $Request.Body.username # Can contain variables like @%tenantfilter% GUID = $GUID } | ConvertTo-Json $Table = Get-CippTable -tablename 'templates' @@ -44,7 +59,7 @@ function Invoke-AddGroupTemplate { RowKey = "$GUID" PartitionKey = 'GroupTemplate' } - Write-LogMessage -headers $Request.Headers -API $APINAME -message "Created Group template named $($Request.Body.displayname) with GUID $GUID" -Sev 'Debug' + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Created Group template named $displayName with GUID $GUID" -Sev 'Debug' $body = [pscustomobject]@{'Results' = 'Successfully added template' } } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 index 2e9364e73535..8a4d4b0034dd 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 @@ -22,10 +22,26 @@ function Invoke-ListGroupTemplates { $Filter = "PartitionKey eq 'GroupTemplate'" $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { $data = $_.JSON | ConvertFrom-Json + + # Normalize groupType to camelCase for consistent frontend handling + $normalizedGroupType = switch -Wildcard ($data.groupType.ToLower()) { + '*dynamicdistribution*' { 'dynamicDistribution'; break } + '*dynamic*' { 'dynamic'; break } + '*azurerole*' { 'azureRole'; break } + '*unified*' { 'm365'; break } + '*microsoft*' { 'm365'; break } + '*m365*' { 'm365'; break } + '*generic*' { 'generic'; break } + '*security*' { 'security'; break } + '*distribution*' { 'distribution'; break } + '*mail*' { 'distribution'; break } + default { $data.groupType } + } + [PSCustomObject]@{ displayName = $data.displayName description = $data.description - groupType = $data.groupType + groupType = $normalizedGroupType membershipRules = $data.membershipRules allowExternal = $data.allowExternal username = $data.username diff --git a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 new file mode 100644 index 000000000000..066f4db19af0 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 @@ -0,0 +1,236 @@ +function New-CIPPGroup { + <# + .SYNOPSIS + Creates a new group in Microsoft 365 or Exchange Online + + .DESCRIPTION + Unified function for creating groups that handles all group types consistently. + Used by both direct group creation and group template application. + + .PARAMETER GroupObject + Object containing group properties (displayName, description, groupType, etc.) + + .PARAMETER TenantFilter + The tenant domain name where the group should be created + + .PARAMETER APIName + The API name for logging purposes + + .PARAMETER ExecutingUser + The user executing the request (for logging) + + .EXAMPLE + New-CIPPGroup -GroupObject $GroupData -TenantFilter 'contoso.com' -APIName 'AddGroup' + + .NOTES + Supports all group types: Generic, Security, AzureRole, Dynamic, M365, Distribution, DynamicDistribution + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [PSCustomObject]$GroupObject, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [string]$APIName = 'New-CIPPGroup', + + [Parameter(Mandatory = $false)] + [string]$ExecutingUser = 'CIPP' + ) + + try { + # Normalize group type for consistent handling (accept camelCase from templates) + $NormalizedGroupType = switch -Wildcard ($GroupObject.groupType.ToLower()) { + '*dynamicdistribution*' { 'DynamicDistribution'; break } # Check this first before *dynamic* and *distribution* + '*dynamic*' { 'Dynamic'; break } + '*generic*' { 'Generic'; break } + '*security*' { 'Security'; break } + '*azurerole*' { 'AzureRole'; break } + '*m365*' { 'M365'; break } + '*unified*' { 'M365'; break } + '*microsoft*' { 'M365'; break } + '*distribution*' { 'Distribution'; break } + '*mail*' { 'Distribution'; break } + default { $GroupObject.groupType } + } + + # Determine if this group type needs an email address + $GroupTypesNeedingEmail = @('M365', 'Distribution', 'DynamicDistribution') + $NeedsEmail = $NormalizedGroupType -in $GroupTypesNeedingEmail + + # Determine email address only for group types that need it + $Email = if ($NeedsEmail) { + if ($GroupObject.primDomain.value) { + "$($GroupObject.username)@$($GroupObject.primDomain.value)" + } elseif ($GroupObject.primaryEmailAddress) { + $GroupObject.primaryEmailAddress + } elseif ($GroupObject.username -like '*@*') { + # Username already contains an email address (e.g., from templates with @%tenantfilter%) + $GroupObject.username + } else { + "$($GroupObject.username)@$($TenantFilter)" + } + } else { + $null + } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Creating group $($GroupObject.displayName) of type $NormalizedGroupType$(if ($NeedsEmail) { " with email $Email" })" -Sev Info + + # Handle Graph API groups (Security, Generic, AzureRole, Dynamic, M365) + if ($NormalizedGroupType -in @('Generic', 'Security', 'AzureRole', 'Dynamic', 'M365')) { + Write-Information "Creating group $($GroupObject.displayName) of type $NormalizedGroupType$(if ($NeedsEmail) { " with email $Email" })" + $BodyParams = [PSCustomObject]@{ + 'displayName' = $GroupObject.displayName + 'description' = $GroupObject.description + 'mailNickname' = $GroupObject.username + 'mailEnabled' = $false + 'securityEnabled' = $true + 'isAssignableToRole' = ($NormalizedGroupType -eq 'AzureRole') + } + + # Handle dynamic membership + if ($GroupObject.membershipRules) { + $BodyParams | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue $GroupObject.membershipRules + $BodyParams | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' + + if ($NormalizedGroupType -eq 'M365') { + $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified', 'DynamicMembership') + $BodyParams.mailEnabled = $true + } else { + $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') + } + + # Skip adding static members for dynamic groups + $SkipStaticMembers = $true + } elseif ($NormalizedGroupType -eq 'M365') { + # Static M365 group + $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified') + $BodyParams.mailEnabled = $true + } + + # Add owners + if ($GroupObject.owners -and $GroupObject.owners.Count -gt 0) { + $OwnerBindings = $GroupObject.owners | ForEach-Object { + if ($_.value) { + "https://graph.microsoft.com/v1.0/users/$($_.value)" + } elseif ($_ -is [string]) { + "https://graph.microsoft.com/v1.0/users/$_" + } + } | Where-Object { $_ } + + if ($OwnerBindings) { + $BodyParams | Add-Member -NotePropertyName 'owners@odata.bind' -NotePropertyValue @($OwnerBindings) + } + } + + # Add members (only for non-dynamic groups) + if ($GroupObject.members -and $GroupObject.members.Count -gt 0 -and -not $SkipStaticMembers) { + $MemberBindings = $GroupObject.members | ForEach-Object { + if ($_.value) { + "https://graph.microsoft.com/v1.0/users/$($_.value)" + } elseif ($_ -is [string]) { + "https://graph.microsoft.com/v1.0/users/$_" + } + } | Where-Object { $_ } + + if ($MemberBindings) { + $BodyParams | Add-Member -NotePropertyName 'members@odata.bind' -NotePropertyValue @($MemberBindings) + } + } + + # Create the group via Graph API + $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter -type POST -body (ConvertTo-Json -InputObject $BodyParams -Depth 10) + + $Result = [PSCustomObject]@{ + Success = $true + Message = "Successfully created group $($GroupObject.displayName)" + GroupId = $GraphRequest.id + GroupType = $NormalizedGroupType + Email = if ($NeedsEmail) { $Email } else { $null } + } + + } else { + # Handle Exchange Online groups (Distribution, DynamicDistribution) + + if ($NormalizedGroupType -eq 'DynamicDistribution') { + Write-Information "Creating dynamic distribution group $($GroupObject.displayName) with email $Email" + $ExoParams = @{ + Name = $GroupObject.displayName + RecipientFilter = $GroupObject.membershipRules + PrimarySmtpAddress = $Email + } + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DynamicDistributionGroup' -cmdParams $ExoParams + + # Set external sender restrictions if specified + if ($null -ne $GroupObject.allowExternal -and $GroupObject.allowExternal -eq $true -and $GraphRequest.Identity) { + $SetParams = @{ + RequireSenderAuthenticationEnabled = [bool]!$GroupObject.allowExternal + Identity = $GraphRequest.Identity + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $SetParams + } + + } else { + # Regular Distribution Group + Write-Information "Creating distribution group $($GroupObject.displayName) with email $Email" + + $ExoParams = @{ + Name = $GroupObject.displayName + Alias = $GroupObject.username + Description = $GroupObject.description + PrimarySmtpAddress = $Email + Type = $GroupObject.groupType + RequireSenderAuthenticationEnabled = [bool]!$GroupObject.allowExternal + } + + # Add owners + if ($GroupObject.owners -and $GroupObject.owners.Count -gt 0) { + $OwnerEmails = $GroupObject.owners | ForEach-Object { + if ($_.value) { $_.value } elseif ($_ -is [string]) { $_ } + } | Where-Object { $_ } + + if ($OwnerEmails) { + $ExoParams.ManagedBy = @($OwnerEmails) + } + } + + # Add members + if ($GroupObject.members -and $GroupObject.members.Count -gt 0) { + $MemberEmails = $GroupObject.members | ForEach-Object { + if ($_.value) { $_.value } elseif ($_ -is [string]) { $_ } + } | Where-Object { $_ } + + if ($MemberEmails) { + $ExoParams.Members = @($MemberEmails) + } + } + + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DistributionGroup' -cmdParams $ExoParams + } + + $Result = [PSCustomObject]@{ + Success = $true + Message = "Successfully created group $($GroupObject.displayName)" + GroupId = $GraphRequest.Identity + GroupType = $NormalizedGroupType + Email = $Email + } + } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Created group $($GroupObject.displayName) with id $($Result.GroupId)" -Sev Info + return $Result + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Group creation failed for $($GroupObject.displayName): $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + + return [PSCustomObject]@{ + Success = $false + Message = "Failed to create group $($GroupObject.displayName): $($ErrorMessage.NormalizedError)" + Error = $ErrorMessage.NormalizedError + GroupType = $NormalizedGroupType + } + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 4a82d249bd69..038645609bc5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -31,100 +31,191 @@ function Invoke-CIPPStandardGroupTemplate { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GroupTemplate' $existingGroups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $tenant + + $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') -SkipLog + + $Settings.groupTemplate ? ($Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.groupTemplate) : $null + + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'GroupTemplate' and (RowKey eq '$($Settings.TemplateList.value -join "' or RowKey eq '")')" + $GroupTemplates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json + + if ('dynamicDistribution' -in $GroupTemplates.groupType) { + # Get dynamic distro list from exchange + $DynamicDistros = New-ExoRequest -cmdlet 'Get-DynamicDistributionGroup' -tenantid $tenant -Select 'Identity,Name,Alias,RecipientFilter,PrimarySmtpAddress' + } + if ($Settings.remediate -eq $true) { #Because the list name changed from TemplateList to groupTemplate by someone :@, we'll need to set it back to TemplateList - $Settings.groupTemplate ? ($Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.groupTemplate) : $null + Write-Host "Settings: $($Settings.TemplateList | ConvertTo-Json)" - foreach ($Template in $Settings.TemplateList) { + foreach ($Template in $GroupTemplates) { + Write-Information "Processing template: $($Template.displayName)" try { - $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'GroupTemplate' and RowKey eq '$($Template.value)'" - $groupobj = (Get-AzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json - $email = if ($groupobj.domain) { "$($groupobj.username)@$($groupobj.domain)" } else { "$($groupobj.username)@$($Tenant)" } - $CheckExististing = $existingGroups | Where-Object -Property displayName -EQ $groupobj.displayname - $BodyToship = [pscustomobject] @{ - 'displayName' = $groupobj.Displayname - 'description' = $groupobj.Description - 'mailNickname' = $groupobj.username - mailEnabled = [bool]$false - securityEnabled = [bool]$true - } - if ($groupobj.groupType -eq 'AzureRole') { - $BodyToship | Add-Member -NotePropertyName 'isAssignableToRole' -NotePropertyValue $true - } - if ($groupobj.membershipRules) { - $BodyToship | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue ($groupobj.membershipRules) - $BodyToship | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') - $BodyToship | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' + $groupobj = $Template + + if ($Template.groupType -eq 'dynamicDistribution') { + $CheckExisting = $DynamicDistros | Where-Object { $_.Name -eq $Template.displayName } + } else { + $CheckExisting = $existingGroups | Where-Object -Property displayName -EQ $groupobj.displayName } - if (!$CheckExististing) { + + if (!$CheckExisting) { + Write-Information 'Creating group' $ActionType = 'create' - if ($groupobj.groupType -in 'Generic', 'azurerole', 'dynamic', 'Security') { - $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenant -type POST -body (ConvertTo-Json -InputObject $BodyToship -Depth 10) -verbose - } else { - if (!$TestResult) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot create group $($groupobj.displayname) as the tenant is not licensed for Exchange." -Sev 'Error' - continue + + # Check if Exchange license is required for distribution groups + if ($groupobj.groupType -in @('distribution', 'dynamicdistribution') -and !$TestResult) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot create group $($groupobj.displayname) as the tenant is not licensed for Exchange." -Sev 'Error' + continue + } + + # Use the centralized New-CIPPGroup function + $Result = New-CIPPGroup -GroupObject $groupobj -TenantFilter $tenant -APIName 'Standards' -ExecutingUser 'CIPP-Standards' + + if (!$Result.Success) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create group $($groupobj.displayname): $($Result.Message)" -Sev 'Error' + continue + } + } else { + $ActionType = 'update' + + # Normalize group type like New-CIPPGroup does + $NormalizedGroupType = switch -Wildcard ($groupobj.groupType.ToLower()) { + '*dynamicdistribution*' { 'DynamicDistribution'; break } + '*dynamic*' { 'Dynamic'; break } + '*generic*' { 'Generic'; break } + '*security*' { 'Security'; break } + '*azurerole*' { 'AzureRole'; break } + '*m365*' { 'M365'; break } + '*unified*' { 'M365'; break } + '*microsoft*' { 'M365'; break } + '*distribution*' { 'Distribution'; break } + '*mail*' { 'Distribution'; break } + default { $groupobj.groupType } + } + + # Handle Graph API groups (Security, Generic, AzureRole, Dynamic, M365) + if ($NormalizedGroupType -in @('Generic', 'Security', 'AzureRole', 'Dynamic', 'M365')) { + + # Compare existing group with template to determine what needs updating + $PatchBody = [PSCustomObject]@{} + $ChangesNeeded = [System.Collections.Generic.List[string]]::new() + + # Check description + if ($CheckExisting.description -ne $groupobj.description) { + $PatchBody | Add-Member -NotePropertyName 'description' -NotePropertyValue $groupobj.description + $ChangesNeeded.Add("description: '$($CheckExisting.description)' → '$($groupobj.description)'") } - if ($groupobj.groupType -eq 'dynamicdistribution') { - $Params = @{ - Name = $groupobj.Displayname - RecipientFilter = $groupobj.membershipRules - PrimarySmtpAddress = $email + # Handle membership rules for dynamic groups + # Only update if the template specifies this should be a dynamic group + if ($NormalizedGroupType -eq 'Dynamic' -and $groupobj.membershipRules) { + if ($CheckExisting.membershipRule -ne $groupobj.membershipRules) { + $PatchBody | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue $groupobj.membershipRules + $PatchBody | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' + $ChangesNeeded.Add("membershipRule: '$($CheckExisting.membershipRule)' → '$($groupobj.membershipRules)'") } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DynamicDistributionGroup' -cmdParams $params + } - if (!$groupobj.AllowExternal) { - $SetParams = @{ - RequireSenderAuthenticationEnabled = [bool]!$groupobj.AllowExternal - Name = $groupobj.Displayname - PrimarySmtpAddress = $email - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $SetParams - } + # Only patch if there are actual changes + if ($ChangesNeeded.Count -gt 0) { + $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($CheckExisting.id)" -tenantid $tenant -type PATCH -body (ConvertTo-Json -InputObject $PatchBody -Depth 10) + Write-LogMessage -API 'Standards' -tenant $tenant -message "Updated Group '$($groupobj.displayName)' - Changes: $($ChangesNeeded -join ', ')" -Sev Info } else { - $Params = @{ - Name = $groupobj.Displayname - Alias = $groupobj.username - Description = $groupobj.Description - PrimarySmtpAddress = $email - Type = $groupobj.groupType - RequireSenderAuthenticationEnabled = [bool]!$groupobj.AllowExternal - } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'New-DistributionGroup' -cmdParams $params + Write-Information "Group '$($groupobj.displayName)' already matches template - no update needed" } - } - Write-LogMessage -API 'Standards' -tenant $tenant -message "Created group $($groupobj.displayname) with id $($GraphRequest.id) " -Sev 'Info' - } else { - $ActionType = 'update' - if ($groupobj.groupType -in 'Generic', 'azurerole', 'dynamic') { - $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($CheckExististing.id)" -tenantid $tenant -type PATCH -body (ConvertTo-Json -InputObject $BodyToship -Depth 10) -verbose + } else { + # Handle Exchange Online groups (Distribution, DynamicDistribution) if (!$TestResult) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot update group $($groupobj.displayname) as the tenant is not licensed for Exchange." -Sev 'Error' + Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot update group $($groupobj.displayName) as the tenant is not licensed for Exchange." -Sev 'Error' continue } - if ($groupobj.groupType -eq 'dynamicdistribution') { - $Params = @{ - Name = $groupobj.Displayname - RecipientFilter = $groupobj.membershipRules - PrimarySmtpAddress = $email + + # Construct email address if needed + $Email = if ($groupobj.username -like '*@*') { + $groupobj.username + } else { + "$($groupobj.username)@$($tenant)" + } + + $ExoChangesNeeded = [System.Collections.Generic.List[string]]::new() + + if ($NormalizedGroupType -eq 'DynamicDistribution') { + # Compare Dynamic Distribution Group properties + $SetParams = @{ + Identity = $CheckExisting.Identity + } + + # Check recipient filter change + if ($CheckExisting.RecipientFilter -notmatch $groupobj.membershipRules) { + $SetParams.RecipientFilter = $groupobj.membershipRules + $ExoChangesNeeded.Add("RecipientFilter: '$($CheckExisting.RecipientFilter)' → '$($groupobj.membershipRules)'") + } + + # Only update if there are changes + if ($SetParams.Count -gt 1) { + $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $SetParams + } + + # Check external sender restrictions + if ($null -ne $groupobj.allowExternal) { + $currentAuthRequired = $CheckExisting.RequireSenderAuthenticationEnabled + $templateAuthRequired = [bool]!$groupobj.allowExternal + + if ($currentAuthRequired -ne $templateAuthRequired) { + $ExtParams = @{ + Identity = $CheckExisting.displayName + RequireSenderAuthenticationEnabled = $templateAuthRequired + } + $null = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $ExtParams + $ExoChangesNeeded.Add("RequireSenderAuthenticationEnabled: '$currentAuthRequired' → '$templateAuthRequired'") + } } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $params + } else { - $Params = @{ - Identity = $groupobj.Displayname - Alias = $groupobj.username - Description = $groupobj.Description - PrimarySmtpAddress = $email - Type = $groupobj.groupType - RequireSenderAuthenticationEnabled = [bool]!$groupobj.AllowExternal + # Compare Regular Distribution Group properties + $SetParams = @{ + Identity = $CheckExisting.displayName + } + + # Check display name change + if ($CheckExisting.displayName -ne $groupobj.displayName) { + $SetParams.DisplayName = $groupobj.displayName + $ExoChangesNeeded.Add("DisplayName: '$($CheckExisting.displayName)' → '$($groupobj.displayName)'") + } + + # Check description change + if ($CheckExisting.description -ne $groupobj.description) { + $SetParams.Description = $groupobj.description + $ExoChangesNeeded.Add("Description: '$($CheckExisting.description)' → '$($groupobj.description)'") + } + + # Check external sender restrictions + if ($null -ne $groupobj.allowExternal) { + $currentAuthRequired = $CheckExisting.RequireSenderAuthenticationEnabled + $templateAuthRequired = [bool]!$groupobj.allowExternal + + if ($currentAuthRequired -ne $templateAuthRequired) { + $SetParams.RequireSenderAuthenticationEnabled = $templateAuthRequired + $ExoChangesNeeded.Add("RequireSenderAuthenticationEnabled: '$currentAuthRequired' → '$templateAuthRequired'") + } + } + + # Only update if there are changes + if ($SetParams.Count -gt 0) { + $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DistributionGroup' -cmdParams $SetParams } - $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DistributionGroup' -cmdParams $params + } + + # Log results + if ($ExoChangesNeeded.Count -gt 0) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Updated Exchange group '$($groupobj.displayName)' - Changes: $($ExoChangesNeeded -join ', ')" -Sev Info + } else { + Write-Information "Exchange group '$($groupobj.displayName)' already matches template - no update needed" } } - Write-LogMessage -API 'Standards' -tenant $tenant -message "Group exists $($groupobj.displayname). Updated to latest settings." -Sev 'Info' } } catch { @@ -134,12 +225,18 @@ function Invoke-CIPPStandardGroupTemplate { } } if ($Settings.report -eq $true) { - $Groups = $Settings.groupTemplate.JSON | ConvertFrom-Json -Depth 10 #check if all groups.displayName are in the existingGroups, if not $fieldvalue should contain all missing groups, else it should be true. - $MissingGroups = foreach ($Group in $Groups) { - $CheckExististing = $existingGroups | Where-Object -Property displayName -EQ $Group.displayname - if (!$CheckExististing) { - $Group.displayname + $MissingGroups = foreach ($Group in $GroupTemplates) { + if ($Group.groupType -eq 'dynamicDistribution') { + $CheckExisting = $DynamicDistros | Where-Object { $_.Name -eq $Group.displayName } + if (!$CheckExisting) { + $Group.displayName + } + } else { + $CheckExisting = $existingGroups | Where-Object { $_.displayName -eq $Group.displayName } + if (!$CheckExisting) { + $Group.displayName + } } } From 1de164fa9d283799ab14b070386c4cc096bed06d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 8 Sep 2025 18:32:34 -0400 Subject: [PATCH 59/68] fix double logging --- .../Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 038645609bc5..715482c0fc80 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -74,7 +74,7 @@ function Invoke-CIPPStandardGroupTemplate { $Result = New-CIPPGroup -GroupObject $groupobj -TenantFilter $tenant -APIName 'Standards' -ExecutingUser 'CIPP-Standards' if (!$Result.Success) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create group $($groupobj.displayname): $($Result.Message)" -Sev 'Error' + Write-Information "Failed to create group $($groupobj.displayname): $($Result.Message)" continue } } else { From a292ead212ba9388a1cb823dbb53be939e45d2a6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 10 Sep 2025 16:52:05 -0400 Subject: [PATCH 60/68] fix issue with activity function returning $true Only return $true when no output is detected --- Modules/CippEntrypoints/CippEntrypoints.psm1 | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index da0fd7d643c6..cd9bb3bb8ef2 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -133,9 +133,9 @@ function Receive-CippOrchestrationTrigger { } if (!$OrchestratorInput.Batch -or ($OrchestratorInput.Batch | Measure-Object).Count -eq 0) { - $Batch = (Invoke-ActivityFunction -FunctionName 'CIPPActivityFunction' -Input $OrchestratorInput.QueueFunction -ErrorAction Stop) + $Batch = @(Invoke-ActivityFunction -FunctionName 'CIPPActivityFunction' -Input $OrchestratorInput.QueueFunction -ErrorAction Stop) } else { - $Batch = $OrchestratorInput.Batch + $Batch = @($OrchestratorInput.Batch) } if (($Batch | Measure-Object).Count -gt 0) { @@ -179,6 +179,7 @@ function Receive-CippActivityTrigger { Write-Warning "Hey Boo, the activity function is running. Here's some info: $($Item | ConvertTo-Json -Depth 10 -Compress)" try { $Start = Get-Date + $Output = $null Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName if ($Item.QueueId) { @@ -202,7 +203,7 @@ function Receive-CippActivityTrigger { $FunctionName = 'Push-{0}' -f $Item.FunctionName try { Write-Warning "Activity starting Function: $FunctionName." - Invoke-Command -ScriptBlock { & $FunctionName -Item $Item } + $Output = Invoke-Command -ScriptBlock { & $FunctionName -Item $Item } Write-Warning "Activity completed Function: $FunctionName." if ($TaskStatus) { $QueueTask.Status = 'Completed' @@ -244,7 +245,13 @@ function Receive-CippActivityTrigger { $null = Set-CippQueueTask @QueueTask } } - return $true + + # Return the captured output if it exists and is not null, otherwise return $true + if ($null -ne $Output -and $Output -ne '') { + return $Output + } else { + return $true + } } function Receive-CIPPTimerTrigger { From a44cb7baeec0f052918ca9121d893156074864e6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 11 Sep 2025 00:29:42 +0200 Subject: [PATCH 61/68] Add package ability --- .../CIPP/Core/Invoke-ExecSetPackageTag.ps1 | 48 +++++++++++++++++++ .../MEM/Invoke-ListIntuneTemplates.ps1 | 3 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 new file mode 100644 index 000000000000..c55f0fbeef68 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 @@ -0,0 +1,48 @@ +using namespace System.Net + +function Invoke-ExecSetPackageTag { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.Core.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $Table = Get-CippTable -tablename 'templates' + + try { + $GUIDS = $Request.body.GUID + $PackageName = $Request.body.Package | Select-Object -First 1 + foreach ($GUID in $GUIDS) { + $Filter = "RowKey eq '$GUID'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = $Template.JSON + RowKey = "$GUID" + PartitionKey = $Template.PartitionKey + GUID = "$GUID" + Package = "$PackageName" + } -Force + + Write-LogMessage -headers $Headers -API $APIName -message "Successfully updated template with GUID $GUID with package tag: $PackageName" -Sev 'Info' + } + + $body = [pscustomobject]@{ 'Results' = "Successfully updated template(s) with package tag: $PackageName" } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Failed to set package tag: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed to set package tag: $($ErrorMessage.NormalizedError)" } + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 index 518bc40a7ac8..e49cf6b8e167 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ListIntuneTemplates { +function Invoke-ListIntuneTemplates { <# .FUNCTIONALITY Entrypoint,AnyTenant @@ -45,6 +45,7 @@ Function Invoke-ListIntuneTemplates { $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data | Add-Member -NotePropertyName 'package' -NotePropertyValue $_.Package -Force $data } catch { From d2c83578a175e6b5ddec84f321945fcd64d72a28 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 10 Sep 2025 19:39:03 -0400 Subject: [PATCH 62/68] Update CippEntrypoints.psm1 --- Modules/CippEntrypoints/CippEntrypoints.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index cd9bb3bb8ef2..2166fd1d1534 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -133,9 +133,9 @@ function Receive-CippOrchestrationTrigger { } if (!$OrchestratorInput.Batch -or ($OrchestratorInput.Batch | Measure-Object).Count -eq 0) { - $Batch = @(Invoke-ActivityFunction -FunctionName 'CIPPActivityFunction' -Input $OrchestratorInput.QueueFunction -ErrorAction Stop) + $Batch = (Invoke-ActivityFunction -FunctionName 'CIPPActivityFunction' -Input $OrchestratorInput.QueueFunction -ErrorAction Stop) } else { - $Batch = @($OrchestratorInput.Batch) + $Batch = $OrchestratorInput.Batch } if (($Batch | Measure-Object).Count -gt 0) { From 9a61b67a340ea9a630a20f38ba2b0bf73abeb7db Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 11 Sep 2025 23:05:22 -0400 Subject: [PATCH 63/68] fix issue with state changes on CA deployment ticket 27684272542 --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 90 ++++++++++---------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 0ec869f6e596..dda1e9829d22 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -88,17 +88,17 @@ function New-CIPPCAPolicy { $displayname = ($RawJSON | ConvertFrom-Json).Displayname - $JSONObj = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty ID, GUID, *time* - Remove-EmptyArrays $JSONObj + $JSONobj = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty ID, GUID, *time* + Remove-EmptyArrays $JSONobj #Remove context as it does not belong in the payload. try { - $JsonObj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') - $JSONObj.templateId ? $JSONObj.PSObject.Properties.Remove('templateId') : $null - if ($JSONObj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) { - $JsonObj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.PSObject.Properties.Remove('@odata.context') + $JSONobj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') + $JSONobj.templateId ? $JSONobj.PSObject.Properties.Remove('templateId') : $null + if ($JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) { + $JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.PSObject.Properties.Remove('@odata.context') } if ($State -and $State -ne 'donotchange') { - $Jsonobj.state = $State + $JSONobj.state = $State } } catch { # no issues here. @@ -108,18 +108,18 @@ function New-CIPPCAPolicy { if ($JSONobj.GrantControls.authenticationStrength.policyType -eq 'custom' -or $JSONobj.GrantControls.authenticationStrength.policyType -eq 'BuiltIn') { $ExistingStrength = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies/' -tenantid $TenantFilter -asApp $true | Where-Object -Property displayName -EQ $JSONobj.GrantControls.authenticationStrength.displayName if ($ExistingStrength) { - $JSONObj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } + $JSONobj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } } else { - $Body = ConvertTo-Json -InputObject $JSONObj.GrantControls.authenticationStrength + $Body = ConvertTo-Json -InputObject $JSONobj.GrantControls.authenticationStrength $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -body $body -Type POST -tenantid $tenantfilter -asApp $true - $JSONObj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } - Write-LogMessage -Headers $User -API $APINAME -message "Created new Authentication Strength Policy: $($JSONObj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' + $JSONobj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } + Write-LogMessage -Headers $User -API $APINAME -message "Created new Authentication Strength Policy: $($JSONobj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' } } - #for each of the locations, check if they exist, if not create them. These are in $jsonobj.LocationInfo - $LocationLookupTable = foreach ($locations in $jsonobj.LocationInfo) { + #for each of the locations, check if they exist, if not create them. These are in $JSONobj.LocationInfo + $LocationLookupTable = foreach ($locations in $JSONobj.LocationInfo) { if (!$locations) { continue } foreach ($location in $locations) { if (!$location.displayName) { continue } @@ -152,20 +152,20 @@ function New-CIPPCAPolicy { } } - foreach ($location in $JSONObj.conditions.locations.includeLocations) { + foreach ($location in $JSONobj.conditions.locations.includeLocations) { Write-Information "Replacing named location - $location" $lookup = $LocationLookupTable | Where-Object -Property name -EQ $location Write-Information "Found $lookup" if (!$lookup) { continue } - $index = [array]::IndexOf($JSONObj.conditions.locations.includeLocations, $location) - $JSONObj.conditions.locations.includeLocations[$index] = $lookup.id + $index = [array]::IndexOf($JSONobj.conditions.locations.includeLocations, $location) + $JSONobj.conditions.locations.includeLocations[$index] = $lookup.id } - foreach ($location in $JSONObj.conditions.locations.excludeLocations) { + foreach ($location in $JSONobj.conditions.locations.excludeLocations) { $lookup = $LocationLookupTable | Where-Object -Property name -EQ $location if (!$lookup) { continue } - $index = [array]::IndexOf($JSONObj.conditions.locations.excludeLocations, $location) - $JSONObj.conditions.locations.excludeLocations[$index] = $lookup.id + $index = [array]::IndexOf($JSONobj.conditions.locations.excludeLocations, $location) + $JSONobj.conditions.locations.excludeLocations[$index] = $lookup.id } switch ($ReplacePattern) { 'none' { @@ -174,10 +174,10 @@ function New-CIPPCAPolicy { } 'AllUsers' { Write-Information 'Replacement pattern for inclusions and exclusions is All users. This policy will now apply to everyone.' - if ($JSONObj.conditions.users.includeUsers -ne 'All') { $JSONObj.conditions.users.includeUsers = @('All') } - if ($JSONObj.conditions.users.excludeUsers) { $JSONObj.conditions.users.excludeUsers = @() } - if ($JSONObj.conditions.users.includeGroups) { $JSONObj.conditions.users.includeGroups = @() } - if ($JSONObj.conditions.users.excludeGroups) { $JSONObj.conditions.users.excludeGroups = @() } + if ($JSONobj.conditions.users.includeUsers -ne 'All') { $JSONobj.conditions.users.includeUsers = @('All') } + if ($JSONobj.conditions.users.excludeUsers) { $JSONobj.conditions.users.excludeUsers = @() } + if ($JSONobj.conditions.users.includeGroups) { $JSONobj.conditions.users.includeGroups = @() } + if ($JSONobj.conditions.users.excludeGroups) { $JSONobj.conditions.users.excludeGroups = @() } } 'displayName' { try { @@ -186,41 +186,41 @@ function New-CIPPCAPolicy { $groups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName' -tenantid $TenantFilter -asApp $true foreach ($userType in 'includeUsers', 'excludeUsers') { - if ($JSONObj.conditions.users.PSObject.Properties.Name -contains $userType -and $JSONObj.conditions.users.$userType -notin 'All', 'None', 'GuestOrExternalUsers') { - $JSONObj.conditions.users.$userType = @(Replace-UserNameWithId -userNames $JSONObj.conditions.users.$userType) + if ($JSONobj.conditions.users.PSObject.Properties.Name -contains $userType -and $JSONobj.conditions.users.$userType -notin 'All', 'None', 'GuestOrExternalUsers') { + $JSONobj.conditions.users.$userType = @(Replace-UserNameWithId -userNames $JSONobj.conditions.users.$userType) } } # Check the included and excluded groups foreach ($groupType in 'includeGroups', 'excludeGroups') { - if ($JSONObj.conditions.users.PSObject.Properties.Name -contains $groupType) { - $JSONObj.conditions.users.$groupType = @(Replace-GroupNameWithId -groupNames $JSONObj.conditions.users.$groupType) + if ($JSONobj.conditions.users.PSObject.Properties.Name -contains $groupType) { + $JSONobj.conditions.users.$groupType = @(Replace-GroupNameWithId -groupNames $JSONobj.conditions.users.$groupType) } } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to replace displayNames for conditional access rule $($JSONObj.displayName). Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage - throw "Failed to replace displayNames for conditional access rule $($JSONObj.displayName): $($ErrorMessage.NormalizedError)" + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to replace displayNames for conditional access rule $($JSONobj.displayName). Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage + throw "Failed to replace displayNames for conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" } } } - $JsonObj.PSObject.Properties.Remove('LocationInfo') - foreach ($condition in $JSONObj.conditions.users.PSObject.Properties.Name) { - $value = $JSONObj.conditions.users.$condition + $JSONobj.PSObject.Properties.Remove('LocationInfo') + foreach ($condition in $JSONobj.conditions.users.PSObject.Properties.Name) { + $value = $JSONobj.conditions.users.$condition if ($null -eq $value) { - $JSONObj.conditions.users.$condition = @() + $JSONobj.conditions.users.$condition = @() continue } if ($value -is [string]) { if ([string]::IsNullOrWhiteSpace($value)) { - $JSONObj.conditions.users.$condition = @() + $JSONobj.conditions.users.$condition = @() continue } } if ($value -is [array]) { $nonWhitespaceItems = $value | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } if ($nonWhitespaceItems.Count -eq 0) { - $JSONObj.conditions.users.$condition = @() + $JSONobj.conditions.users.$condition = @() continue } } @@ -237,7 +237,7 @@ function New-CIPPCAPolicy { Write-Information "Failed to disable security defaults for tenant $($TenantFilter): $($ErrorMessage.NormalizedError)" } } - $RawJSON = ConvertTo-Json -InputObject $JSONObj -Depth 10 -Compress + $RawJSON = ConvertTo-Json -InputObject $JSONobj -Depth 10 -Compress Write-Information $RawJSON try { Write-Information 'Checking for existing policies' @@ -247,27 +247,31 @@ function New-CIPPCAPolicy { throw "Conditional Access Policy with Display Name $($Displayname) Already exists" return $false } else { + if ($State -eq 'donotchange') { + $JSONobj.state = $CheckExististing.state + $RawJSON = ConvertTo-Json -InputObject $JSONobj -Depth 10 -Compress + } Write-Information "overwriting $($CheckExististing.id)" $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExististing.id)" -tenantid $tenantfilter -type PATCH -body $RawJSON -asApp $true - Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Updated Conditional Access Policy $($JSONObj.Displayname) to the template standard." -Sev 'Info' + Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Updated Conditional Access Policy $($JSONobj.Displayname) to the template standard." -Sev 'Info' return "Updated policy $displayname for $tenantfilter" } } else { Write-Information 'Creating new policy' - if ($JSONobj.GrantControls.authenticationStrength.policyType -or $JSONObj.$jsonobj.LocationInfo) { + if ($JSOObj.GrantControls.authenticationStrength.policyType -or $JSONobj.$JSONobj.LocationInfo) { Start-Sleep 3 } $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter -type POST -body $RawJSON -asApp $true - Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONObj.Displayname)" -Sev 'Info' + Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONobj.Displayname)" -Sev 'Info' return "Created policy $displayname for $tenantfilter" } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update conditional access rule $($JSONObj.displayName): $($ErrorMessage.NormalizedError) " -sev 'Error' -LogData $ErrorMessage + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError) " -sev 'Error' -LogData $ErrorMessage - Write-Warning "Failed to create or update conditional access rule $($JSONObj.displayName): $($ErrorMessage.NormalizedError)" + Write-Warning "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" Write-Information $_.InvocationInfo.PositionMessage - Write-Information ($JSONObj | ConvertTo-Json -Depth 10) - throw "Failed to create or update conditional access rule $($JSONObj.displayName): $($ErrorMessage.NormalizedError)" + Write-Information ($JSONobj | ConvertTo-Json -Depth 10) + throw "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" } } From cba5f92813f284accd2ea26ebfce2d11b90f8582 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:40:27 +0200 Subject: [PATCH 64/68] add tag support --- .../MEM/Invoke-ListIntuneTemplates.ps1 | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 index e49cf6b8e167..06afc34a1ca1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 @@ -53,7 +53,35 @@ function Invoke-ListIntuneTemplates { } | Sort-Object -Property displayName } else { - $Templates = $RawTemplates.JSON | ForEach-Object { try { ConvertFrom-Json -InputObject $_ -Depth 100 -ErrorAction SilentlyContinue } catch {} } + if ($Request.query.mode -eq 'Tag') { + #when the mode is tag, show all the potential tags, return the object with: label: tag, value: tag, count: number of templates with that tag, unique only + $Templates = $RawTemplates | Where-Object { $_.Package } | Select-Object -Property Package | ForEach-Object { + $package = $_.Package + [pscustomobject]@{ + label = "$($package) ($(($RawTemplates | Where-Object { $_.Package -eq $package }).Count) Templates)" + value = $package + type = 'tag' + templateCount = ($RawTemplates | Where-Object { $_.Package -eq $package }).Count + templates = ($RawTemplates | Where-Object { $_.Package -eq $package } | ForEach-Object { + try { + $JSONData = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force + $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force + $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data | Add-Member -NotePropertyName 'package' -NotePropertyValue $_.Package -Force + $data + } catch { + + } + }) + } + } | Sort-Object -Property label -Unique + } else { + $Templates = $RawTemplates.JSON | ForEach-Object { try { ConvertFrom-Json -InputObject $_ -Depth 100 -ErrorAction SilentlyContinue } catch {} } + + } } if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property guid -EQ $Request.query.id } From cf3a355c4a409172a90a941a602b2ad6269ddfd5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 12 Sep 2025 09:55:31 -0400 Subject: [PATCH 65/68] blocked endpoint support --- .../Get-CIPPRolePermissions.ps1 | 10 ++++--- .../Public/Authentication/Test-CIPPAccess.ps1 | 4 +++ .../CIPP/Settings/Invoke-ExecCustomRole.ps1 | 29 +++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1 index 8bac3674e677..4b89c560b759 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1 @@ -20,11 +20,13 @@ function Get-CIPPRolePermissions { $Permissions = $Role.Permissions | ConvertFrom-Json $AllowedTenants = if ($Role.AllowedTenants) { $Role.AllowedTenants | ConvertFrom-Json } else { @() } $BlockedTenants = if ($Role.BlockedTenants) { $Role.BlockedTenants | ConvertFrom-Json } else { @() } + $BlockedEndpoints = if ($Role.BlockedEndpoints) { $Role.BlockedEndpoints | ConvertFrom-Json } else { @() } [PSCustomObject]@{ - Role = $Role.RowKey - Permissions = $Permissions.PSObject.Properties.Value - AllowedTenants = @($AllowedTenants) - BlockedTenants = @($BlockedTenants) + Role = $Role.RowKey + Permissions = $Permissions.PSObject.Properties.Value + AllowedTenants = @($AllowedTenants) + BlockedTenants = @($BlockedTenants) + BlockedEndpoints = @($BlockedEndpoints) } } else { throw "Role $RoleName not found." diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 43b3de6e8141..71d78fae0ac5 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -199,6 +199,7 @@ function Test-CIPPAccess { continue } } + if ($PermissionsFound) { if ($TenantList.IsPresent) { $LimitedTenantList = foreach ($Permission in $PermissionSet) { @@ -248,6 +249,9 @@ function Test-CIPPAccess { foreach ($Role in $PermissionSet) { foreach ($Perm in $Role.Permissions) { if ($Perm -match $APIRole) { + if ($Role.BlockedEndpoints -contains $Request.Params.CIPPEndpoint) { + throw "Access to this CIPP API endpoint is not allowed, the custom role '$($Role.Role)' has blocked this endpoint: $($Request.Params.CIPPEndpoint)" + } $APIAllowed = $true break } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 index 738aacfd7183..e95de28bbc65 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 @@ -26,11 +26,12 @@ function Invoke-ExecCustomRole { Write-LogMessage -headers $Request.Headers -API 'ExecCustomRole' -message "Saved custom role $($Request.Body.RoleName)" -Sev 'Info' if ($Request.Body.RoleName -notin $DefaultRoles) { $Role = @{ - 'PartitionKey' = 'CustomRoles' - 'RowKey' = "$($Request.Body.RoleName.ToLower())" - 'Permissions' = "$($Request.Body.Permissions | ConvertTo-Json -Compress)" - 'AllowedTenants' = "$($Request.Body.AllowedTenants | ConvertTo-Json -Compress)" - 'BlockedTenants' = "$($Request.Body.BlockedTenants | ConvertTo-Json -Compress)" + 'PartitionKey' = 'CustomRoles' + 'RowKey' = "$($Request.Body.RoleName.ToLower())" + 'Permissions' = "$($Request.Body.Permissions | ConvertTo-Json -Compress)" + 'AllowedTenants' = "$($Request.Body.AllowedTenants | ConvertTo-Json -Compress)" + 'BlockedTenants' = "$($Request.Body.BlockedTenants | ConvertTo-Json -Compress)" + 'BlockedEndpoints' = "$($Request.Body.BlockedEndpoints | ConvertTo-Json -Compress)" } Add-CIPPAzDataTableEntity @Table -Entity $Role -Force | Out-Null $Results.Add("Custom role $($Request.Body.RoleName) saved") @@ -110,6 +111,15 @@ function Invoke-ExecCustomRole { } else { $Role | Add-Member -NotePropertyName BlockedTenants -NotePropertyValue @() -Force } + if ($Role.BlockedEndpoints) { + try { + $Role.BlockedEndpoints = @($Role.BlockedEndpoints | ConvertFrom-Json) + } catch { + $Role.BlockedEndpoints = '' + } + } else { + $Role | Add-Member -NotePropertyName BlockedEndpoints -NotePropertyValue @() -Force + } $EntraRoleGroup = $EntraRoleGroups | Where-Object -Property RowKey -EQ $Role.RowKey if ($EntraRoleGroup) { $EntraGroup = $EntraRoleGroups | Where-Object -Property RowKey -EQ $Role.RowKey | Select-Object @{Name = 'label'; Expression = { $_.GroupName } }, @{Name = 'value'; Expression = { $_.GroupId } } @@ -120,10 +130,11 @@ function Invoke-ExecCustomRole { } $DefaultRoles = foreach ($DefaultRole in $DefaultRoles) { $Role = @{ - RowKey = $DefaultRole - Permissions = '' - AllowedTenants = @('AllTenants') - BlockedTenants = @('') + RowKey = $DefaultRole + Permissions = '' + AllowedTenants = @('AllTenants') + BlockedTenants = @('') + BlockedEndpoints = @('') } $EntraRoleGroup = $EntraRoleGroups | Where-Object -Property RowKey -EQ $Role.RowKey if ($EntraRoleGroup) { From de9921175a037456f3affc3d477aeb7c379416c2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:21:53 +0200 Subject: [PATCH 66/68] new tags --- .../Functions/Get-CIPPTenantAlignment.ps1 | 18 ++++- .../Public/Standards/Get-CIPPStandards.ps1 | 66 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index b5c549f80db9..364e6c9bdf85 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -158,6 +158,22 @@ function Get-CIPPTenantAlignment { ReportingEnabled = $IntuneReportingEnabled } } + if ($IntuneTemplate.'TemplateList-Tags') { + foreach ($Tag in $IntuneTemplate.'TemplateList-Tags') { + Write-Host "Processing Intune Tag: $($Tag.value)" + $IntuneActions = if ($IntuneTemplate.action) { $IntuneTemplate.action } else { @() } + $IntuneReportingEnabled = ($IntuneActions | Where-Object { $_.value -and ($_.value.ToLower() -eq 'report' -or $_.value.ToLower() -eq 'remediate') }).Count -gt 0 + $TemplatesList = Get-CIPPAzDataTableEntity @TemplateTable -Filter $Filter | Where-Object -Property package -EQ $Tag.value + $TemplatesList | ForEach-Object { + $TagStandardId = "standards.IntuneTemplate.$($_.GUID)" + [PSCustomObject]@{ + StandardId = $TagStandardId + ReportingEnabled = $IntuneReportingEnabled + } + } + + } + } } } # Handle Conditional Access templates specially @@ -224,7 +240,7 @@ function Get-CIPPTenantAlignment { [PSCustomObject]@{ StandardName = $StandardKey Compliant = $IsCompliant - StandardValue = ($Value | ConvertTo-Json -Compress) + StandardValue = ($Value | ConvertTo-Json -Depth 100 -Compress) ComplianceStatus = $ComplianceStatus ReportingDisabled = $IsReportingDisabled } diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index b639cf088d80..9ad77e61923d 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -31,6 +31,72 @@ function Get-CIPPStandards { $_.GUID -like $TemplateId -and $_.runManually -eq $runManually } + # 1.5. Expand templates that contain TemplateList-Tags into multiple standards + $ExpandedTemplates = foreach ($Template in $Templates) { + $NewTemplate = $Template.PSObject.Copy() + $ExpandedStandards = [ordered]@{} + $HasExpansions = $false + + foreach ($StandardName in $Template.standards.PSObject.Properties.Name) { + $StandardValue = $Template.standards.$StandardName + $IsArray = $StandardValue -is [System.Collections.IEnumerable] -and -not ($StandardValue -is [string]) + + if ($IsArray) { + $NewArray = @() + foreach ($Item in $StandardValue) { + if ($Item.'TemplateList-Tags'.value) { + $HasExpansions = $true + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'IntuneTemplate'" + $TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $Item.'TemplateList-Tags'.value + + foreach ($TemplateItem in $TemplatesList) { + $NewItem = $Item.PSObject.Copy() + $NewItem.PSObject.Properties.Remove('TemplateList-Tags') + $NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{ + label = "$($TemplateItem.RowKey)" + value = "$($TemplateItem.RowKey)" + }) -Force + $NewArray = $NewArray + $NewItem + } + } else { + $NewArray = $NewArray + $Item + } + } + $ExpandedStandards[$StandardName] = $NewArray + } else { + if ($StandardValue.'TemplateList-Tags'.value) { + $HasExpansions = $true + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'IntuneTemplate'" + $TemplatesList = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property package -EQ $StandardValue.'TemplateList-Tags'.value + + $NewArray = @() + foreach ($TemplateItem in $TemplatesList) { + $NewItem = $StandardValue.PSObject.Copy() + $NewItem.PSObject.Properties.Remove('TemplateList-Tags') + $NewItem | Add-Member -NotePropertyName TemplateList -NotePropertyValue ([pscustomobject]@{ + label = "$($TemplateItem.RowKey)" + value = "$($TemplateItem.RowKey)" + }) -Force + $NewArray = $NewArray + $NewItem + } + $ExpandedStandards[$StandardName] = $NewArray + } else { + $ExpandedStandards[$StandardName] = $StandardValue + } + } + } + + if ($HasExpansions) { + $NewTemplate.standards = [pscustomobject]$ExpandedStandards + } + + $NewTemplate + } + + $Templates = $ExpandedTemplates + # 2. Get tenant list, filter if needed $AllTenantsList = Get-Tenants if ($TenantFilter -ne 'allTenants') { From d007fa2c1a18468b2c756b59be7ca1c50269f692 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:49:25 +0200 Subject: [PATCH 67/68] up version --- version_latest.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_latest.txt b/version_latest.txt index 9c78b761ea12..a2f28f43be33 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -8.3.2 +8.4.0 From c82ca675464b53db657437b43e4a356fb27e980a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 12 Sep 2025 11:07:36 -0400 Subject: [PATCH 68/68] fix combined setup --- .../HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 index 3fa7ac8df368..318a311cbc79 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 @@ -85,6 +85,7 @@ function Invoke-ExecCombinedSetup { if ($Request.Body.tenantId) { $Secret.TenantId = $Request.Body.tenantid } if ($Request.Body.applicationId) { $Secret.ApplicationId = $Request.Body.applicationId } if ($Request.Body.ApplicationSecret) { $Secret.ApplicationSecret = $Request.Body.ApplicationSecret } + if ($Request.Body.RefreshToken) { $Secret.RefreshToken = $Request.Body.RefreshToken } Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force $Results.add('Manual credentials have been set in the DevSecrets table.') } else { @@ -100,6 +101,10 @@ function Invoke-ExecCombinedSetup { Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -SecretValue (ConvertTo-SecureString -String $Request.Body.applicationSecret -AsPlainText -Force) $Results.add('Set application secret in Key Vault.') } + if ($Request.Body.RefreshToken) { + Set-AzKeyVaultSecret -VaultName $kv -Name 'refreshtoken' -SecretValue (ConvertTo-SecureString -String $Request.Body.RefreshToken -AsPlainText -Force) + $Results.add('Set refresh token in Key Vault.') + } } $Results.add('Manual credentials setup has been completed.')