Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions Config/standards.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion Modules/CIPPCore/Public/MCP/Get-CippMcpToolList.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function Set-CIPPAssignedApplication {
$ApplicationId,
$TenantFilter,
$GroupIds,
$AssignmentMode = 'replace',
$AssignmentMode = 'append',
$AssignmentDirection,
$APIName = 'Assign Application',
$Headers,
Expand Down
2 changes: 1 addition & 1 deletion Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function Set-CIPPAssignedPolicy {
$AssignmentFilterType = 'include',
$GroupIds,
$GroupNames,
$AssignmentMode = 'replace',
$AssignmentMode = 'append',
$AssignmentDirection
)

Expand Down
93 changes: 93 additions & 0 deletions Modules/CIPPCore/Public/Set-CIPPRegistrationCampaign.ps1
Original file line number Diff line number Diff line change
@@ -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)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function Invoke-ExecRemoveSnooze {
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
CIPP.Alert.ReadWrite
CIPP.AlertSnooze.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function Invoke-ExecSnoozeAlert {
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
CIPP.Alert.ReadWrite
CIPP.AlertSnooze.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Loading
Loading