')
$IntroText = "You've configured CIPP to send you alerts based on the logbook. The following alerts match your configured rules
$dataHTML"
+
+ # Add alert comment if provided
+ if ($AlertComment) {
+ $IntroText = "$IntroTextAlert Information
$AlertComment
"
+ }
+
$ButtonUrl = "$CIPPURL/cipp/logs"
$ButtonText = 'Check logbook information'
}
@@ -280,10 +287,11 @@ function New-CIPPAlertTemplate {
}
}
return [pscustomobject]@{
- title = $Title
- buttonurl = $ButtonUrl
- buttontext = $ButtonText
- auditlog = $AuditLogLink
+ title = $Title
+ buttonurl = $ButtonUrl
+ buttontext = $ButtonText
+ auditlog = $AuditLogLink
+ alertcomment = $AlertComment
}
}
}
diff --git a/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 b/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1
index d8b09e412d33..60c83b040d93 100644
--- a/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1
+++ b/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1
@@ -41,7 +41,7 @@ function New-CIPPTemplateRun {
}
foreach ($File in $Files) {
if ($File.name -eq 'MigrationTable' -or $file.name -eq 'ALLOWED COUNTRIES') { continue }
- $ExistingTemplate = $ExistingTemplates | Where-Object { (![string]::IsNullOrEmpty($_.displayName) -and (Get-SanitizedFilename -filename $_.displayName) -eq $File.name) -or (![string]::IsNullOrEmpty($_.templateName) -and (Get-SanitizedFilename -filename $_.templateName) -eq $File.name ) } | Select-Object -First 1
+ $ExistingTemplate = $ExistingTemplates | Where-Object { (![string]::IsNullOrEmpty($_.displayName) -and (Get-SanitizedFilename -filename $_.displayName) -eq $File.name) -or (![string]::IsNullOrEmpty($_.templateName) -and (Get-SanitizedFilename -filename $_.templateName) -eq $File.name ) -and ![string]::IsNullOrEmpty($_.SHA) } | Select-Object -First 1
$UpdateNeeded = $false
if ($ExistingTemplate -and $ExistingTemplate.SHA -ne $File.sha) {
diff --git a/Modules/CIPPCore/Public/PermissionsTranslator.json b/Modules/CIPPCore/Public/PermissionsTranslator.json
index d4abc5200b3d..52f29c119433 100644
--- a/Modules/CIPPCore/Public/PermissionsTranslator.json
+++ b/Modules/CIPPCore/Public/PermissionsTranslator.json
@@ -5328,12 +5328,12 @@
"value": "AllSites.FullControl"
},
{
- "description": "Allows to read the LAPs passwords.",
- "displayName": "Manage LAPs passwords",
+ "description": "Allows to read the LAPS passwords.",
+ "displayName": "Manage LAPS passwords",
"id": "280b3b69-0437-44b1-bc20-3b2fca1ee3e9",
"Origin": "Delegated",
- "userConsentDescription": "Allows to read the LAPs passwords.",
- "userConsentDisplayName": "Manage LAPs passwords",
+ "userConsentDescription": "Allows to read the LAPS passwords.",
+ "userConsentDisplayName": "Manage LAPS passwords",
"value": "DeviceLocalCredential.Read.All"
},
{
diff --git a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1
index 2c972715f672..7606d4d0606e 100644
--- a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1
+++ b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1
@@ -25,7 +25,19 @@ function Send-CIPPAlert {
$Recipients = if ($AltEmail) {
[pscustomobject]@{EmailAddress = @{Address = $AltEmail } }
} else {
- $Config.email.split($(if ($Config.email -like '*,*') { ',' } else { ';' })).trim() | ForEach-Object { if ($_ -like '*@*') { [pscustomobject]@{EmailAddress = @{Address = $_ } } } }
+ $Config.email.split($(if ($Config.email -like '*,*') { ',' } else { ';' })).trim() | ForEach-Object {
+ if ($_ -like '*@*') {
+ ($Alias, $Domain) = $_ -split '@'
+ if ($Alias -match '%') {
+ # Allow for text replacement in alias portion of email address
+ $Alias = Get-CIPPTextReplacement -Text $Alias -Tenant $TenantFilter
+ $Recipient = "$Alias@$Domain"
+ } else {
+ $Recipient = $_
+ }
+ [pscustomobject]@{EmailAddress = @{Address = $Recipient } }
+ }
+ }
}
$PowerShellBody = [PSCustomObject]@{
message = @{
@@ -43,9 +55,14 @@ function Send-CIPPAlert {
if ($PSCmdlet.ShouldProcess($($Recipients.EmailAddress.Address -join ', '), 'Sending email')) {
$null = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/me/sendMail' -tenantid $env:TenantID -NoAuthCheck $true -type POST -body ($JSONBody)
}
+
+ $LogData = @{
+ Recipients = $Recipients
+ }
+ Write-LogMessage -API 'Webhook Alerts' -message "Sent an email alert: $Title" -tenant $TenantFilter -sev info -LogData $LogData
+ return "Sent an email alert: $Title"
}
- Write-LogMessage -API 'Webhook Alerts' -message "Sent an email alert: $Title" -tenant $TenantFilter -sev info
- return "Sent an email alert: $Title"
+
} catch {
$ErrorMessage = Get-CippException -Exception $_
Write-Information "Could not send webhook alert to email: $($ErrorMessage.NormalizedError)"
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1
index c64d3eb1b2f3..a59431315bf2 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1
@@ -59,7 +59,7 @@ function Invoke-CIPPStandardAutopilotProfile {
if ($Settings.NotLocalAdmin -eq $true) { $userType = 'Standard' } else { $userType = 'Administrator' }
if ($Settings.SelfDeployingMode -eq $true) {
$DeploymentMode = 'shared'
- $Setings.AllowWhiteGlove = $false
+ $Settings.AllowWhiteGlove = $false
} else {
$DeploymentMode = 'singleUser'
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1
new file mode 100644
index 000000000000..1588392ebbca
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1
@@ -0,0 +1,101 @@
+function Invoke-CIPPStandardBitLockerKeysForOwnedDevice {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) BitLockerKeysForOwnedDevice
+ .SYNOPSIS
+ (Label) Restrict users from recovering BitLocker keys for owned devices
+ .DESCRIPTION
+ (Helptext) Controls whether standard users can recover BitLocker keys for devices they own via Microsoft 365 portals.
+ (DocsDescription) Updates the default user role setting that governs access to BitLocker recovery keys for owned devices. This allows administrators to either permit self-service recovery or require helpdesk involvement through Microsoft Entra authorization policies.
+ .NOTES
+ CAT
+ Entra (AAD) Standards
+ TAG
+ "NIST CSF 2.0 (PR.AA-05)"
+ EXECUTIVETEXT
+ Ensures administrators retain control over BitLocker recovery secrets when required, while still allowing flexibility to enable self-service recovery when business needs demand it.
+ ADDEDCOMPONENT
+ {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select state","name":"standards.BitLockerKeysForOwnedDevice.state","options":[{"label":"Restrict","value":"restrict"},{"label":"Allow","value":"allow"}]}
+ IMPACT
+ Medium Impact
+ ADDEDDATE
+ 2025-10-12
+ POWERSHELLEQUIVALENT
+ Update-MgBetaPolicyAuthorizationPolicy
+ RECOMMENDEDBY
+ "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
+ #>
+
+ param($Tenant, $Settings)
+ ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'BitLockerKeysForOwnedDevice'
+
+ $StateValue = $Settings.state.value ?? $Settings.state
+ if ([string]::IsNullOrWhiteSpace($StateValue)) {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'BitLockerKeysForOwnedDevice: Invalid state parameter set.' -sev Error
+ return
+ }
+
+ switch ($StateValue.ToLowerInvariant()) {
+ 'restrict' { $DesiredValue = $false; $DesiredLabel = 'restricted'; break }
+ 'allow' { $DesiredValue = $true; $DesiredLabel = 'allowed'; break }
+ default {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "BitLockerKeysForOwnedDevice: Unsupported state value '$StateValue'." -sev Error
+ return
+ }
+ }
+
+ try {
+ $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the BitLockerKeysForOwnedDevice state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ return
+ }
+ $CurrentValue = [bool]$CurrentState.defaultUserRolePermissions.allowedToReadBitLockerKeysForOwnedDevice
+ $StateIsCorrect = ($CurrentValue -eq $DesiredValue)
+
+ if ($Settings.remediate -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Users are already $DesiredLabel from recovering BitLocker keys for their owned devices." -sev Info
+ } else {
+ try {
+ $BodyObject = @{ defaultUserRolePermissions = @{ allowedToReadBitLockerKeysForOwnedDevice = $DesiredValue } }
+ $BodyJson = $BodyObject | ConvertTo-Json -Depth 4 -Compress
+ $null = New-GraphPOSTRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -Type patch -Body $BodyJson
+ $ActionMessage = if ($DesiredValue) { 'Allowed users to recover BitLocker keys for their owned devices.' } else { 'Restricted users from recovering BitLocker keys for their owned devices.' }
+ Write-LogMessage -API 'Standards' -tenant $tenant -message $ActionMessage -sev Info
+
+
+ # Update current state variables to reflect the change immediately if running remediate and report/alert together
+ $CurrentState.defaultUserRolePermissions.allowedToReadBitLockerKeysForOwnedDevice = $DesiredValue
+ $CurrentValue = $DesiredValue
+ $StateIsCorrect = $true
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to $StateValue users to recover BitLocker keys for their owned devices: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ }
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Users are $DesiredLabel to recover BitLocker keys for their owned devices as configured." -sev Info
+ } else {
+ $CurrentLabel = if ($CurrentValue) { 'allowed' } else { 'restricted' }
+ $AlertMessage = "Users are $CurrentLabel to recover BitLocker keys for their owned devices but should be $DesiredLabel."
+ Write-StandardsAlert -message $AlertMessage -object $CurrentState -tenant $tenant -standardName 'BitLockerKeysForOwnedDevice' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ Set-CIPPStandardsCompareField -FieldName 'standards.BitLockerKeysForOwnedDevice' -FieldValue $StateIsCorrect -Tenant $tenant
+ Add-CIPPBPAField -FieldName 'BitLockerKeysForOwnedDevice' -FieldValue $CurrentValue -StoreAs bool -Tenant $tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1
index 1549723f1dba..2ed285caec7f 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1
@@ -29,36 +29,30 @@ function Invoke-CIPPStandardintuneRequireMFA {
#>
param($Tenant, $Settings)
- $TestResult = Test-CIPPStandardLicense -StandardName 'intuneRequireMFA' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1')
##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneRequireMFA'
- if ($TestResult -eq $false) {
- Write-Host "We're exiting as the correct license is not present for this standard."
- return $true
- } #we're done.
-
try {
$PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant
- }
- catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneRequireMFA state for $Tenant. Error: $ErrorMessage" -Sev Error
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneRequireMFA state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
return
}
- If ($Settings.remediate -eq $true) {
+ if ($Settings.remediate -eq $true) {
if ($PreviousSetting.multiFactorAuthConfiguration -eq 'required') {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Require to use MFA when joining/registering Entra Devices is already enabled.' -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Require to use MFA when joining/registering Entra Devices is already enabled.' -sev Info
} else {
try {
$NewSetting = $PreviousSetting
$NewSetting.multiFactorAuthConfiguration = 'required'
$NewBody = ConvertTo-Json -Compress -InputObject $NewSetting -Depth 10
- New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -Type PUT -Body $NewBody -ContentType 'application/json'
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Set required to use MFA when joining/registering Entra Devices' -sev Info
+ New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -Type PUT -Body $NewBody
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Set required to use MFA when joining/registering Entra Devices' -sev Info
+ $PreviousSetting.multiFactorAuthConfiguration = 'required'
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set require to use MFA when joining/registering Entra Devices: $ErrorMessage" -sev Error
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set require to use MFA when joining/registering Entra Devices: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
}
}
}
@@ -66,16 +60,16 @@ function Invoke-CIPPStandardintuneRequireMFA {
if ($Settings.alert -eq $true) {
if ($PreviousSetting.multiFactorAuthConfiguration -eq 'required') {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Require to use MFA when joining/registering Entra Devices is enabled.' -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Require to use MFA when joining/registering Entra Devices is enabled.' -sev Info
} else {
- Write-StandardsAlert -message 'Require to use MFA when joining/registering Entra Devices is not enabled' -object $PreviousSetting -tenant $tenant -standardName 'intuneRequireMFA' -standardId $Settings.standardId
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Require to use MFA when joining/registering Entra Devices is not enabled.' -sev Info
+ Write-StandardsAlert -message 'Require to use MFA when joining/registering Entra Devices is not enabled' -object $PreviousSetting -tenant $Tenant -standardName 'intuneRequireMFA' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Require to use MFA when joining/registering Entra Devices is not enabled.' -sev Info
}
}
if ($Settings.report -eq $true) {
$RequireMFA = if ($PreviousSetting.multiFactorAuthConfiguration -eq 'required') { $true } else { $false }
- Set-CIPPStandardsCompareField -FieldName 'standards.intuneRequireMFA' -FieldValue $RequireMFA -Tenant $tenant
- Add-CIPPBPAField -FieldName 'intuneRequireMFA' -FieldValue $RequireMFA -StoreAs bool -Tenant $tenant
+ Set-CIPPStandardsCompareField -FieldName 'standards.intuneRequireMFA' -FieldValue $RequireMFA -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'intuneRequireMFA' -FieldValue $RequireMFA -StoreAs bool -Tenant $Tenant
}
}
diff --git a/Modules/CIPPCore/Public/Functions/Expand-CIPPTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1
similarity index 100%
rename from Modules/CIPPCore/Public/Functions/Expand-CIPPTenantGroups.ps1
rename to Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1
diff --git a/Modules/CIPPCore/Public/Functions/Get-TenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1
similarity index 80%
rename from Modules/CIPPCore/Public/Functions/Get-TenantGroups.ps1
rename to Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1
index 761e1462d9cb..bb53c0e9e0b3 100644
--- a/Modules/CIPPCore/Public/Functions/Get-TenantGroups.ps1
+++ b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1
@@ -11,8 +11,9 @@ function Get-TenantGroups {
#>
[CmdletBinding()]
param(
- $GroupId,
- $TenantFilter
+ [string]$GroupId,
+ [string]$TenantFilter,
+ [switch]$Dynamic
)
$GroupTable = Get-CippTable -tablename 'TenantGroups'
@@ -30,6 +31,12 @@ function Get-TenantGroups {
}
$Tenants = Get-Tenants @TenantParams
+ if ($Dynamic.IsPresent) {
+ $GroupTable.Filter = "PartitionKey eq 'TenantGroup' and GroupType eq 'dynamic'"
+ } else {
+ $GroupTable.Filter = "PartitionKey eq 'TenantGroup'"
+ }
+
if ($GroupId) {
$Groups = Get-CIPPAzDataTableEntity @GroupTable -Filter "RowKey eq '$GroupId'"
$AllMembers = Get-CIPPAzDataTableEntity @MembersTable -Filter "GroupId eq '$GroupId'"
@@ -77,10 +84,13 @@ function Get-TenantGroups {
$SortedMembers = @()
}
$Results.Add([PSCustomObject]@{
- Id = $Group.RowKey
- Name = $Group.Name
- Description = $Group.Description
- Members = @($SortedMembers)
+ Id = $Group.RowKey
+ Name = $Group.Name
+ Description = $Group.Description
+ GroupType = $Group.GroupType ?? 'static'
+ RuleLogic = $Group.RuleLogic ?? 'and'
+ DynamicRules = $Group.DynamicRules ? @($Group.DynamicRules | ConvertFrom-Json) : @()
+ Members = @($SortedMembers)
})
}
return $Results | Sort-Object Name
diff --git a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1
new file mode 100644
index 000000000000..8738313f79e1
--- /dev/null
+++ b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1
@@ -0,0 +1,190 @@
+function Update-CIPPDynamicTenantGroups {
+ <#
+ .SYNOPSIS
+ Update dynamic tenant groups based on their rules
+ .DESCRIPTION
+ This function processes dynamic tenant group rules and updates membership accordingly
+ .PARAMETER GroupId
+ The specific group ID to update. If not provided, all dynamic groups will be updated
+ .FUNCTIONALITY
+ Internal
+ #>
+ [CmdletBinding()]
+ param(
+ [string]$GroupId
+ )
+
+ try {
+ $GroupTable = Get-CippTable -tablename 'TenantGroups'
+ $MembersTable = Get-CippTable -tablename 'TenantGroupMembers'
+ $LicenseCacheTable = Get-CippTable -tablename 'cachetenantskus'
+
+ $Skus = Get-CIPPAzDataTableEntity @LicenseCacheTable -Filter "PartitionKey eq 'sku' and Timestamp ge datetime'$( (Get-Date).ToUniversalTime().AddHours(-8).ToString('yyyy-MM-ddTHH:mm:ssZ') )'"
+
+ $SkuHashtable = @{}
+ foreach ($Sku in $Skus) {
+ if ($Sku.JSON -and (Test-Json -Json $Sku.JSON -ErrorAction SilentlyContinue)) {
+ $SkuHashtable[$Sku.RowKey] = $Sku.JSON | ConvertFrom-Json
+ }
+ }
+
+ if ($GroupId) {
+ $DynamicGroups = Get-CIPPAzDataTableEntity @GroupTable -Filter "PartitionKey eq 'TenantGroup' and RowKey eq '$GroupId'"
+ } else {
+ $DynamicGroups = Get-CIPPAzDataTableEntity @GroupTable -Filter "PartitionKey eq 'TenantGroup' and GroupType eq 'dynamic'"
+ }
+
+ if (-not $DynamicGroups) {
+ Write-LogMessage -API 'TenantGroups' -message 'No dynamic groups found to process' -sev Info
+ return @{ MembersAdded = 0; MembersRemoved = 0; GroupsProcessed = 0 }
+ }
+
+ $AllTenants = Get-Tenants -IncludeErrors
+ $TotalMembersAdded = 0
+ $TotalMembersRemoved = 0
+ $GroupsProcessed = 0
+
+ foreach ($Group in $DynamicGroups) {
+ try {
+ Write-LogMessage -API 'TenantGroups' -message "Processing dynamic group: $($Group.Name)" -sev Info
+ $Rules = @($Group.DynamicRules | ConvertFrom-Json)
+ # Build a single Where-Object string for AND logic
+ $WhereConditions = foreach ($Rule in $Rules) {
+ $Property = $Rule.property
+ $Operator = $Rule.operator
+ $Value = $Rule.value
+
+ switch ($Property) {
+ 'delegatedAccessStatus' {
+ "`$_.delegatedPrivilegeStatus -$Operator '$($Value.value)'"
+ }
+ 'availableLicense' {
+ if ($Operator -in @('in', 'notin')) {
+ $arrayValues = if ($Value -is [array]) { $Value.guid } else { @($Value.guid) }
+ $arrayAsString = $arrayValues | ForEach-Object { "'$_'" }
+ if ($Operator -eq 'in') {
+ "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0"
+ } else {
+ "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0"
+ }
+ } else {
+ "`$_.skuId -$Operator '$($Value.guid)'"
+ }
+ }
+ 'availableServicePlan' {
+ if ($Operator -in @('in', 'notin')) {
+ $arrayValues = if ($Value -is [array]) { $Value.value } else { @($Value.value) }
+ $arrayAsString = $arrayValues | ForEach-Object { "'$_'" }
+ if ($Operator -eq 'in') {
+ # Keep tenants with ANY of the provided plans
+ "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0"
+ } else {
+ # Exclude tenants with ANY of the provided plans
+ "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0"
+ }
+ } else {
+ "`$_.servicePlans -$Operator '$($Value.value)'"
+ }
+ }
+ default {
+ Write-LogMessage -API 'TenantGroups' -message "Unknown property type: $Property" -sev Warning
+ $null
+ }
+ }
+
+ }
+ if (!$WhereConditions) {
+ throw 'Generating the conditions failed. The conditions seem to be empty.'
+ }
+ $TenantObj = $AllTenants | ForEach-Object {
+ if ($Rules.property -contains 'availableLicense') {
+ if ($SkuHashtable.ContainsKey($_.customerId)) {
+ Write-Information "Using cached licenses for tenant $($_.defaultDomainName)"
+ $LicenseInfo = $SkuHashtable[$_.customerId]
+ } else {
+ Write-Information "Fetching licenses for tenant $($_.defaultDomainName)"
+ $LicenseInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/subscribedSkus' -TenantId $_.defaultDomainName
+ # Cache the result
+ $CacheEntity = @{
+ PartitionKey = 'sku'
+ RowKey = [string]$_.customerId
+ JSON = [string]($LicenseInfo | ConvertTo-Json -Depth 5 -Compress)
+ }
+ Add-CIPPAzDataTableEntity @LicenseCacheTable -Entity $CacheEntity -Force
+ }
+ }
+ $SKUId = $LicenseInfo.SKUId ?? @()
+ $ServicePlans = (Get-CIPPTenantCapabilities -TenantFilter $_.defaultDomainName).psobject.properties.name
+ [pscustomobject]@{
+ customerId = $_.customerId
+ defaultDomainName = $_.defaultDomainName
+ displayName = $_.displayName
+ skuId = $SKUId
+ servicePlans = $ServicePlans
+ delegatedPrivilegeStatus = $_.delegatedPrivilegeStatus
+ }
+ }
+ # Combine all conditions with the specified logic (AND or OR)
+ $LogicOperator = if ($Group.RuleLogic -eq 'or') { ' -or ' } else { ' -and ' }
+ $WhereString = $WhereConditions -join $LogicOperator
+ Write-Information "Evaluating tenants with condition: $WhereString"
+
+ $ScriptBlock = [ScriptBlock]::Create($WhereString)
+ $MatchingTenants = $TenantObj | Where-Object $ScriptBlock
+
+ Write-Information "Found $($MatchingTenants.Count) matching tenants for group '$($Group.Name)'"
+
+ $CurrentMembers = Get-CIPPAzDataTableEntity @MembersTable -Filter "PartitionKey eq 'Member' and GroupId eq '$($Group.RowKey)'"
+ $CurrentMemberIds = $CurrentMembers.customerId
+ $NewMemberIds = $MatchingTenants.customerId
+
+ $ToAdd = $NewMemberIds | Where-Object { $_ -notin $CurrentMemberIds }
+ $ToRemove = $CurrentMemberIds | Where-Object { $_ -notin $NewMemberIds }
+
+ foreach ($TenantId in $ToAdd) {
+ $TenantInfo = $AllTenants | Where-Object { $_.customerId -eq $TenantId }
+ $MemberEntity = @{
+ PartitionKey = 'Member'
+ RowKey = '{0}-{1}' -f $Group.RowKey, $TenantId
+ GroupId = $Group.RowKey
+ customerId = "$TenantId"
+ }
+ Add-CIPPAzDataTableEntity @MembersTable -Entity $MemberEntity -Force
+ Write-LogMessage -API 'TenantGroups' -message "Added tenant '$($TenantInfo.displayName)' to dynamic group '$($Group.Name)'" -sev Info
+ $TotalMembersAdded++
+ }
+
+ foreach ($TenantId in $ToRemove) {
+ $TenantInfo = $AllTenants | Where-Object { $_.customerId -eq $TenantId }
+ $MemberToRemove = $CurrentMembers | Where-Object { $_.customerId -eq $TenantId }
+ if ($MemberToRemove) {
+ Remove-AzDataTableEntity @MembersTable -Entity $MemberToRemove -Force
+ Write-LogMessage -API 'TenantGroups' -message "Removed tenant '$($TenantInfo.displayName)' from dynamic group '$($Group.Name)'" -sev Info
+ $TotalMembersRemoved++
+ }
+ }
+
+ $GroupsProcessed++
+ Write-LogMessage -API 'TenantGroups' -message "Group '$($Group.Name)' updated: +$($ToAdd.Count) members, -$($ToRemove.Count) members" -sev Info
+
+ } catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -API 'TenantGroups' -message "Failed to process group '$($Group.Name)': $ErrorMessage" -sev Error
+ }
+ }
+
+ Write-LogMessage -API 'TenantGroups' -message "Dynamic tenant group update completed. Groups processed: $GroupsProcessed, Members added: $TotalMembersAdded, Members removed: $TotalMembersRemoved" -sev Info
+
+ return @{
+ MembersAdded = $TotalMembersAdded
+ MembersRemoved = $TotalMembersRemoved
+ GroupsProcessed = $GroupsProcessed
+ }
+
+ } catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -API 'TenantGroups' -message "Failed to update dynamic tenant groups: $ErrorMessage" -sev Error
+ throw
+ }
+}
+
diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1
index b7361fe4b2ea..0d7f1aa20e1d 100644
--- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1
+++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1
@@ -79,7 +79,7 @@ function Invoke-CippWebhookProcessing {
# Save audit log entry to table
$LocationInfo = $Data.CIPPLocationInfo | ConvertFrom-Json -ErrorAction SilentlyContinue
$AuditRecord = $Data.AuditRecord | ConvertFrom-Json -ErrorAction SilentlyContinue
- $GenerateJSON = New-CIPPAlertTemplate -format 'json' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL
+ $GenerateJSON = New-CIPPAlertTemplate -format 'json' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -AlertComment $WebhookRule.AlertComment
$JsonContent = @{
Title = $GenerateJSON.Title
ActionUrl = $GenerateJSON.ButtonUrl
@@ -102,7 +102,7 @@ function Invoke-CippWebhookProcessing {
$LogId = Send-CIPPAlert @CIPPAlert
$AuditLogLink = '{0}/tenant/administration/audit-logs/log?logId={1}&tenantFilter={2}' -f $CIPPURL, $LogId, $Tenant.defaultDomainName
- $GenerateEmail = New-CIPPAlertTemplate -format 'html' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -Tenant $Tenant.defaultDomainName -AuditLogLink $AuditLogLink
+ $GenerateEmail = New-CIPPAlertTemplate -format 'html' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -Tenant $Tenant.defaultDomainName -AuditLogLink $AuditLogLink -AlertComment $WebhookRule.AlertComment
Write-Host 'Going to create the content'
foreach ($action in $ActionList ) {
diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1
index 04fcdfb76c9b..f6d5b73a7ebf 100644
--- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1
+++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1
@@ -117,8 +117,6 @@ function Test-CIPPAuditLogRules {
$CacheWebhooksTable = Get-CippTable -TableName 'CacheWebhooks'
$ExtendedPropertiesIgnoreList = @(
- 'OAuth2:Authorize'
- 'OAuth2:Token'
'SAS:EndAuth'
'SAS:ProcessAuth'
'deviceAuth:ReprocessTls'
diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1
index 71c92337da9b..963614aa0aad 100644
--- a/Modules/CippEntrypoints/CippEntrypoints.psm1
+++ b/Modules/CippEntrypoints/CippEntrypoints.psm1
@@ -144,6 +144,16 @@ function Receive-CippHttpTrigger {
$Response.Body = $Response.Body | ConvertTo-Json -Depth 20 -Compress
}
Push-OutputBinding -Name Response -Value ([HttpResponseContext]$Response)
+ } else {
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::InternalServerError
+ Body = @{
+ error = @{
+ code = 'InternalServerError'
+ message = 'An error occurred processing the request'
+ }
+ }
+ })
}
}
return
diff --git a/host.json b/host.json
index e0b8b636c71e..cb92d98a0fcc 100644
--- a/host.json
+++ b/host.json
@@ -14,8 +14,8 @@
"maxConcurrentOrchestratorFunctions": 2,
"tracing": {
"distributedTracingEnabled": false,
- "version": "V2"
+ "version": "None"
}
}
}
-}
\ No newline at end of file
+}
diff --git a/version_latest.txt b/version_latest.txt
index 85e2cd530929..acd405b1d62e 100644
--- a/version_latest.txt
+++ b/version_latest.txt
@@ -1 +1 @@
-8.5.2
+8.6.0