From b2985a6f07cd60c812747367d8c64e64add34cb6 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:25:26 +0200 Subject: [PATCH 1/6] fix(assignments): change default mode to append for consistency --- .../Public/Set-CIPPAssignedApplication.ps1 | 2 +- .../Public/Set-CIPPAssignedPolicy.ps1 | 2 +- .../Applications/Invoke-ExecAssignApp.ps1 | 2 +- .../Endpoint/MEM/Invoke-ExecAssignPolicy.ps1 | 2 +- Tests/Endpoint/Invoke-ExecAssignApp.Tests.ps1 | 66 +++++++++++++++++++ .../Invoke-ExecAssignPolicy.Tests.ps1 | 64 ++++++++++++++++++ 6 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 Tests/Endpoint/Invoke-ExecAssignApp.Tests.ps1 create mode 100644 Tests/Endpoint/Invoke-ExecAssignPolicy.Tests.ps1 diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 index 18abcd1c7c659..ac9db1621c57e 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 @@ -10,7 +10,7 @@ function Set-CIPPAssignedApplication { $ApplicationId, $TenantFilter, $GroupIds, - $AssignmentMode = 'replace', + $AssignmentMode = 'append', $AssignmentDirection, $APIName = 'Assign Application', $Headers, diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 index faff217f016f1..568ee56c4afaa 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 @@ -15,7 +15,7 @@ function Set-CIPPAssignedPolicy { $AssignmentFilterType = 'include', $GroupIds, $GroupNames, - $AssignmentMode = 'replace', + $AssignmentMode = 'append', $AssignmentDirection ) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAssignApp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAssignApp.ps1 index 1f6e1fcb6edef..dac3e6bba060c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAssignApp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAssignApp.ps1 @@ -31,7 +31,7 @@ function Invoke-ExecAssignApp { $Intent = if ([string]::IsNullOrWhiteSpace($Intent)) { 'Required' } else { $Intent } if ([string]::IsNullOrWhiteSpace($AssignmentMode)) { - $AssignmentMode = 'replace' + $AssignmentMode = 'append' } else { $AssignmentMode = $AssignmentMode.ToLower() if ($AssignmentMode -notin @('replace', 'append')) { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecAssignPolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecAssignPolicy.ps1 index 722a5f37cdc04..147f670e64fd5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecAssignPolicy.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecAssignPolicy.ps1 @@ -49,7 +49,7 @@ function Invoke-ExecAssignPolicy { # Validate and default AssignmentMode if ([string]::IsNullOrWhiteSpace($AssignmentMode)) { - $AssignmentMode = 'replace' + $AssignmentMode = 'append' } $AssignTo = if ($AssignTo -ne 'on') { $AssignTo } diff --git a/Tests/Endpoint/Invoke-ExecAssignApp.Tests.ps1 b/Tests/Endpoint/Invoke-ExecAssignApp.Tests.ps1 new file mode 100644 index 0000000000000..306807a4565ca --- /dev/null +++ b/Tests/Endpoint/Invoke-ExecAssignApp.Tests.ps1 @@ -0,0 +1,66 @@ +# Pester tests for Invoke-ExecAssignApp assignment mode defaults. + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $FunctionPath = Get-ChildItem -Path (Join-Path $RepoRoot 'Modules') -Recurse -Filter 'Invoke-ExecAssignApp.ps1' -File | + Select-Object -First 1 -ExpandProperty FullName + + ([PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')).GetMethod('Add').Invoke( + $null, @('HttpStatusCode', [System.Net.HttpStatusCode])) + + class HttpResponseContext { + [int]$StatusCode + [object]$Body + } + + function Set-CIPPAssignedApplication { param($ApplicationId, $TenantFilter, $Intent, $APIName, $Headers, $GroupName, $AssignmentMode) } + + . $FunctionPath +} + +Describe 'Invoke-ExecAssignApp assignment mode' { + BeforeEach { + $script:assignmentMode = $null + Mock -CommandName Set-CIPPAssignedApplication -MockWith { + $script:assignmentMode = $AssignmentMode + } + } + + It 'defaults omitted assignment mode to append' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'ExecAssignApp' } + Headers = @{} + Query = [pscustomobject]@{} + Body = [pscustomobject]@{ + tenantFilter = 'contoso.onmicrosoft.com' + ID = 'app-1' + AppType = 'Win32Lob' + AssignTo = 'AllDevices' + } + } + + $response = Invoke-ExecAssignApp -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $script:assignmentMode | Should -Be 'append' + } + + It 'preserves explicit replace mode' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'ExecAssignApp' } + Headers = @{} + Query = [pscustomobject]@{} + Body = [pscustomobject]@{ + tenantFilter = 'contoso.onmicrosoft.com' + ID = 'app-1' + AppType = 'Win32Lob' + AssignTo = 'AllDevices' + assignmentMode = 'replace' + } + } + + $null = Invoke-ExecAssignApp -Request $request -TriggerMetadata $null + + $script:assignmentMode | Should -Be 'replace' + } +} diff --git a/Tests/Endpoint/Invoke-ExecAssignPolicy.Tests.ps1 b/Tests/Endpoint/Invoke-ExecAssignPolicy.Tests.ps1 new file mode 100644 index 0000000000000..e600ac0dd7825 --- /dev/null +++ b/Tests/Endpoint/Invoke-ExecAssignPolicy.Tests.ps1 @@ -0,0 +1,64 @@ +# Pester tests for Invoke-ExecAssignPolicy assignment mode defaults. + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $FunctionPath = Get-ChildItem -Path (Join-Path $RepoRoot 'Modules') -Recurse -Filter 'Invoke-ExecAssignPolicy.ps1' -File | + Select-Object -First 1 -ExpandProperty FullName + + ([PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')).GetMethod('Add').Invoke( + $null, @('HttpStatusCode', [System.Net.HttpStatusCode])) + + class HttpResponseContext { + [int]$StatusCode + [object]$Body + } + + function Set-CIPPAssignedPolicy { param($PolicyId, $TenantFilter, $GroupName, $Type, $Headers, $AssignmentMode) } + + . $FunctionPath +} + +Describe 'Invoke-ExecAssignPolicy assignment mode' { + BeforeEach { + $script:assignmentMode = $null + Mock -CommandName Set-CIPPAssignedPolicy -MockWith { + $script:assignmentMode = $AssignmentMode + } + } + + It 'defaults omitted assignment mode to append' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'ExecAssignPolicy' } + Headers = @{} + Body = [pscustomobject]@{ + tenantFilter = 'contoso.onmicrosoft.com' + ID = 'policy-1' + Type = 'configurationPolicies' + AssignTo = 'AllDevices' + } + } + + $response = Invoke-ExecAssignPolicy -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $script:assignmentMode | Should -Be 'append' + } + + It 'preserves explicit replace mode' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'ExecAssignPolicy' } + Headers = @{} + Body = [pscustomobject]@{ + tenantFilter = 'contoso.onmicrosoft.com' + ID = 'policy-1' + Type = 'configurationPolicies' + AssignTo = 'AllDevices' + assignmentMode = 'replace' + } + } + + $null = Invoke-ExecAssignPolicy -Request $request -TriggerMetadata $null + + $script:assignmentMode | Should -Be 'replace' + } +} From 5b42363cd7f489e0ef4f523f1d568e909f6567dd Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 14 Jul 2026 09:35:52 +0100 Subject: [PATCH 2/6] feat(nudge-mfa): add group targeting and auth method selection Expand the NudgeMFA standard to support: - Multiple authentication methods (Authenticator, Passkey) - Group-level include/exclude targeting with group name resolution - Snooze enforcement limits (max 3 snoozes) Adds Set-CIPPRegistrationCampaign helper function as a single writer for the campaign, shared by the new ExecRegistrationCampaign HTTP endpoint and the enhanced NudgeMFA standard. The standard now supports both portal-configured and template-specified group targeting, with full compliance comparison. --- Config/standards.json | 56 +++++- .../Public/Set-CIPPRegistrationCampaign.ps1 | 93 ++++++++++ .../Invoke-ExecRegistrationCampaign.ps1 | 80 ++++++++ .../Standards/Invoke-CIPPStandardNudgeMFA.ps1 | 171 ++++++++++++++---- 4 files changed, 364 insertions(+), 36 deletions(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPRegistrationCampaign.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecRegistrationCampaign.ps1 diff --git a/Config/standards.json b/Config/standards.json index 5a8d0a83eba9f..0feef69292203 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -1371,21 +1371,38 @@ "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.5)"], "appliesToTest": ["SMB1001_2_5", "ZTNA21889"], - "helpText": "Sets the state of the registration campaign for the tenant", - "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.", + "helpText": "Sets the state of the registration campaign for the tenant, including the targeted authentication method, snooze settings and include/exclude groups. Leave include/exclude blank to keep the groups currently configured in the tenant, or use 'AllUsers' to target all users.", + "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the targeted authentication method (Microsoft Authenticator or a Passkey) during sign-in. Supports limiting the number of snoozes, and including or excluding specific groups (by display name).", "executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.", "addedComponent": [ { "type": "autoComplete", "multiple": false, "creatable": false, - "label": "Select value", + "label": "Registration campaign state", "name": "standards.NudgeMFA.state", "options": [ { "label": "Enabled", "value": "enabled" }, { "label": "Disabled", "value": "disabled" } ] }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "required": false, + "label": "Authentication method to nudge users to register (default is Microsoft Authenticator)", + "name": "standards.NudgeMFA.targetedAuthenticationMethod", + "options": [ + { "label": "Microsoft Authenticator", "value": "microsoftAuthenticator" }, + { "label": "Passkey (FIDO2)", "value": "fido2" } + ], + "condition": { + "field": "standards.NudgeMFA.state", + "compareType": "valueEq", + "compareValue": "enabled" + } + }, { "type": "number", "name": "standards.NudgeMFA.snoozeDurationInDays", @@ -1395,6 +1412,39 @@ "min": { "value": 0, "message": "Minimum value is 0" }, "max": { "value": 14, "message": "Maximum value is 14" } } + }, + { + "type": "switch", + "name": "standards.NudgeMFA.enforceRegistrationAfterAllowedSnoozes", + "label": "Limited number of snoozes (require registration after 3 snoozes)", + "defaultValue": true, + "condition": { + "field": "standards.NudgeMFA.state", + "compareType": "valueEq", + "compareValue": "enabled" + } + }, + { + "type": "textField", + "name": "standards.NudgeMFA.includeTargets", + "label": "Include groups (comma separated group names, 'AllUsers' for everyone, blank = keep current targets)", + "required": false, + "condition": { + "field": "standards.NudgeMFA.state", + "compareType": "valueEq", + "compareValue": "enabled" + } + }, + { + "type": "textField", + "name": "standards.NudgeMFA.excludeTargets", + "label": "Exclude groups (comma separated group names, blank = keep current exclusions)", + "required": false, + "condition": { + "field": "standards.NudgeMFA.state", + "compareType": "valueEq", + "compareValue": "enabled" + } } ], "label": "Sets the state for the request to setup Authenticator", diff --git a/Modules/CIPPCore/Public/Set-CIPPRegistrationCampaign.ps1 b/Modules/CIPPCore/Public/Set-CIPPRegistrationCampaign.ps1 new file mode 100644 index 0000000000000..52b7e0e29544f --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPRegistrationCampaign.ps1 @@ -0,0 +1,93 @@ +function Set-CIPPRegistrationCampaign { + <# + .SYNOPSIS + Updates the authentication methods registration campaign (nudge) for a tenant. + .DESCRIPTION + Single writer for the registration campaign, shared by the ExecRegistrationCampaign + endpoint and the NudgeMFA standard. Any parameter left as $null keeps the value + currently configured in the tenant, so callers can update settings independently. + .PARAMETER IncludeTargets + Array of @{ id; targetType } targets. $null keeps the current include targets. The + targeted authentication method is applied to every include target, and a campaign + always ends up with at least one include target (falls back to all_users). + .PARAMETER ExcludeTargets + Array of @{ id; targetType } targets. $null keeps the current exclusions, an empty + array clears them. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)]$Tenant, + $State, + $TargetedAuthenticationMethod, + $SnoozeDurationInDays, + $EnforceRegistrationAfterAllowedSnoozes, + $IncludeTargets, + $ExcludeTargets, + $APIName = 'Set Registration Campaign', + $Headers + ) + + try { + $CurrentPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -tenantid $Tenant + $CurrentCampaign = $CurrentPolicy.registrationEnforcement.authenticationMethodsRegistrationCampaign + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Could not get the current registration campaign. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + throw "Could not get the current registration campaign. Error: $($ErrorMessage.NormalizedError)" + } + + $DesiredState = $State ?? $CurrentCampaign.state + $DesiredMethod = $TargetedAuthenticationMethod ?? (@($CurrentCampaign.includeTargets).targetedAuthenticationMethod | Select-Object -First 1) ?? 'microsoftAuthenticator' + $DesiredSnooze = if ($null -ne $SnoozeDurationInDays) { [int]$SnoozeDurationInDays } else { [int]($CurrentCampaign.snoozeDurationInDays ?? 1) } + $DesiredEnforce = if ($null -ne $EnforceRegistrationAfterAllowedSnoozes) { [bool]$EnforceRegistrationAfterAllowedSnoozes } else { [bool]$CurrentCampaign.enforceRegistrationAfterAllowedSnoozes } + + if ($DesiredState -notin @('default', 'enabled', 'disabled')) { + throw "State must be one of 'default', 'enabled' or 'disabled'" + } + if ($DesiredMethod -notin @('microsoftAuthenticator', 'fido2')) { + throw "TargetedAuthenticationMethod must be 'microsoftAuthenticator' or 'fido2'" + } + if ($DesiredSnooze -lt 0 -or $DesiredSnooze -gt 14) { + throw 'SnoozeDurationInDays must be between 0 and 14' + } + + $DesiredIncludeTargets = if ($null -ne $IncludeTargets) { + @($IncludeTargets | ForEach-Object { @{ id = "$($_.id)"; targetType = "$($_.targetType)"; targetedAuthenticationMethod = $DesiredMethod } }) + } else { + @($CurrentCampaign.includeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType; targetedAuthenticationMethod = $DesiredMethod } }) + } + if ($DesiredIncludeTargets.Count -eq 0) { + $DesiredIncludeTargets = @(@{ id = 'all_users'; targetType = 'group'; targetedAuthenticationMethod = $DesiredMethod }) + } + + $DesiredExcludeTargets = if ($null -ne $ExcludeTargets) { + @($ExcludeTargets | ForEach-Object { @{ id = "$($_.id)"; targetType = "$($_.targetType)" } }) + } else { + @($CurrentCampaign.excludeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType } }) + } + + $Body = @{ + registrationEnforcement = @{ + authenticationMethodsRegistrationCampaign = @{ + state = $DesiredState + snoozeDurationInDays = $DesiredSnooze + enforceRegistrationAfterAllowedSnoozes = $DesiredEnforce + includeTargets = @($DesiredIncludeTargets) + excludeTargets = @($DesiredExcludeTargets) + } + } + } | ConvertTo-Json -Depth 10 -Compress + + try { + $Result = "Set the registration campaign state to $DesiredState targeting $DesiredMethod with a snooze duration of $DesiredSnooze day(s), $($DesiredIncludeTargets.Count) include target(s) and $($DesiredExcludeTargets.Count) exclude target(s)" + if ($PSCmdlet.ShouldProcess('Registration campaign', "Set state to $DesiredState")) { + $null = New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -Type PATCH -Body $Body -ContentType 'application/json' -AsApp $false + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message $Result -sev Info + } + return $Result + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Failed to update the registration campaign. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + throw "Failed to update the registration campaign. Error: $($ErrorMessage.NormalizedError)" + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecRegistrationCampaign.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecRegistrationCampaign.ps1 new file mode 100644 index 0000000000000..ce03c60af94d2 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecRegistrationCampaign.ps1 @@ -0,0 +1,80 @@ +function Invoke-ExecRegistrationCampaign { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Tenant.Administration.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $TenantFilter = $Request.Body.tenantFilter.value ?? $Request.Body.tenantFilter + + if (-not $TenantFilter) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: tenantFilter is required' } + } + } + + if ($null -ne $Request.Body.snoozeDurationInDays -and ([int]$Request.Body.snoozeDurationInDays -lt 0 -or [int]$Request.Body.snoozeDurationInDays -gt 14)) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: snoozeDurationInDays must be between 0 and 14' } + } + } + + try { + # Build include/exclude target lists; $null means "keep what is currently configured" + $IncludeSpecified = ($null -ne $Request.Body.includeAllUsers) -or ($null -ne $Request.Body.includeGroups) -or ($null -ne $Request.Body.includeUsers) + $IncludeTargets = if ($IncludeSpecified) { + if ([bool]$Request.Body.includeAllUsers) { + @(@{ id = 'all_users'; targetType = 'group' }) + } else { + $Targets = [System.Collections.Generic.List[hashtable]]::new() + foreach ($GroupId in @($Request.Body.includeGroups)) { + if ($GroupId) { $Targets.Add(@{ id = "$GroupId"; targetType = 'group' }) } + } + foreach ($UserId in @($Request.Body.includeUsers)) { + if ($UserId) { $Targets.Add(@{ id = "$UserId"; targetType = 'user' }) } + } + @($Targets) + } + } else { $null } + + $ExcludeSpecified = ($null -ne $Request.Body.excludeGroups) -or ($null -ne $Request.Body.excludeUsers) + $ExcludeTargets = if ($ExcludeSpecified) { + $Targets = [System.Collections.Generic.List[hashtable]]::new() + foreach ($GroupId in @($Request.Body.excludeGroups)) { + if ($GroupId) { $Targets.Add(@{ id = "$GroupId"; targetType = 'group' }) } + } + foreach ($UserId in @($Request.Body.excludeUsers)) { + if ($UserId) { $Targets.Add(@{ id = "$UserId"; targetType = 'user' }) } + } + @($Targets) + } else { $null } + + $CampaignParams = @{ + Tenant = $TenantFilter + State = $Request.Body.state.value ?? $Request.Body.state + TargetedAuthenticationMethod = $Request.Body.targetedAuthenticationMethod.value ?? $Request.Body.targetedAuthenticationMethod + SnoozeDurationInDays = $Request.Body.snoozeDurationInDays + EnforceRegistrationAfterAllowedSnoozes = $Request.Body.enforceRegistrationAfterAllowedSnoozes + IncludeTargets = $IncludeTargets + ExcludeTargets = $ExcludeTargets + APIName = $APIName + Headers = $Headers + } + $Result = Set-CIPPRegistrationCampaign @CampaignParams + } catch { + $Result = $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode ?? [HttpStatusCode]::OK + Body = @{ Results = $Result } + } +} diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 index 17921a7878a31..afac49b65a297 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 @@ -7,8 +7,8 @@ function Invoke-CIPPStandardNudgeMFA { .SYNOPSIS (Label) Sets the state for the request to setup Authenticator .DESCRIPTION - (Helptext) Sets the state of the registration campaign for the tenant - (DocsDescription) Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in. + (Helptext) Sets the state of the registration campaign for the tenant, including the targeted authentication method, snooze settings and include/exclude groups. Leave include/exclude blank to keep the groups currently configured in the tenant, or use 'AllUsers' to target all users. + (DocsDescription) Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the targeted authentication method (Microsoft Authenticator or a Passkey) during sign-in. Supports limiting the number of snoozes, and including or excluding specific groups (by display name). .NOTES CAT Entra (AAD) Standards @@ -17,8 +17,12 @@ function Invoke-CIPPStandardNudgeMFA { EXECUTIVETEXT Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes. ADDEDCOMPONENT - {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.NudgeMFA.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Registration campaign state","name":"standards.NudgeMFA.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + {"type":"autoComplete","multiple":false,"creatable":false,"required":false,"label":"Authentication method to nudge users to register (default is Microsoft Authenticator)","name":"standards.NudgeMFA.targetedAuthenticationMethod","options":[{"label":"Microsoft Authenticator","value":"microsoftAuthenticator"},{"label":"Passkey (FIDO2)","value":"fido2"}],"condition":{"field":"standards.NudgeMFA.state","compareType":"valueEq","compareValue":"enabled"}} {"type":"number","name":"standards.NudgeMFA.snoozeDurationInDays","label":"Number of days to allow users to skip registering Authenticator (0-14, default is 1)","defaultValue":1,"validators":{"min":{"value":0,"message":"Minimum value is 0"},"max":{"value":14,"message":"Maximum value is 14"}}} + {"type":"switch","name":"standards.NudgeMFA.enforceRegistrationAfterAllowedSnoozes","label":"Limited number of snoozes (require registration after 3 snoozes)","defaultValue":true,"condition":{"field":"standards.NudgeMFA.state","compareType":"valueEq","compareValue":"enabled"}} + {"type":"textField","name":"standards.NudgeMFA.includeTargets","label":"Include groups (comma separated group names, 'AllUsers' for everyone, blank = keep current targets)","required":false,"condition":{"field":"standards.NudgeMFA.state","compareType":"valueEq","compareValue":"enabled"}} + {"type":"textField","name":"standards.NudgeMFA.excludeTargets","label":"Exclude groups (comma separated group names, blank = keep current exclusions)","required":false,"condition":{"field":"standards.NudgeMFA.state","compareType":"valueEq","compareValue":"enabled"}} IMPACT Low Impact ADDEDDATE @@ -34,45 +38,138 @@ function Invoke-CIPPStandardNudgeMFA { param($Tenant, $Settings) + # NOTE: The ADDEDCOMPONENT conditions above use compareType 'valueEq' rather than the usual 'is'. + # The state field is an autoComplete which stores a {label, value} object, so 'is' (deep equality + # against the raw string) never matches; 'valueEq' compares against the object's .value property + # and is supported by CippFormCondition. Changing state to a 'select' field would allow 'is', but + # would break existing saved NudgeMFA templates that already store the object shape. + + # Resolves comma separated group name entries to registration campaign targets + function Resolve-NudgeMFATarget { + param($Entries, $TenantFilter) + $Resolved = [System.Collections.Generic.List[hashtable]]::new() + $Failed = $false + foreach ($Entry in $Entries) { + try { + if ($Entry -match '^(all_users|allusers|all users)$') { + $Resolved.Add(@{ id = 'all_users'; targetType = 'group' }) + } else { + $EscapedName = $Entry -replace "'", "''" + $GroupFilter = [System.Uri]::EscapeDataString("startsWith(displayName,'$EscapedName')") + $MatchedGroups = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$select=id,displayName&`$filter=$GroupFilter" -tenantid $TenantFilter) + if ($MatchedGroups.Count -gt 0) { + foreach ($Group in $MatchedGroups) { $Resolved.Add(@{ id = $Group.id; targetType = 'group' }) } + if ($MatchedGroups.Count -gt 1) { + Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "NudgeMFA: Multiple groups matched '$Entry': $($MatchedGroups.displayName -join ', ')" -sev Info + } + } else { + Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "NudgeMFA: No group found matching '$Entry'" -sev Warning + $Failed = $true + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "NudgeMFA: Failed to resolve target '$Entry'. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $Failed = $true + } + } + return [PSCustomObject]@{ Targets = $Resolved; Failed = $Failed } + } + # Get state value using null-coalescing operator $State = $Settings.state.value ?? $Settings.state try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -tenantid $Tenant - $StateIsCorrect = ($CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.state -eq $State) -and - ($CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.snoozeDurationInDays -eq $Settings.snoozeDurationInDays) -and - ($CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.enforceRegistrationAfterAllowedSnoozes -eq $true) + $CurrentCampaign = $CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Failed to get Authenticator App Nudge state, check your permissions and try again' -sev Error -LogData (Get-CippException -Exception $_) return } + $SnoozeDuration = [int]($Settings.snoozeDurationInDays ?? 1) + $EnforceAfterSnoozes = if ($null -eq $Settings.enforceRegistrationAfterAllowedSnoozes) { $true } else { [bool]$Settings.enforceRegistrationAfterAllowedSnoozes } + # Fall back to the method already targeted in the tenant so existing templates keep their current campaign type + $TargetedMethod = $Settings.targetedAuthenticationMethod.value ?? $Settings.targetedAuthenticationMethod ?? (@($CurrentCampaign.includeTargets).targetedAuthenticationMethod | Select-Object -First 1) ?? 'microsoftAuthenticator' + + # NOTE: Unlike the AuthenticationMethods standard (where a blank group field means "All Users"), + # blank include/exclude here means "keep the targets currently configured in the tenant". This is + # deliberate: NudgeMFA predates these fields and existing deployments would otherwise have their + # portal-configured targeting overwritten to all_users on the next run. Use the literal 'AllUsers' + # entry to target everyone explicitly. + $IncludeEntries = @(([string]($Settings.includeTargets ?? '')) -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $ExcludeEntries = @(([string]($Settings.excludeTargets ?? '')) -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + + # $Remediation*Targets are passed to Set-CIPPRegistrationCampaign ($null = keep current targets); + # $Desired*Targets are the fully resolved lists used for the compliance comparison below. + if ($IncludeEntries.Count -gt 0) { + $IncludeResolution = Resolve-NudgeMFATarget -Entries $IncludeEntries -TenantFilter $Tenant + if ($IncludeResolution.Failed -or $IncludeResolution.Targets.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'NudgeMFA: Could not resolve all include groups, skipping to avoid removing intended targets.' -sev Error + return + } + $RemediationIncludeTargets = @($IncludeResolution.Targets) + $DesiredIncludeTargets = @($RemediationIncludeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType; targetedAuthenticationMethod = $TargetedMethod } }) + } else { + $RemediationIncludeTargets = $null + $DesiredIncludeTargets = @($CurrentCampaign.includeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType; targetedAuthenticationMethod = $TargetedMethod } }) + if ($DesiredIncludeTargets.Count -eq 0) { + $DesiredIncludeTargets = @(@{ id = 'all_users'; targetType = 'group'; targetedAuthenticationMethod = $TargetedMethod }) + } + } + + $ManageExcludeTargets = $ExcludeEntries.Count -gt 0 + if ($ManageExcludeTargets) { + $ExcludeResolution = Resolve-NudgeMFATarget -Entries $ExcludeEntries -TenantFilter $Tenant + if ($ExcludeResolution.Failed) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'NudgeMFA: Could not resolve all exclude groups, skipping to avoid excluding the wrong groups.' -sev Error + return + } + $RemediationExcludeTargets = @($ExcludeResolution.Targets) + $DesiredExcludeTargets = @($RemediationExcludeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType } }) + } else { + $RemediationExcludeTargets = $null + $DesiredExcludeTargets = @($CurrentCampaign.excludeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType } }) + } + + $CurrentIncludeIds = @($CurrentCampaign.includeTargets.id) + $DesiredIncludeIds = @($DesiredIncludeTargets.id) + $IncludeIsCorrect = ($CurrentIncludeIds.Count -eq $DesiredIncludeIds.Count) -and + (-not (Compare-Object -ReferenceObject @($DesiredIncludeIds | Sort-Object) -DifferenceObject @($CurrentIncludeIds | Sort-Object) -ErrorAction SilentlyContinue)) + $MethodIsCorrect = @($CurrentCampaign.includeTargets | Where-Object { $_.targetedAuthenticationMethod -ne $TargetedMethod }).Count -eq 0 + + if ($ManageExcludeTargets) { + $CurrentExcludeIds = @($CurrentCampaign.excludeTargets.id) + $DesiredExcludeIds = @($DesiredExcludeTargets.id) + $ExcludeIsCorrect = ($CurrentExcludeIds.Count -eq $DesiredExcludeIds.Count) -and + (-not (Compare-Object -ReferenceObject @($DesiredExcludeIds | Sort-Object) -DifferenceObject @($CurrentExcludeIds | Sort-Object) -ErrorAction SilentlyContinue)) + } else { + $ExcludeIsCorrect = $true + } + + $StateIsCorrect = ($CurrentCampaign.state -eq $State) -and + ([int]$CurrentCampaign.snoozeDurationInDays -eq $SnoozeDuration) -and + ([bool]$CurrentCampaign.enforceRegistrationAfterAllowedSnoozes -eq $EnforceAfterSnoozes) -and + $IncludeIsCorrect -and $MethodIsCorrect -and $ExcludeIsCorrect + if ($Settings.remediate -eq $true) { $StateName = $State.Substring(0, 1).ToUpper() + $State.Substring(1) if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Authenticator App Nudge is already set to $State with a snooze duration of $($Settings.snoozeDurationInDays)." -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Authenticator App Nudge is already set to $State targeting $TargetedMethod with a snooze duration of $SnoozeDuration." -sev Info } else { try { - $GraphRequest = @{ - tenantid = $Tenant - uri = 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' - AsApp = $false - Type = 'PATCH' - ContentType = 'application/json' - Body = @{ - registrationEnforcement = @{ - authenticationMethodsRegistrationCampaign = @{ - state = $State - snoozeDurationInDays = $Settings.snoozeDurationInDays - enforceRegistrationAfterAllowedSnoozes = $true - includeTargets = $CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.includeTargets - excludeTargets = $CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.excludeTargets - } - } - } | ConvertTo-Json -Depth 10 -Compress + $CampaignParams = @{ + Tenant = $Tenant + State = $State + TargetedAuthenticationMethod = $TargetedMethod + SnoozeDurationInDays = $SnoozeDuration + EnforceRegistrationAfterAllowedSnoozes = $EnforceAfterSnoozes + IncludeTargets = $RemediationIncludeTargets + ExcludeTargets = $RemediationExcludeTargets + APIName = 'Standards' } - New-GraphPostRequest @GraphRequest - Write-LogMessage -API 'Standards' -tenant $Tenant -message "$StateName Authenticator App Nudge with a snooze duration of $($Settings.snoozeDurationInDays)" -sev Info + $null = Set-CIPPRegistrationCampaign @CampaignParams + Write-LogMessage -API 'Standards' -tenant $Tenant -message "$StateName Authenticator App Nudge targeting $TargetedMethod with a snooze duration of $SnoozeDuration" -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Authenticator App Nudge to $State. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage @@ -82,21 +179,29 @@ function Invoke-CIPPStandardNudgeMFA { if ($Settings.alert -eq $true) { if ($StateIsCorrect -eq $true) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Authenticator App Nudge is enabled with a snooze duration of $($CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.snoozeDurationInDays)" -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Authenticator App Nudge is configured correctly: $($CurrentCampaign.state) targeting $TargetedMethod with a snooze duration of $($CurrentCampaign.snoozeDurationInDays)" -sev Info } else { - Write-StandardsAlert -message "Authenticator App Nudge is not enabled with a snooze duration of $($CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.snoozeDurationInDays)" -object ($CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign | Select-Object snoozeDurationInDays, state) -tenant $Tenant -standardName 'NudgeMFA' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Authenticator App Nudge is not enabled with a snooze duration of $($CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.snoozeDurationInDays)" -sev Info + Write-StandardsAlert -message "Authenticator App Nudge is not configured as expected: state $($CurrentCampaign.state), snooze duration $($CurrentCampaign.snoozeDurationInDays)" -object ($CurrentCampaign | Select-Object state, snoozeDurationInDays, enforceRegistrationAfterAllowedSnoozes, includeTargets, excludeTargets) -tenant $Tenant -standardName 'NudgeMFA' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Authenticator App Nudge is not configured as expected: state $($CurrentCampaign.state), snooze duration $($CurrentCampaign.snoozeDurationInDays)" -sev Info } } if ($Settings.report -eq $true) { $CurrentValue = @{ - snoozeDurationInDays = $CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.snoozeDurationInDays - state = $CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.state + state = $CurrentCampaign.state + snoozeDurationInDays = $CurrentCampaign.snoozeDurationInDays + enforceRegistrationAfterAllowedSnoozes = [bool]$CurrentCampaign.enforceRegistrationAfterAllowedSnoozes + targetedAuthenticationMethod = (@($CurrentCampaign.includeTargets).targetedAuthenticationMethod | Select-Object -First 1) + includeTargets = @($CurrentCampaign.includeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType } }) + excludeTargets = @($CurrentCampaign.excludeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType } }) } $ExpectedValue = @{ - snoozeDurationInDays = $Settings.snoozeDurationInDays - state = $State + state = $State + snoozeDurationInDays = $SnoozeDuration + enforceRegistrationAfterAllowedSnoozes = $EnforceAfterSnoozes + targetedAuthenticationMethod = $TargetedMethod + includeTargets = @($DesiredIncludeTargets | ForEach-Object { @{ id = $_.id; targetType = $_.targetType } }) + excludeTargets = $DesiredExcludeTargets } Set-CIPPStandardsCompareField -FieldName 'standards.NudgeMFA' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'NudgeMFA' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant From 176d43baa79735b87352c61f56d72888f7221056 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:16:50 +0200 Subject: [PATCH 3/6] force external uri to be partner center --- .../Webhooks/Invoke-CIPPPartnerWebhookProcessing.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPPartnerWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPPartnerWebhookProcessing.ps1 index 528d0bdf07355..c13c4a18eb526 100644 --- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPPartnerWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPPartnerWebhookProcessing.ps1 @@ -6,7 +6,15 @@ function Invoke-CippPartnerWebhookProcessing { try { if ($Data.AuditUri) { - $AuditLog = New-GraphGetRequest -uri $Data.AuditUri -tenantid $env:TenantID -NoAuthCheck $true -scope 'https://api.partnercenter.microsoft.com/.default' + $ParsedAuditUri = $null + if ([System.Uri]::TryCreate([string]$Data.AuditUri, [System.UriKind]::Absolute, [ref]$ParsedAuditUri) -and + $ParsedAuditUri.Scheme -eq 'https' -and + $ParsedAuditUri.Host -eq 'api.partnercenter.microsoft.com') { + $AuditLog = New-GraphGetRequest -uri $ParsedAuditUri.AbsoluteUri -tenantid $env:TenantID -NoAuthCheck $true -scope 'https://api.partnercenter.microsoft.com/.default' + } else { + Write-LogMessage -API 'Webhooks' -message "Partner Center webhook rejected: AuditUri is not a Partner Center API URL ($($Data.AuditUri))" -Sev 'Alert' + return + } } switch ($Data.EventName) { From 8c4cf8d946a3660ad2434a123a847036ce1e1db5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:35:20 +0200 Subject: [PATCH 4/6] fixes resolver for MCP --- Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 b/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 index 5852d6d966e89..8abadfc81dd88 100644 --- a/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 +++ b/Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1 @@ -155,7 +155,8 @@ function Resolve-CippMcpNode { } if ($Node -is [System.Collections.IEnumerable]) { - return @($Node | ForEach-Object { Resolve-CippMcpNode -Node $_ -Spec $Spec -Depth ($Depth + 1) -Seen $Seen }) + $Resolved = @($Node | ForEach-Object { Resolve-CippMcpNode -Node $_ -Spec $Spec -Depth ($Depth + 1) -Seen $Seen }) + return , $Resolved } return $Node From 32ab1c860ee605f2facec2d2cc3127f6672d522c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:39:25 +0200 Subject: [PATCH 5/6] API HF --- version_latest.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_latest.txt b/version_latest.txt index 04ada833e886e..6842906c6fc24 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.6.1 +10.6.2 From 2da1872186d613926c1f37d0ff5542ada91da904 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:44:28 +0200 Subject: [PATCH 6/6] move snooze to new permission --- .../HTTP Functions/CIPP/Core/Invoke-ExecRemoveSnooze.ps1 | 2 +- .../HTTP Functions/CIPP/Core/Invoke-ExecSnoozeAlert.ps1 | 2 +- .../HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecRemoveSnooze.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecRemoveSnooze.ps1 index 3c9c50adc18fb..506af62287af0 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecRemoveSnooze.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecRemoveSnooze.ps1 @@ -3,7 +3,7 @@ function Invoke-ExecRemoveSnooze { .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - CIPP.Alert.ReadWrite + CIPP.AlertSnooze.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSnoozeAlert.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSnoozeAlert.ps1 index 262bf2fe87b4b..1cc28caaf8a49 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSnoozeAlert.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSnoozeAlert.ps1 @@ -3,7 +3,7 @@ function Invoke-ExecSnoozeAlert { .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - CIPP.Alert.ReadWrite + CIPP.AlertSnooze.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 index e0841c6eeced1..0f689058aa179 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListSnoozedAlerts.ps1 @@ -3,7 +3,7 @@ function Invoke-ListSnoozedAlerts { .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - CIPP.Alert.Read + CIPP.AlertSnooze.Read .DESCRIPTION Lists alerts that have been snoozed (temporarily suppressed), filterable by cmdlet name. Returns snooze duration and scope details. #>