From 843ebfb409ac09eaeea6ad5de89853e329ca8bba Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:52:17 +0800 Subject: [PATCH 01/14] Fix mailcontact when values un-set --- .../Invoke-CIPPStandardMailContacts.ps1 | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 index 49e6233afbf8..e2bea7df24a7 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 @@ -46,9 +46,10 @@ function Invoke-CIPPStandardMailContacts { $contacts = $settings $TechAndSecurityContacts = @(@($contacts.SecurityContact, $contacts.TechContact) | Where-Object { $_ } | Select-Object -Unique) - $marketingMatch = @($CurrentInfo.marketingNotificationEmails) -contains $contacts.MarketingContact - $techMatch = -not (Compare-Object @($CurrentInfo.technicalNotificationMails) $TechAndSecurityContacts) - $generalMatch = $CurrentInfo.privacyProfile.contactEmail -eq $contacts.GeneralContact + # If an input value is null/empty, ignore the target tenant's current state and treat it as compliant. + $marketingMatch = [string]::IsNullOrWhiteSpace($contacts.MarketingContact) -or (@($CurrentInfo.marketingNotificationEmails) -contains $contacts.MarketingContact) + $techMatch = $TechAndSecurityContacts.Count -eq 0 -or (-not (Compare-Object @($CurrentInfo.technicalNotificationMails) $TechAndSecurityContacts)) + $generalMatch = [string]::IsNullOrWhiteSpace($contacts.GeneralContact) -or ($CurrentInfo.privacyProfile.contactEmail -eq $contacts.GeneralContact) $state = $marketingMatch -and $techMatch -and $generalMatch @@ -74,7 +75,7 @@ function Invoke-CIPPStandardMailContacts { if ($Settings.alert -eq $true) { - if ($CurrentInfo.marketingNotificationEmails -eq $Contacts.MarketingContact) { + if (!$Contacts.MarketingContact -or $CurrentInfo.marketingNotificationEmails -eq $Contacts.MarketingContact) { Write-LogMessage -API 'Standards' -tenant $tenant -message "Marketing contact email is set to $($Contacts.MarketingContact)" -sev Info } else { $Object = $CurrentInfo | Select-Object marketingNotificationEmails @@ -95,7 +96,7 @@ function Invoke-CIPPStandardMailContacts { Write-StandardsAlert -message "Technical contact email is not set to $($Contacts.TechContact)" -object $Object -tenant $tenant -standardName 'MailContacts' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $tenant -message "Technical contact email is not set to $($Contacts.TechContact)" -sev Info } - if ($CurrentInfo.privacyProfile.contactEmail -eq $Contacts.GeneralContact) { + if (!$Contacts.GeneralContact -or $CurrentInfo.privacyProfile.contactEmail -eq $Contacts.GeneralContact) { Write-LogMessage -API 'Standards' -tenant $tenant -message "General contact email is set to $($Contacts.GeneralContact)" -sev Info } else { $Object = $CurrentInfo | Select-Object privacyProfile @@ -110,10 +111,11 @@ function Invoke-CIPPStandardMailContacts { technicalNotificationMails = @($CurrentInfo.technicalNotificationMails | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false } | Sort-Object -Unique) contactEmail = $CurrentInfo.privacyProfile.contactEmail } + # When an input value is null/empty, mirror the current state so the field is reported as compliant. $ExpectedValue = @{ - marketingNotificationEmails = @($Contacts.MarketingContact | Sort-Object -Unique) - technicalNotificationMails = @(@($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false } | Sort-Object -Unique) - contactEmail = $Contacts.GeneralContact + marketingNotificationEmails = @(if ([string]::IsNullOrWhiteSpace($Contacts.MarketingContact)) { $CurrentValue.marketingNotificationEmails } else { $Contacts.MarketingContact | Sort-Object -Unique }) + technicalNotificationMails = @(if ($TechAndSecurityContacts.Count -eq 0) { $CurrentValue.technicalNotificationMails } else { $TechAndSecurityContacts | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false } | Sort-Object -Unique }) + contactEmail = if ([string]::IsNullOrWhiteSpace($Contacts.GeneralContact)) { $CurrentValue.contactEmail } else { $Contacts.GeneralContact } } Set-CIPPStandardsCompareField -FieldName 'standards.MailContacts' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant Add-CIPPBPAField -FieldName 'MailContacts' -FieldValue $CurrentInfo -StoreAs json -Tenant $tenant From d2e54ee857d41054c49d891398da86e35799eff4 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:52:30 +0800 Subject: [PATCH 02/14] SSO app password policy modification --- .../Authentication/Add-CIPPSSOAppSecret.ps1 | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1 b/Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1 index 7d40ec88282f..ffb9018b0e88 100644 --- a/Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1 @@ -3,16 +3,24 @@ function Add-CIPPSSOAppSecret { .SYNOPSIS Creates a client secret on the CIPP-SSO app registration with retry. .DESCRIPTION - Adds a new password credential to the given app object via Graph. Retries up to - MaxRetries times with backoff because Entra propagation can take a few seconds - after the app is freshly created or its app-management-policy exemption is set. - Throws on final failure so callers can persist Status=error + LastError. + Adds a new password credential to the given app object via Graph. Before adding the + secret it ensures the app is exempt from the tenant default app-management policy (so a + 'passwordAddition' restriction can't block the secret) via Update-AppManagementPolicy, + and honours any 'passwordLifetime' restriction when building the credential body. + Retries up to MaxRetries times with backoff because Entra propagation can take a few + seconds after the app is freshly created or its app-management-policy exemption is set: + replication misses back off 3s, and credential-policy blocks back off min(30, 5*attempt)s + while the exemption propagates. Throws on final failure so callers can persist + Status=error + LastError. .PARAMETER ObjectId Graph object ID of the application (NOT the appId/clientId). + .PARAMETER AppId + AppId/clientId of the application, used to target the app-management-policy exemption. + Resolved from ObjectId when not supplied. .PARAMETER DisplayName Display name to set on the password credential. Defaults to 'CIPP-SSO-Secret'. .PARAMETER MaxRetries - Number of secret-creation attempts before giving up. Defaults to 5. + Number of secret-creation attempts before giving up. Defaults to 6. #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] [CmdletBinding()] @@ -20,32 +28,78 @@ function Add-CIPPSSOAppSecret { [Parameter(Mandatory = $true)] [string]$ObjectId, + [Parameter(Mandatory = $false)] + [string]$AppId, + [Parameter(Mandatory = $false)] [string]$DisplayName = 'CIPP-SSO-Secret', [Parameter(Mandatory = $false)] - [int]$MaxRetries = 5 + [int]$MaxRetries = 6 ) + # Update-AppManagementPolicy targets the app by appId/clientId; resolve it from the object id when not supplied. + if (-not $AppId) { + try { + $SSOApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId`?`$select=id,appId" -NoAuthCheck $true -AsApp $true + $AppId = $SSOApp.appId + } catch { + Write-Warning "[SSO-Secret] Failed to resolve appId for objectId $ObjectId : $($_.Exception.Message)" + } + } + + # Ensure the app is exempt from any credential-addition restriction before adding the secret. + if ($AppId) { + try { + $PolicyUpdate = Update-AppManagementPolicy -ApplicationId $AppId + Write-Information "[SSO-Secret] App management policy: $($PolicyUpdate.PolicyAction)" + } catch { + Write-Information "[SSO-Secret] Failed to update app management policy: $($_.Exception.Message)" + } + } + + # Honour the tenant password-lifetime restriction (if enforced) when building the credential body. + $AppManagementPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/defaultAppManagementPolicy' -AsApp $true -NoAuthCheck $true + $PasswordExpirationPolicy = $AppManagementPolicy.applicationRestrictions.passwordcredentials | + Where-Object { $_.restrictionType -eq 'passwordLifetime' } + if (-not ($PasswordExpirationPolicy.state -eq 'disabled' -or $null -eq $PasswordExpirationPolicy.state)) { + $TimeToExpiration = [System.Xml.XmlConvert]::ToTimeSpan($PasswordExpirationPolicy.maxLifetime) + $ExpirationDate = (Get-Date).AddDays($TimeToExpiration.Days).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + $PasswordBody = "{`"passwordCredential`":{`"displayName`":`"$DisplayName`",`"endDateTime`":`"$ExpirationDate`"}}" + } else { + $PasswordBody = "{`"passwordCredential`":{`"displayName`":`"$DisplayName`"}}" + } + $SecretText = $null - $SecretAttempt = 0 - $BackoffSchedule = @(2, 5, 10, 15, 30) $LastException = $null - - while ($SecretAttempt -lt $MaxRetries -and -not $SecretText) { + for ($Attempt = 1; $Attempt -le $MaxRetries; $Attempt++) { try { - $PasswordBody = @{ passwordCredential = @{ displayName = $DisplayName } } | ConvertTo-Json -Compress - $PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true + $PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId/addPassword" -AsApp $true -NoAuthCheck $true -type POST -body $PasswordBody -maxRetries 3 $SecretText = $PasswordResult.secretText Write-Information "[SSO-Secret] Client secret created on objectId $ObjectId" + break } catch { - $SecretAttempt++ $LastException = $_ - Write-Warning "[SSO-Secret] Secret creation attempt $SecretAttempt/$MaxRetries failed: $($_.Exception.Message)" - if ($SecretAttempt -lt $MaxRetries) { - $Delay = $BackoffSchedule[[Math]::Min($SecretAttempt - 1, $BackoffSchedule.Count - 1)] - Start-Sleep -Seconds $Delay + $ExceptionMessage = $_.Exception.Message + $IsNotReplicatedYet = $ExceptionMessage -match "Resource '.*' does not exist or one of its queried reference-property objects are not present" + $IsCredentialPolicyBlocked = $ExceptionMessage -match 'Credential type not allowed as per assigned policy' + Write-Warning "[SSO-Secret] Secret creation attempt $Attempt/$MaxRetries failed: $ExceptionMessage" + + if ($IsNotReplicatedYet -and $Attempt -lt $MaxRetries) { + $DelaySeconds = 3 + Write-Information "[SSO-Secret] Application object not yet replicated for addPassword (attempt $Attempt of $MaxRetries). Retrying in $DelaySeconds second(s)." + Start-Sleep -Seconds $DelaySeconds + continue + } + + if ($IsCredentialPolicyBlocked -and $Attempt -lt $MaxRetries) { + $DelaySeconds = [Math]::Min(30, 5 * $Attempt) + Write-Information "[SSO-Secret] Credential policy still blocks addPassword (attempt $Attempt of $MaxRetries). Waiting for policy propagation and retrying in $DelaySeconds second(s)." + Start-Sleep -Seconds $DelaySeconds + continue } + + throw } } From d2655de84a26a7202ea5702091eab002f415191c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:05:15 +0800 Subject: [PATCH 03/14] github template id sync fixes --- .../Public/Tools/Import-CommunityTemplate.ps1 | 49 ++++++++++++------- .../MEM/Invoke-ListIntuneTemplates.ps1 | 11 ++++- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 index 56d3c5843e44..48e7a2fa2bd2 100644 --- a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 +++ b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 @@ -84,15 +84,6 @@ function Import-CommunityTemplate { switch -Wildcard ($Type) { '*Group*' { - $RawJsonObj = [PSCustomObject]@{ - Displayname = $Template.displayName - Description = $Template.Description - MembershipRules = $Template.membershipRule - username = $Template.mailNickname - GUID = $id - groupType = 'generic' - } | ConvertTo-Json -Depth 100 - # Check for duplicate template $DuplicateFilter = "PartitionKey eq 'GroupTemplate'" $ExistingTemplates = Get-CIPPAzDataTableEntity @Table -Filter $DuplicateFilter -ErrorAction SilentlyContinue @@ -115,6 +106,19 @@ function Import-CommunityTemplate { break } + # On update, reuse the existing GUID so the JSON-embedded GUID stays in + # sync with the table RowKey (see the Intune path below for the full rationale). + $TemplateGuid = if ($Duplicate) { $Duplicate.GUID } else { $id } + + $RawJsonObj = [PSCustomObject]@{ + Displayname = $Template.displayName + Description = $Template.Description + MembershipRules = $Template.membershipRule + username = $Template.mailNickname + GUID = $TemplateGuid + groupType = 'generic' + } | ConvertTo-Json -Depth 100 + if ($Duplicate) { $StatusMessage = "Updating Group template '$($Template.displayName)' from source '$Source' (SHA changed)." Write-Information $StatusMessage @@ -126,7 +130,7 @@ function Import-CommunityTemplate { JSON = "$RawJsonObj" PartitionKey = 'GroupTemplate' SHA = $SHA - GUID = if ($Duplicate) { $Duplicate.GUID } else { $id } + GUID = $TemplateGuid RowKey = if ($Duplicate) { $Duplicate.RowKey } else { $id } Source = $Source } @@ -239,14 +243,6 @@ function Import-CommunityTemplate { #create a new template $DisplayName = $Template.displayName ?? $template.Name - $RawJsonObj = [PSCustomObject]@{ - Displayname = $DisplayName - Description = $Template.Description - RAWJson = $RawJson - Type = $URLName - GUID = $id - } | ConvertTo-Json -Depth 100 -Compress - # Check for duplicate template $DuplicateFilter = "PartitionKey eq 'IntuneTemplate'" $ExistingTemplates = Get-CIPPAzDataTableEntity @Table -Filter $DuplicateFilter -ErrorAction SilentlyContinue @@ -263,6 +259,21 @@ function Import-CommunityTemplate { } } | Select-Object -First 1 + # On update, reuse the existing template's GUID so the GUID embedded + # in the JSON blob stays in sync with the table RowKey. Minting a fresh + # GUID here desyncs the two: the standards engine resolves templates by + # RowKey, while the template picker surfaces the JSON GUID, so the drift + # would point at a GUID that no longer matches any RowKey. + $TemplateGuid = if ($Duplicate) { $Duplicate.GUID } else { $id } + + $RawJsonObj = [PSCustomObject]@{ + Displayname = $DisplayName + Description = $Template.Description + RAWJson = $RawJson + Type = $URLName + GUID = $TemplateGuid + } | ConvertTo-Json -Depth 100 -Compress + if ($Duplicate -and $Duplicate.SHA -eq $SHA -and -not $Force) { $StatusMessage = "Intune template '$DisplayName' from source '$Source' is already up to date. Skipping import." Write-Information $StatusMessage @@ -280,7 +291,7 @@ function Import-CommunityTemplate { JSON = "$RawJsonObj" PartitionKey = 'IntuneTemplate' SHA = $SHA - GUID = if ($Duplicate) { $Duplicate.GUID } else { $id } + GUID = $TemplateGuid RowKey = if ($Duplicate) { $Duplicate.RowKey } else { $id } Source = $Source } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 index 4944d2f2f7cb..f258a21892b1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 @@ -137,7 +137,16 @@ function Invoke-ListIntuneTemplates { } } | Sort-Object -Property label) } else { - $Templates = $RawTemplates.JSON | ForEach-Object { try { ConvertFrom-Json -InputObject $_ -Depth 100 -ErrorAction SilentlyContinue } catch {} } + # Force GUID to the table RowKey (the authoritative key the standards engine + # resolves against). The JSON-embedded GUID can drift out of sync with the + # RowKey after a community-repo re-sync, so never surface it as the selectable value. + $Templates = $RawTemplates | ForEach-Object { + try { + $Parsed = ConvertFrom-Json -InputObject $_.JSON -Depth 100 -ErrorAction SilentlyContinue + if ($Parsed) { $Parsed | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force } + $Parsed + } catch {} + } } } From 8e21ee1f87ad9e6f106159cd7864f3573420ee8d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:38:08 +0800 Subject: [PATCH 04/14] Fixes for when duplicate intune standards are applied to the same tenant --- .../Public/Standards/Merge-CippStandards.ps1 | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 b/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 index dcea014c0def..7f231faf55ff 100644 --- a/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 @@ -11,14 +11,34 @@ function Merge-CippStandards { # If the standard name ends with 'Template', we treat them as arrays to merge. if ($StandardName -like '*Template') { - $ExistingIsArray = $Existing -is [System.Collections.IEnumerable] -and -not ($Existing -is [string]) - $NewIsArray = $New -is [System.Collections.IEnumerable] -and -not ($New -is [string]) + # Combine both tiers, then collapse duplicates that target the same template + # (same TemplateList.value). Without this, the same Intune/CA template configured + # in more than one tier (or in more than one standard) for a tenant gets + # concatenated into a multi-element array, which downstream stringifies into a + # doubled GUID ("Failed to find template ") that matches no RowKey. + # + # The standards engine already keys each template instance by TemplateList.value, + # so when this function runs the items share a template GUID and should resolve to + # a single deployment. Items without a TemplateList.value can't be keyed, so they + # are always kept (preserves the additive behaviour for those). + $Combined = @($Existing) + @($New) - # Make sure both are arrays - if (-not $ExistingIsArray) { $Existing = @($Existing) } - if (-not $NewIsArray) { $New = @($New) } + $Deduped = [System.Collections.Generic.List[object]]::new() + $SeenValues = [System.Collections.Generic.HashSet[string]]::new() + # Walk newest-first so the most-specific tier wins for a given template, while + # Insert(0, ...) keeps the overall ordering stable. + for ($i = $Combined.Count - 1; $i -ge 0; $i--) { + $Item = $Combined[$i] + $TemplateValue = $Item.TemplateList.value + if ([string]::IsNullOrEmpty($TemplateValue)) { + $Deduped.Insert(0, $Item) + } elseif ($SeenValues.Add([string]$TemplateValue)) { + $Deduped.Insert(0, $Item) + } + } - return $Existing + $New + if ($Deduped.Count -eq 1) { return $Deduped[0] } + return $Deduped.ToArray() } else { # Single‐value standard: override the old with the new return $New From 444a746f9c883f975dac55d5aced76fbe4afe3db Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:14:41 +0800 Subject: [PATCH 05/14] emit message when mailbox conversion might hit 50gb limit --- .../CIPPCore/Public/Set-CIPPMailboxType.ps1 | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxType.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxType.ps1 index ed8fdd7060e8..f575d9fc0241 100644 --- a/Modules/CIPPCore/Public/Set-CIPPMailboxType.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPMailboxType.ps1 @@ -14,6 +14,35 @@ function Set-CIPPMailboxType { if ([string]::IsNullOrWhiteSpace($Username)) { $Username = $UserID } $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Mailbox' -cmdParams @{Identity = $UserID; Type = $MailboxType } -Anchor $Username $Message = "Successfully converted $Username to a $MailboxType mailbox" + + # When converting to a shared mailbox, surface the cached mailbox size if it exceeds the + # unlicensed shared-mailbox limit (50 GiB; we warn at 49 GiB). This is best-effort: any + # lookup failure or unexpected response shape falls through to the standard success message. + if ($MailboxType -eq 'Shared') { + try { + # 49 GiB warning threshold (shared mailboxes are capped at 50 GiB without a license) + $SharedMailboxWarnBytes = 49GB + # Resolve the partition key (defaultDomainName) the reporting DB is keyed on + $PartitionKey = (Get-Tenants -TenantFilter $TenantFilter).defaultDomainName + if ($PartitionKey) { + # Server-side point lookup for this specific mailbox only. + # Cached mailbox rows are keyed RowKey = 'Mailboxes-'. + $Table = Get-CippTable -tablename 'CippReportingDB' + $Filter = "PartitionKey eq '{0}' and RowKey eq 'Mailboxes-{1}'" -f $PartitionKey, $UserID + $CachedMailbox = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Select-Object -First 1 + if ($CachedMailbox.Data) { + $StorageBytes = [int64]([string]($CachedMailbox.Data | ConvertFrom-Json).storageUsedInBytes) + if ($StorageBytes -ge $SharedMailboxWarnBytes) { + $StorageGB = [math]::Round($StorageBytes / 1GB, 1) + $Message = "$Message. Warning: detected mailbox size is $StorageGB GB, which exceeds the 50 GB shared mailbox limit. The mailbox may stop receiving mail unless an Exchange Online Plan 2 license is retained." + } + } + } + } catch { + # Best-effort size check only; ignore lookup/parse errors and return the standard message. + } + } + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' -tenant $TenantFilter return $Message } catch { From e2b092bdd87def82904e846c693a167b45badf74 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:08:51 +0800 Subject: [PATCH 06/14] update json from frontend --- Config/standards.json | 4490 ++++++++++++++++++++++++++++------------- 1 file changed, 3140 insertions(+), 1350 deletions(-) diff --git a/Config/standards.json b/Config/standards.json index 1e2ceba142dc..a698010ce141 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -1,4 +1,113 @@ [ + { + "name": "standards.CopilotSettings", + "cat": "Copilot (M365) Standards", + "tag": [], + "helpText": "Configures Microsoft 365 Copilot tenant policy settings: Copilot Chat pinning, blocking Copilot access to open content, Designer image generation, web search, and admin-center Copilot. Each setting can be left unconfigured, enabled, or disabled. These settings are managed through the Copilot policy service (Cloud Policy / Intune) and are applied at the tenant level.", + "docsDescription": "Manages Microsoft 365 Copilot admin policy settings via the `/copilot/admin/policySettings` Microsoft Graph API (beta). Each of the five supported settings can be independently set or left unmanaged using the \"Do not configure\" option. NOTE: this API currently requires delegated authentication and supports only tenant-level policies; settings scoped to group-level policies return an error and are skipped. The exact accepted value per setting is a string (commonly \"1\"/\"0\") and should be validated against a Copilot-licensed tenant.", + "executiveText": "Provides centralized governance of Microsoft 365 Copilot capabilities across the organization. Administrators can control whether Copilot Chat is pinned for users, whether Copilot can access open files, and whether features such as image generation and web search are available, helping balance employee productivity with data governance and compliance requirements.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Pin Microsoft 365 Copilot Chat", + "name": "standards.CopilotSettings.copilotChatPinning", + "options": [ + { "label": "Do not configure", "value": "donotconfigure" }, + { "label": "Enabled", "value": "1" }, + { "label": "Disabled", "value": "0" } + ] + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Copilot Access to Open Content", + "name": "standards.CopilotSettings.blockAccessToOpenFiles", + "options": [ + { "label": "Do not configure", "value": "donotconfigure" }, + { "label": "Block open content", "value": "1" }, + { "label": "Allow open content", "value": "0" } + ] + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Designer Image Generation", + "name": "standards.CopilotSettings.imageGeneration", + "options": [ + { "label": "Do not configure", "value": "donotconfigure" }, + { "label": "Enabled", "value": "1" }, + { "label": "Disabled", "value": "0" } + ] + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Web Search in Copilot", + "name": "standards.CopilotSettings.allowWebSearch", + "options": [ + { "label": "Do not configure", "value": "donotconfigure" }, + { "label": "Enabled", "value": "1" }, + { "label": "Disabled", "value": "0" } + ] + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Admin Copilot in Microsoft 365 Admin Center", + "name": "standards.CopilotSettings.allowInAdminCenters", + "options": [ + { "label": "Do not configure", "value": "donotconfigure" }, + { "label": "Enabled", "value": "1" }, + { "label": "Disabled", "value": "0" } + ] + } + ], + "label": "Configure Microsoft 365 Copilot policy settings", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-06-09", + "powershellEquivalent": "Graph API: PATCH /beta/copilot/admin/policySettings/{id}", + "recommendedBy": [] + }, + { + "name": "standards.CopilotLimitedMode", + "cat": "Copilot (M365) Standards", + "tag": [], + "helpText": "Controls Microsoft 365 Copilot Limited Mode for Teams meetings. When enabled for a group, Copilot in Teams meetings does not respond to sentiment-related prompts (inferring emotions, behavior, or judgments) for members of the selected group. A target group is required when enabling. Managed via the Copilot admin settings Graph API.", + "docsDescription": "Configures the `copilotAdminLimitedMode` setting through the `/copilot/admin/settings/limitedMode` Microsoft Graph API (beta). When enabled, `isEnabledForGroup` is set to true and applied to the resolved target group; when disabled, `isEnabledForGroup` is set to false. NOTE: this API currently requires delegated authentication and the acting identity must be Global Administrator to write the setting.", + "executiveText": "Limits Microsoft 365 Copilot in Teams meetings so it does not provide opinions on sentiment, emotions, or judgments for a selected group of users. This helps organizations meet workplace policy, privacy, and works-council requirements while still allowing Copilot to summarize and answer factual questions grounded in the meeting.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.CopilotLimitedMode.LimitedModeEnabled", + "label": "Enable Copilot Limited Mode for a group (Teams meetings)", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.CopilotLimitedMode.GroupName", + "label": "Target Group Name (wildcard match; required when enabled)", + "required": false, + "condition": { + "field": "standards.CopilotLimitedMode.LimitedModeEnabled", + "compareType": "is", + "compareValue": true + } + } + ], + "label": "Configure Microsoft 365 Copilot Limited Mode (Teams meetings)", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-06-09", + "powershellEquivalent": "Graph API: PATCH /beta/copilot/admin/settings/limitedMode", + "recommendedBy": [] + }, { "name": "standards.MailContacts", "cat": "Global Standards", @@ -77,7 +186,14 @@ "impactColour": "info", "addedDate": "2024-03-19", "powershellEquivalent": "New-MailContact", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.DeployContactTemplates", @@ -102,27 +218,25 @@ } ], "label": "Deploy Mail Contact Template", - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Low Impact", "impactColour": "info", "addedDate": "2025-05-31", "powershellEquivalent": "New-MailContact", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.AuditLog", "cat": "Global Standards", - "tag": [ - "CIS M365 5.0 (3.1.1)", - "mip_search_auditlog", - "NIST CSF 2.0 (DE.CM-09)", - "CISAMSEXO171", - "CISAMSEXO173" - ], + "tag": ["CIS M365 7.0.0 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"], + "appliesToTest": ["CISAMSEXO171", "CISAMSEXO173", "CIS_3_1_1"], "helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.", "executiveText": "Activates comprehensive activity logging across Microsoft 365 services to track user actions, system changes, and security events. This provides essential audit trails for compliance requirements, security investigations, and regulatory reporting.", "addedComponent": [], @@ -131,12 +245,20 @@ "impactColour": "info", "addedDate": "2021-11-16", "powershellEquivalent": "Enable-OrganizationCustomization", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.RestrictThirdPartyStorageServices", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (1.3.7)"], + "tag": ["CIS M365 7.0.0 (1.3.7)"], + "appliesToTest": ["CIS_1_3_7"], "helpText": "Restricts third-party storage services in Microsoft 365 on the web by managing the Microsoft 365 on the web service principal. This disables integrations with services like Dropbox, Google Drive, Box, and other third-party storage providers.", "docsDescription": "Third-party storage can be enabled for users in Microsoft 365, allowing them to store and share documents using services such as Dropbox, alongside OneDrive and team sites. This standard ensures Microsoft 365 on the web third-party storage services are restricted by creating and disabling the Microsoft 365 on the web service principal (appId: c1f33bc0-bdb4-4248-ba9b-096807ddb43e). By using external storage services an organization may increase the risk of data breaches and unauthorized access to confidential information. Additionally, third-party services may not adhere to the same security standards as the organization, making it difficult to maintain data privacy and security. Impact is highly dependent upon current practices - if users do not use other storage providers, then minimal impact is likely. However, if users regularly utilize providers outside of the tenant this will affect their ability to continue to do so.", "executiveText": "Prevents employees from using external cloud storage services like Dropbox, Google Drive, and Box within Microsoft 365, reducing data security risks and ensuring all company data remains within controlled corporate systems. This helps maintain data governance and prevents potential data leaks to unauthorized platforms.", @@ -146,7 +268,15 @@ "impactColour": "warning", "addedDate": "2025-06-06", "powershellEquivalent": "New-MgServicePrincipal and Update-MgServicePrincipal", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.ProfilePhotos", @@ -163,14 +293,8 @@ "label": "Select value", "name": "standards.ProfilePhotos.state", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] } ], @@ -179,7 +303,14 @@ "impactColour": "info", "addedDate": "2025-01-19", "powershellEquivalent": "Set-OrganizationConfig -ProfilePhotoOptions EnablePhotos and Update-MgBetaAdminPeople", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.PhishProtection", @@ -192,13 +323,10 @@ "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-01-22", - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, + "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "powershellEquivalent": "Portal only", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2", "OFFICE_BUSINESS"] }, { "name": "standards.Branding", @@ -230,38 +358,26 @@ "label": "Visual Template", "name": "standards.Branding.layoutTemplateType", "options": [ - { - "label": "Full-screen background", - "value": "default" - }, - { - "label": "Partial-screen background", - "value": "verticalSplit" - } + { "label": "Full-screen background", "value": "default" }, + { "label": "Partial-screen background", "value": "verticalSplit" } ] }, - { - "type": "switch", - "name": "standards.Branding.isHeaderShown", - "label": "Show header" - }, - { - "type": "switch", - "name": "standards.Branding.isFooterShown", - "label": "Show footer" - } + { "type": "switch", "name": "standards.Branding.isHeaderShown", "label": "Show header" }, + { "type": "switch", "name": "standards.Branding.isFooterShown", "label": "Show footer" } ], "label": "Set branding for the tenant", "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-05-13", "powershellEquivalent": "Portal only", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2", "OFFICE_BUSINESS"] }, { "name": "standards.EnableCustomerLockbox", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (1.3.6)", "CustomerLockBoxEnabled"], + "tag": ["CIS M365 7.0.0 (1.3.6)", "CustomerLockBoxEnabled"], + "appliesToTest": ["CIS_1_3_6"], "helpText": "**Requires Entra ID P2.** Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data", "docsDescription": "**Requires Entra ID P2.** Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.", "executiveText": "Requires explicit organizational approval before Microsoft support staff can access company data for service operations. This provides an additional layer of data protection and ensures the organization maintains control over who can access sensitive business information, even during technical support scenarios.", @@ -271,7 +387,8 @@ "impactColour": "info", "addedDate": "2024-01-08", "powershellEquivalent": "Set-OrganizationConfig -CustomerLockBoxEnabled $true", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": ["CustomerLockbox"] }, { "name": "standards.EnablePronouns", @@ -320,15 +437,22 @@ "name": "standards.DisableGuestDirectory", "cat": "Global Standards", "tag": [ - "CIS M365 5.0 (5.1.6.2)", + "CIS M365 7.0.0 (5.1.6.2)", "CISA (MS.AAD.5.1v1)", "EIDSCA.AP14", "EIDSCA.ST08", "EIDSCA.ST09", "NIST CSF 2.0 (PR.AA-05)", + "SMB1001 (2.8)" + ], + "appliesToTest": [ + "CIS_5_1_6_2", "EIDSCAAP07", + "EIDSCAAP14", "EIDSCAST08", - "EIDSCAST09" + "EIDSCAST09", + "SMB1001_2_8", + "ZTNA21792" ], "helpText": "Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory.", "docsDescription": "Sets it so guests can view only their own user profile. Permission to view other users isn't allowed. Also restricts guest users from seeing the membership of groups they're in. See exactly what get locked down in the [Microsoft documentation.](https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions)", @@ -344,12 +468,8 @@ { "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", - "tag": [ - "CIS M365 5.0 (6.5.4)", - "NIST CSF 2.0 (PR.IR-01)", - "ZTNA21799", - "CISAMSEXO51" - ], + "tag": ["CIS M365 7.0.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], + "appliesToTest": ["CISAMSEXO51", "CIS_6_5_4", "ZTNA21799"], "helpText": "Disables SMTP AUTH organization-wide, impacting POP and IMAP clients that rely on SMTP for sending emails. Default for new tenants. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)", "docsDescription": "Disables tenant-wide SMTP basic authentication, including for all explicitly enabled users, impacting POP and IMAP clients that rely on SMTP for sending emails. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).", "executiveText": "Disables outdated email authentication methods that are vulnerable to security attacks, forcing applications and devices to use modern, more secure authentication protocols. This reduces the risk of email-based security breaches and credential theft.", @@ -359,19 +479,20 @@ "impactColour": "warning", "addedDate": "2021-11-16", "powershellEquivalent": "Set-TransportConfig -SmtpClientAuthenticationDisabled $true", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.ActivityBasedTimeout", "cat": "Global Standards", - "tag": [ - "CIS M365 5.0 (1.3.2)", - "spo_idle_session_timeout", - "NIST CSF 2.0 (PR.AA-03)", - "ZTNA21813", - "ZTNA21814", - "ZTNA21815" - ], + "tag": ["CIS M365 7.0.0 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"], + "appliesToTest": ["CIS_1_3_2", "ZTNA21813", "ZTNA21814", "ZTNA21815"], "helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps", "executiveText": "Automatically logs out inactive users from Microsoft 365 applications after a specified time period to prevent unauthorized access to company data on unattended devices. This security measure protects against data breaches when employees leave workstations unlocked.", "addedComponent": [ @@ -382,26 +503,11 @@ "label": "Select value", "name": "standards.ActivityBasedTimeout.timeout", "options": [ - { - "label": "1 Hour", - "value": "01:00:00" - }, - { - "label": "3 Hours", - "value": "03:00:00" - }, - { - "label": "6 Hours", - "value": "06:00:00" - }, - { - "label": "12 Hours", - "value": "12:00:00" - }, - { - "label": "24 Hours", - "value": "1.00:00:00" - } + { "label": "1 Hour", "value": "01:00:00" }, + { "label": "3 Hours", "value": "03:00:00" }, + { "label": "6 Hours", "value": "06:00:00" }, + { "label": "12 Hours", "value": "12:00:00" }, + { "label": "24 Hours", "value": "1.00:00:00" } ] } ], @@ -416,11 +522,19 @@ "name": "standards.AuthMethodsSettings", "cat": "Entra (AAD) Standards", "tag": [ + "CIS M365 7.0.0 (5.2.3.6)", "EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", + "SMB1001 (2.8)" + ], + "appliesToTest": [ + "CIS_5_2_3_6", + "EIDSCAAG01", "EIDSCAAG02", - "EIDSCAAG03" + "EIDSCAAG03", + "SMB1001_2_8", + "ZTNA21841" ], "helpText": "Configures the report suspicious activity settings and system credential preferences in the authentication methods policy.", "docsDescription": "Controls the authentication methods policy settings for reporting suspicious activity and system credential preferences. These settings help enhance the security of authentication in your organization.", @@ -434,18 +548,9 @@ "name": "standards.AuthMethodsSettings.ReportSuspiciousActivity", "label": "Report Suspicious Activity Settings", "options": [ - { - "label": "Microsoft managed", - "value": "default" - }, - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] }, { @@ -456,18 +561,9 @@ "name": "standards.AuthMethodsSettings.SystemCredential", "label": "System Credential Preferences", "options": [ - { - "label": "Microsoft managed", - "value": "default" - }, - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] } ], @@ -479,79 +575,447 @@ "recommendedBy": [] }, { - "name": "standards.AuthMethodsPolicyMigration", - "cat": "Entra (AAD) Standards", - "tag": ["EIDSCAAG01"], - "helpText": "Completes the migration of authentication methods policy to the new format", - "docsDescription": "Sets the authentication methods policy migration state to complete. This is required when migrating from legacy authentication policies to the new unified authentication methods policy.", - "executiveText": "Completes the transition from legacy authentication policies to Microsoft's modern unified authentication methods policy, ensuring the organization benefits from the latest security features and management capabilities. This migration enables enhanced security controls and simplified policy management.", - "addedComponent": [], - "label": "Complete Authentication Methods Policy Migration", - "impact": "Medium Impact", - "impactColour": "warning", - "addedDate": "2025-07-07", - "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicy", - "recommendedBy": ["CIPP"] - }, - { - "name": "standards.AppDeploy", + "name": "standards.AuthenticationMethods", "cat": "Entra (AAD) Standards", "tag": [], - "helpText": "Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application.", - "docsDescription": "Uses the CIPP functionality that deploys applications across an entire tenant base as a standard.", - "executiveText": "Automatically deploys approved business applications across all company locations and users, ensuring consistent access to essential tools and maintaining standardized software configurations. This streamlines application management and reduces IT deployment overhead.", + "helpText": "Configures all authentication methods for the tenant including Microsoft Authenticator, FIDO2, SMS, Voice, Email OTP, Temporary Access Pass, Software OATH, Hardware OATH, Certificate-based, and QR Code Pin. Enable or disable each method and optionally target specific groups.", + "docsDescription": "Unified standard to configure all authentication method policies in a single place. Each method can be independently enabled or disabled, targeted to all users or specific groups using group name wildcards, and configured with method-specific settings such as TAP lifetime, QR code pin length, and Authenticator software OTP.", + "executiveText": "Provides centralized control over all tenant authentication methods from a single standard. Administrators can enable phishing-resistant methods like FIDO2 and Microsoft Authenticator while disabling less secure options like SMS and Voice. Each method supports group-level targeting using wildcard group names, allowing staged rollouts and granular control.", "addedComponent": [ { - "type": "select", + "type": "switch", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "label": "Microsoft Authenticator", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorSoftwareOath", + "label": "Enable Software OTP in Authenticator", + "defaultValue": false, + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", "multiple": false, "creatable": false, - "label": "App Approval Mode", - "name": "standards.AppDeploy.mode", + "label": "Number Matching", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorNumberMatching", "options": [ - { - "label": "Template", - "value": "template" - }, - { - "label": "Copy Permissions", - "value": "copy" - } - ] + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ], + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } }, { "type": "autoComplete", - "multiple": true, + "multiple": false, "creatable": false, - "label": "Select Applications", - "name": "standards.AppDeploy.templateIds", - "api": { - "url": "/api/ListAppApprovalTemplates", - "labelField": "TemplateName", - "valueField": "TemplateId", - "queryKey": "StdAppApprovalTemplateList", - "addedField": { - "AppId": "AppId" - } - }, + "label": "Show Application Name in Push Notifications", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorDisplayAppInfo", + "options": [ + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ], "condition": { - "field": "standards.AppDeploy.mode", + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", "compareType": "is", - "compareValue": "template" + "compareValue": true + } + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Show Geographic Location in Push Notifications", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorDisplayLocation", + "options": [ + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ], + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Companion App (Authenticator Lite)", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorCompanionApp", + "options": [ + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ], + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true } }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.FIDO2Enabled", + "label": "FIDO2 Security Keys", + "defaultValue": false + }, { "type": "textField", - "name": "standards.AppDeploy.appids", - "label": "Application IDs, comma separated", + "name": "standards.AuthenticationMethods.FIDO2Group", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, "condition": { - "field": "standards.AppDeploy.mode", - "compareType": "isNot", - "compareValue": "template" + "field": "standards.AuthenticationMethods.FIDO2Enabled", + "compareType": "is", + "compareValue": true } - } - ], - "label": "Deploy Application", - "impact": "Low Impact", + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.TAPEnabled", + "label": "Temporary Access Pass", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.TAPGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "TAP Usage Mode", + "name": "standards.AuthenticationMethods.TAPUsableOnce", + "options": [ + { "label": "Only Once", "value": "true" }, + { "label": "Multiple Logons", "value": "false" } + ], + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.TAPDefaultLifetime", + "label": "TAP Default Lifetime (minutes)", + "defaultValue": 60, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.TAPMinLifetime", + "label": "TAP Minimum Lifetime (minutes)", + "defaultValue": 60, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.TAPMaxLifetime", + "label": "TAP Maximum Lifetime (minutes)", + "defaultValue": 480, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.TAPDefaultLength", + "label": "TAP Length (characters)", + "defaultValue": 8, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.SoftwareOathEnabled", + "label": "Third-Party Software OATH Tokens", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.SoftwareOathGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.SoftwareOathEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.HardwareOathEnabled", + "label": "Hardware OATH Tokens", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.HardwareOathGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.HardwareOathEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.SMSEnabled", + "label": "SMS", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.SMSGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.SMSEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.VoiceEnabled", + "label": "Voice Call", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.VoiceGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.VoiceEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.EmailEnabled", + "label": "Email OTP", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.EmailGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.EmailEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.x509CertificateEnabled", + "label": "Certificate-Based Authentication", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.x509CertificateGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.x509CertificateEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.QRCodePinEnabled", + "label": "QR Code Pin", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.QRCodePinGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.QRCodePinEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.QRCodeLifetimeInDays", + "label": "QR Code Lifetime (days, 1-395)", + "defaultValue": 365, + "condition": { + "field": "standards.AuthenticationMethods.QRCodePinEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.QRCodePinLength", + "label": "QR Code PIN Length (8-20)", + "defaultValue": 8, + "condition": { + "field": "standards.AuthenticationMethods.QRCodePinEnabled", + "compareType": "is", + "compareValue": true + } + } + ], + "label": "Configure Authentication Methods", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-05-28", + "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", + "recommendedBy": ["CIPP"] + }, + { + "name": "standards.AdminSSPR", + "cat": "Entra (AAD) Standards", + "tag": ["EIDSCA.AP01"], + "appliesToTest": ["EIDSCAAP01", "ZTNA21842"], + "helpText": "Controls whether administrators are allowed to use Self-Service Password Reset through the Microsoft Entra authorization policy.", + "docsDescription": "Configures the allowedToUseSSPR property on the Microsoft Entra authorization policy. Microsoft documents this property as controlling whether administrators of the tenant can use Self-Service Password Reset. Use this standard to explicitly enable or disable administrator SSPR based on your security policy.", + "executiveText": "Controls whether tenant administrators can reset their own passwords through Self-Service Password Reset. Disabling this capability forces privileged accounts through more controlled recovery processes and reduces the risk of self-service recovery being misused on administrative identities.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Select value", + "name": "standards.AdminSSPR.state", + "options": [ + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ] + } + ], + "label": "Set administrator Self-Service Password Reset state", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-04-21", + "powershellEquivalent": "Update-MgBetaPolicyAuthorizationPolicy", + "recommendedBy": ["CIPP"] + }, + { + "name": "standards.AuthMethodsPolicyMigration", + "cat": "Entra (AAD) Standards", + "tag": [], + "appliesToTest": ["EIDSCAAG01"], + "helpText": "Completes the migration of authentication methods policy to the new format", + "docsDescription": "Sets the authentication methods policy migration state to complete. This is required when migrating from legacy authentication policies to the new unified authentication methods policy.", + "executiveText": "Completes the transition from legacy authentication policies to Microsoft's modern unified authentication methods policy, ensuring the organization benefits from the latest security features and management capabilities. This migration enables enhanced security controls and simplified policy management.", + "addedComponent": [], + "label": "Complete Authentication Methods Policy Migration", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-07-07", + "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicy", + "recommendedBy": ["CIPP"] + }, + { + "name": "standards.AppDeploy", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application.", + "docsDescription": "Uses the CIPP functionality that deploys applications across an entire tenant base as a standard.", + "executiveText": "Automatically deploys approved business applications across all company locations and users, ensuring consistent access to essential tools and maintaining standardized software configurations. This streamlines application management and reduces IT deployment overhead.", + "addedComponent": [ + { + "type": "select", + "multiple": false, + "creatable": false, + "label": "App Approval Mode", + "name": "standards.AppDeploy.mode", + "options": [ + { "label": "Template", "value": "template" }, + { "label": "Copy Permissions", "value": "copy" } + ] + }, + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "label": "Select Applications", + "name": "standards.AppDeploy.templateIds", + "api": { + "url": "/api/ListAppApprovalTemplates", + "labelField": "TemplateName", + "valueField": "TemplateId", + "queryKey": "StdAppApprovalTemplateList", + "addedField": { "AppId": "AppId" } + }, + "condition": { + "field": "standards.AppDeploy.mode", + "compareType": "is", + "compareValue": "template" + } + }, + { + "type": "textField", + "name": "standards.AppDeploy.appids", + "label": "Application IDs, comma separated", + "condition": { + "field": "standards.AppDeploy.mode", + "compareType": "isNot", + "compareValue": "template" + } + } + ], + "label": "Deploy Application", + "impact": "Low Impact", "impactColour": "info", "addedDate": "2024-07-07", "powershellEquivalent": "Portal or Graph API", @@ -560,7 +1024,8 @@ { "name": "standards.laps", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21953", "ZTNA21955", "ZTNA24560"], + "tag": ["CIS M365 7.0.0 (5.1.4.5)", "SMB1001 (2.2)"], + "appliesToTest": ["CIS_5_1_4_5", "SMB1001_2_2", "ZTNA21953", "ZTNA21955", "ZTNA24560"], "helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.", "docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.", "executiveText": "Enables Local Administrator Password Solution (LAPS) capability, which automatically manages and rotates local administrator passwords on company computers. This significantly improves security by preventing the use of shared or static administrator passwords that could be exploited by attackers.", @@ -576,14 +1041,17 @@ "name": "standards.PWdisplayAppInformationRequiredState", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 5.0 (2.3.1)", + "CIS M365 7.0.0 (5.2.3.1)", "EIDSCA.AM03", "EIDSCA.AM04", "EIDSCA.AM06", "EIDSCA.AM07", "EIDSCA.AM09", "EIDSCA.AM10", - "NIST CSF 2.0 (PR.AA-03)", + "NIST CSF 2.0 (PR.AA-03)" + ], + "appliesToTest": [ + "CIS_5_2_3_1", "EIDSCAAM01", "EIDSCAAM03", "EIDSCAAM04", @@ -606,7 +1074,8 @@ { "name": "standards.allowOTPTokens", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AM02", "EIDSCAAM02"], + "tag": ["EIDSCA.AM02"], + "appliesToTest": ["EIDSCAAM02"], "helpText": "Allows you to use MS authenticator OTP token generator", "docsDescription": "Allows you to use Microsoft Authenticator OTP token generator. Useful for using the NPS extension as MFA on VPN clients.", "executiveText": "Enables one-time password generation through Microsoft Authenticator app, providing an additional secure authentication method for employees. This is particularly useful for secure VPN access and other systems requiring multi-factor authentication.", @@ -621,7 +1090,8 @@ { "name": "standards.PWcompanionAppAllowedState", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AM01"], + "tag": ["CIS M365 7.0.0 (5.2.3.10)", "EIDSCA.AM01"], + "appliesToTest": ["CIS_5_2_3_10", "EIDSCAAM01"], "helpText": "Sets the state of Authenticator Lite, Authenticator lite is a companion app for passwordless authentication.", "docsDescription": "Sets the Authenticator Lite state to enabled. This allows users to use the Authenticator Lite built into the Outlook app instead of the full Authenticator app.", "executiveText": "Enables a simplified authentication experience by allowing users to authenticate directly through Outlook without requiring a separate authenticator app. This improves user convenience while maintaining security standards for passwordless authentication.", @@ -633,18 +1103,9 @@ "label": "Select value", "name": "standards.PWcompanionAppAllowedState.state", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - }, - { - "label": "Microsoft managed", - "value": "default" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" }, + { "label": "Microsoft managed", "value": "default" } ] } ], @@ -666,12 +1127,22 @@ "EIDSCA.AF05", "EIDSCA.AF06", "NIST CSF 2.0 (PR.AA-03)", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], + "appliesToTest": [ "EIDSCAAF01", "EIDSCAAF02", "EIDSCAAF03", "EIDSCAAF04", "EIDSCAAF05", - "EIDSCAAF06" + "EIDSCAAF06", + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9", + "ZTNA21838", + "ZTNA21839" ], "helpText": "Enables the FIDO2 authenticationMethod for the tenant", "docsDescription": "Enables FIDO2 capabilities for the tenant. This allows users to use FIDO2 keys like a Yubikey for authentication.", @@ -687,7 +1158,8 @@ { "name": "standards.EnableHardwareOAuth", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], + "appliesToTest": ["SMB1001_2_5", "SMB1001_2_6", "SMB1001_2_9"], "helpText": "Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes.", "docsDescription": "Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication.", "executiveText": "Enables physical hardware tokens that generate secure authentication codes, providing an alternative to smartphone-based authentication. This is particularly valuable for employees who cannot use mobile devices or require the highest security standards for accessing sensitive systems.", @@ -703,6 +1175,7 @@ "name": "standards.allowOAuthTokens", "cat": "Entra (AAD) Standards", "tag": ["EIDSCA.AT01", "EIDSCA.AT02"], + "appliesToTest": ["EIDSCAAT01", "EIDSCAAT02"], "helpText": "Allows you to use any software OAuth token generator", "docsDescription": "Enables OTP Software OAuth tokens for the tenant. This allows users to use OTP codes generated via software, like a password manager to be used as an authentication method.", "executiveText": "Allows employees to use third-party authentication apps and password managers to generate secure login codes, providing flexibility in authentication methods while maintaining security standards. This accommodates diverse user preferences and existing security tools.", @@ -717,7 +1190,8 @@ { "name": "standards.FormsPhishingProtection", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (1.3.5)", "Security", "PhishingProtection"], + "tag": ["CIS M365 7.0.0 (1.3.5)", "Security", "PhishingProtection"], + "appliesToTest": ["CIS_1_3_5"], "helpText": "Enables internal phishing protection for Microsoft Forms to help prevent malicious forms from being created and shared within the organization. This feature scans forms created by internal users for potential phishing content and suspicious patterns.", "docsDescription": "Enables internal phishing protection for Microsoft Forms by setting the isInOrgFormsPhishingScanEnabled property to true. This security feature helps protect organizations from internal phishing attacks through Microsoft Forms by automatically scanning forms created by internal users for potential malicious content, suspicious links, and phishing patterns. When enabled, Forms will analyze form content and block or flag potentially dangerous forms before they can be shared within the organization.", "executiveText": "Automatically scans Microsoft Forms created by employees for malicious content and phishing attempts, preventing the creation and distribution of harmful forms within the organization. This protects against both internal threats and compromised accounts that might be used to distribute malicious content.", @@ -732,10 +1206,11 @@ { "name": "standards.TAP", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21845", "ZTNA21846", "EIDSCAAT01", "EIDSCAAT02"], + "tag": [], + "appliesToTest": ["EIDSCAAT01", "EIDSCAAT02", "ZTNA21845", "ZTNA21846"], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", - "docsDescription": "Enables Temporary Password generation for the tenant.", - "executiveText": "Enables temporary access passes that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", + "docsDescription": "Enables Temporary Access Pass generation for the tenant.", + "executiveText": "Enables temporary access passes that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passs provide a secure way to restore access without compromising long-term security policies.", "addedComponent": [ { "type": "autoComplete", @@ -744,18 +1219,12 @@ "label": "Select TAP Lifetime", "name": "standards.TAP.config", "options": [ - { - "label": "Only Once", - "value": "true" - }, - { - "label": "Multiple Logons", - "value": "false" - } + { "label": "Only Once", "value": "true" }, + { "label": "Multiple Logons", "value": "false" } ] } ], - "label": "Enable Temporary Access Passes", + "label": "Enable Temporary Access Passes (TAP)", "impact": "Low Impact", "impactColour": "info", "addedDate": "2022-03-15", @@ -765,7 +1234,8 @@ { "name": "standards.PasswordExpireDisabled", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (1.3.1)", "PWAgePolicyNew"], + "tag": ["CIS M365 7.0.0 (1.3.1)", "PWAgePolicyNew"], + "appliesToTest": ["CIS_1_3_1", "ZTNA21811"], "helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.", "docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.", "executiveText": "Eliminates mandatory password expiration requirements, allowing employees to keep strong passwords indefinitely rather than forcing frequent changes that often lead to weaker passwords. This modern security approach reduces help desk calls and improves overall password security when combined with multi-factor authentication.", @@ -780,16 +1250,18 @@ { "name": "standards.CustomBannedPasswordList", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS M365 5.0 (5.2.3.2)", - "ZTNA21848", - "ZTNA21849", - "ZTNA21850", + "tag": ["CIS M365 7.0.0 (5.2.3.2)", "SMB1001 (2.1)"], + "appliesToTest": [ + "CIS_5_2_3_2", "EIDSCAPR01", "EIDSCAPR02", "EIDSCAPR03", "EIDSCAPR05", - "EIDSCAPR06" + "EIDSCAPR06", + "SMB1001_2_1", + "ZTNA21848", + "ZTNA21849", + "ZTNA21850" ], "helpText": "**Requires Entra ID P1.** Updates and enables the Entra ID custom banned password list with the supplied words. Enter words separated by commas or semicolons. Each word must be 4-16 characters long. Maximum 1,000 words allowed.", "docsDescription": "Updates and enables the Entra ID custom banned password list with the supplied words. This supplements the global banned password list maintained by Microsoft. The custom list is limited to 1,000 key base terms of 4-16 characters each. Entra ID will [block variations and common substitutions](https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-configure-custom-password-protection#configure-custom-banned-passwords) of these words in user passwords. [How are passwords evaluated?](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad#score-calculation)", @@ -807,12 +1279,14 @@ "impactColour": "warning", "addedDate": "2025-06-28", "powershellEquivalent": "Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2"] }, { "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21803", "ZTNA21804"], + "tag": [], + "appliesToTest": ["ZTNA21803", "ZTNA21804"], "helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.", "executiveText": "Allows external partners and vendors to use their own organization's multi-factor authentication when accessing company resources, streamlining collaboration while maintaining security standards. This reduces friction for external users while ensuring they still meet authentication requirements.", "addedComponent": [ @@ -823,14 +1297,8 @@ "label": "Select value", "name": "standards.ExternalMFATrusted.state", "options": [ - { - "label": "Enabled", - "value": "true" - }, - { - "label": "Disabled", - "value": "false" - } + { "label": "Enabled", "value": "true" }, + { "label": "Disabled", "value": "false" } ] } ], @@ -844,12 +1312,8 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS M365 5.0 (1.2.3)", - "CISA (MS.AAD.6.1v1)", - "ZTNA21772", - "ZTNA21787" - ], + "tag": ["CIS M365 7.0.0 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", "SMB1001 (2.8)"], + "appliesToTest": ["CIS_5_1_2_3", "SMB1001_2_8", "ZTNA21772", "ZTNA21787"], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", "executiveText": "Prevents regular employees from creating new Microsoft 365 organizations, ensuring all new tenants are properly managed and controlled by IT administrators. This prevents unauthorized shadow IT environments and maintains centralized governance over Microsoft 365 resources.", @@ -865,7 +1329,7 @@ "name": "standards.EnableAppConsentRequests", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 5.0 (1.5.2)", + "CIS M365 7.0.0 (5.1.5.2)", "CISA (MS.AAD.9.1v1)", "EIDSCA.CP04", "EIDSCA.CR01", @@ -873,12 +1337,17 @@ "EIDSCA.CR03", "EIDSCA.CR04", "Essential 8 (1507)", - "NIST CSF 2.0 (PR.AA-05)", - "ZTNA21869", + "NIST CSF 2.0 (PR.AA-05)" + ], + "appliesToTest": [ + "CIS_5_1_5_2", + "EIDSCACP04", "EIDSCACR01", "EIDSCACR02", "EIDSCACR03", - "EIDSCACR04" + "EIDSCACR04", + "ZTNA21809", + "ZTNA21869" ], "helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings", "docsDescription": "Enables the ability for users to request admin consent for applications. Should be used in conjunction with the \"Require admin consent for applications\" standards", @@ -900,7 +1369,8 @@ { "name": "standards.NudgeMFA", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21889"], + "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.", "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.", @@ -912,14 +1382,8 @@ "label": "Select value", "name": "standards.NudgeMFA.state", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] }, { @@ -943,7 +1407,8 @@ { "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.21.1v1)", "ZTNA21868"], + "tag": ["CISA (MS.AAD.21.1v1)", "SMB1001 (2.8)"], + "appliesToTest": ["SMB1001_2_8", "ZTNA21868"], "helpText": "Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "docsDescription": "Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "executiveText": "Restricts the creation of Microsoft 365 groups, Teams, and SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces. This ensures proper governance, naming conventions, and resource management while maintaining oversight of all collaborative environments.", @@ -953,19 +1418,28 @@ "impactColour": "info", "addedDate": "2022-07-17", "powershellEquivalent": "Update-MgBetaDirectorySetting", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.DisableAppCreation", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 5.0 (1.2.2)", + "CIS M365 7.0.0 (5.1.2.2)", "CISA (MS.AAD.4.1v1)", "EIDSCA.AP10", "Essential 8 (1175)", "NIST CSF 2.0 (PR.AA-05)", - "EIDSCAAP10" + "SMB1001 (2.8)" ], + "appliesToTest": ["CIS_5_1_2_2", "EIDSCAAP10", "SMB1001_2_8"], "helpText": "Disables the ability for users to create App registrations in the tenant.", "docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.", "executiveText": "Prevents regular employees from creating application registrations that could be used to maintain unauthorized access to company systems. This security measure ensures that only authorized IT personnel can create applications, reducing the risk of persistent security breaches through malicious applications.", @@ -980,7 +1454,8 @@ { "name": "standards.BitLockerKeysForOwnedDevice", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21954"], + "tag": ["CIS M365 7.0.0 (5.1.4.6)"], + "appliesToTest": ["CIS_5_1_4_6", "ZTNA21954"], "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.", "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.", "executiveText": "Gives administrators centralized control over BitLocker recovery secrets—restrict access to ensure IT-assisted recovery flows, or allow self-service when rapid device unlocks are a priority.", @@ -992,14 +1467,8 @@ "label": "Select state", "name": "standards.BitLockerKeysForOwnedDevice.state", "options": [ - { - "label": "Restrict", - "value": "restrict" - }, - { - "label": "Allow", - "value": "allow" - } + { "label": "Restrict", "value": "restrict" }, + { "label": "Allow", "value": "allow" } ] } ], @@ -1013,7 +1482,13 @@ { "name": "standards.DisableSecurityGroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA21868"], + "tag": [ + "CIS M365 7.0.0 (5.1.3.1)", + "CISA (MS.AAD.20.1v1)", + "NIST CSF 2.0 (PR.AA-05)", + "SMB1001 (2.8)" + ], + "appliesToTest": ["CIS_5_1_3_1", "SMB1001_2_8", "ZTNA21868"], "helpText": "Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams", "executiveText": "Restricts the creation of security groups to IT administrators only, preventing employees from creating unauthorized access groups that could bypass security controls. This ensures proper governance of access permissions and maintains centralized control over who can access what resources.", "addedComponent": [], @@ -1041,7 +1516,8 @@ { "name": "standards.DisableSelfServiceLicenses", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (1.3.4)", "SMB1001 (2.8)"], + "appliesToTest": ["CIS_1_3_4", "EIDSCAAP05", "SMB1001_2_8"], "helpText": "**Requires 'Billing Administrator' GDAP role.** This standard disables all self service licenses and enables all exclusions", "executiveText": "Prevents employees from purchasing Microsoft 365 licenses independently, ensuring all software acquisitions go through proper procurement channels. This maintains budget control, prevents unauthorized spending, and ensures compliance with corporate licensing agreements.", "addedComponent": [ @@ -1050,6 +1526,11 @@ "name": "standards.DisableSelfServiceLicenses.Exclusions", "label": "License Ids to exclude from this standard", "required": false + }, + { + "type": "switch", + "name": "standards.DisableSelfServiceLicenses.DisableTrials", + "label": "Disable starting trials on behalf of your organization" } ], "label": "Disable Self Service Licensing", @@ -1062,7 +1543,8 @@ { "name": "standards.DisableGuests", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21858"], + "tag": ["SMB1001 (2.8)"], + "appliesToTest": ["SMB1001_2_8", "ZTNA21858"], "helpText": "Blocks login for guest users that have not logged in for a number of days", "executiveText": "Automatically disables external guest accounts that haven't been used for a number of days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.", "addedComponent": [ @@ -1079,26 +1561,31 @@ "impactColour": "warning", "addedDate": "2022-10-20", "powershellEquivalent": "Graph API", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2"] }, { "name": "standards.OauthConsent", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 5.0 (1.5.1)", + "CIS M365 7.0.0 (5.1.5.1)", "CISA (MS.AAD.4.2v1)", "EIDSCA.AP08", "EIDSCA.AP09", "Essential 8 (1175)", - "NIST CSF 2.0 (PR.AA-05)", - "ZTNA21772", - "ZTNA21774", - "ZTNA21807", + "NIST CSF 2.0 (PR.AA-05)" + ], + "appliesToTest": [ + "CIS_5_1_5_1", "EIDSCAAP08", "EIDSCAAP09", "EIDSCACP01", "EIDSCACP03", - "EIDSCACP04" + "EIDSCACP04", + "ZTNA21772", + "ZTNA21774", + "ZTNA21807", + "ZTNA21810" ], "helpText": "Disables users from being able to consent to applications, except for those specified in the field below", "docsDescription": "Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications.", @@ -1135,7 +1622,8 @@ { "name": "standards.GuestInvite", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "EIDSCAAP04"], + "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "SMB1001 (2.8)"], + "appliesToTest": ["CIS_5_1_6_3", "EIDSCAAP04", "EIDSCAAP07", "SMB1001_2_8", "ZTNA21791"], "helpText": "This setting controls who can invite guests to your directory to collaborate on resources secured by your company, such as SharePoint sites or Azure resources.", "executiveText": "Controls who within the organization can invite external partners and vendors to access company resources, ensuring proper oversight of external access while enabling necessary business collaboration. This helps maintain security while supporting partnership and vendor relationships.", "addedComponent": [ @@ -1147,22 +1635,13 @@ "label": "Who can send invites?", "name": "standards.GuestInvite.allowInvitesFrom", "options": [ - { - "label": "Everyone", - "value": "everyone" - }, + { "label": "Everyone", "value": "everyone" }, { "label": "Admins, Guest inviters and All Members", "value": "adminsGuestInvitersAndAllMembers" }, - { - "label": "Admins and Guest inviters", - "value": "adminsAndGuestInviters" - }, - { - "label": "None", - "value": "none" - } + { "label": "Admins and Guest inviters", "value": "adminsAndGuestInviters" }, + { "label": "None", "value": "none" } ] } ], @@ -1176,11 +1655,7 @@ { "name": "standards.StaleEntraDevices", "cat": "Entra (AAD) Standards", - "tag": [ - "Essential 8 (1501)", - "NIST CSF 2.0 (ID.AM-08)", - "NIST CSF 2.0 (PR.PS-03)" - ], + "tag": ["Essential 8 (1501)", "NIST CSF 2.0 (ID.AM-08)", "NIST CSF 2.0 (PR.PS-03)"], "helpText": "**Remediate is currently not available**. Cleans up Entra devices that have not connected/signed in for the specified number of days.", "docsDescription": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days. First disables and later deletes the devices. More info can be found in the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices)", "executiveText": "Automatically identifies and removes inactive devices that haven't connected to company systems for a specified period, reducing security risks from abandoned or lost devices. This maintains a clean device inventory and prevents potential unauthorized access through dormant device registrations.", @@ -1194,17 +1669,14 @@ } } ], - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": true - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": true }, "label": "Cleanup stale Entra devices", "impact": "High Impact", "impactColour": "danger", "addedDate": "2025-01-19", "powershellEquivalent": "Remove-MgDevice, Update-MgDevice or Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.UndoOauth", @@ -1223,7 +1695,8 @@ { "name": "standards.SecurityDefaults", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.11.1v1)", "ZTNA21843"], + "tag": ["CISA (MS.AAD.11.1v1)", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], + "appliesToTest": ["SMB1001_2_5", "SMB1001_2_6", "SMB1001_2_9", "ZTNA21843"], "helpText": "Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access.", "docsDescription": "Enables SD for the tenant, which disables all forms of basic authentication and enforces users to configure MFA. Users are only prompted for MFA when a logon is considered 'suspect' by Microsoft.", "executiveText": "Activates Microsoft's baseline security configuration that requires multi-factor authentication and blocks legacy authentication methods. This provides essential security protection for organizations without complex conditional access policies, significantly improving security posture with minimal configuration.", @@ -1239,10 +1712,20 @@ "name": "standards.DisableSMS", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 5.0 (2.3.5)", + "CIS M365 7.0.0 (5.2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", - "EIDSCAAS04" + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], + "appliesToTest": [ + "CIS_5_2_3_5", + "EIDSCAAS04", + "SMB1001_2_5", + "SMB1001_2_5_L4", + "SMB1001_2_6", + "SMB1001_2_9" ], "helpText": "This blocks users from using SMS as an MFA method. If a user only has SMS as a MFA method, they will be unable to log in.", "docsDescription": "Disables SMS as an MFA method for the tenant. If a user only has SMS as a MFA method, they will be unable to sign in.", @@ -1259,10 +1742,20 @@ "name": "standards.DisableVoice", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 5.0 (2.3.5)", + "CIS M365 7.0.0 (5.2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", - "EIDSCAAV01" + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], + "appliesToTest": [ + "CIS_5_2_3_5", + "EIDSCAAV01", + "SMB1001_2_5", + "SMB1001_2_5_L4", + "SMB1001_2_6", + "SMB1001_2_9" ], "helpText": "This blocks users from using Voice call as an MFA method. If a user only has Voice as a MFA method, they will be unable to log in.", "docsDescription": "Disables Voice call as an MFA method for the tenant. If a user only has Voice call as a MFA method, they will be unable to sign in.", @@ -1278,7 +1771,14 @@ { "name": "standards.DisableEmail", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (2.3.5)", "NIST CSF 2.0 (PR.AA-03)"], + "tag": [ + "CIS M365 7.0.0 (5.2.3.7)", + "NIST CSF 2.0 (PR.AA-03)", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], + "appliesToTest": ["CIS_5_2_3_7", "SMB1001_2_5", "SMB1001_2_5_L4", "SMB1001_2_6", "SMB1001_2_9"], "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.", "executiveText": "Disables email-based authentication codes due to security concerns with email interception and account compromise. This forces users to adopt more secure authentication methods, particularly affecting guest users who must use stronger verification methods.", "addedComponent": [], @@ -1289,6 +1789,28 @@ "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", "recommendedBy": [] }, + { + "name": "standards.EmailAsAlternateLoginId", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Configures the tenant-wide Email as alternate login ID setting in Home Realm Discovery policy. Enabling this can help during migrations, if users are changing UPN.", + "docsDescription": "Sets the Home Realm Discovery policy AlternateIdLogin setting to enable or disable using email as an alternate sign-in ID.", + "executiveText": "Controls whether users can sign in with email as an alternate identifier, allowing organizations to align sign-in behavior with their identity strategy and reduce authentication ambiguity.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.EmailAsAlternateLoginId.Enabled", + "label": "Enable Email as Alternate Login ID", + "defaultValue": true + } + ], + "label": "Configure Email as alternate login ID", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-06-03", + "powershellEquivalent": "Invoke-MgGraphRequest https://graph.microsoft.com/v1.0/policies/homeRealmDiscoveryPolicies/", + "recommendedBy": ["CIPP"] + }, { "name": "standards.Disablex509Certificate", "cat": "Entra (AAD) Standards", @@ -1323,15 +1845,20 @@ "name": "standards.PerUserMFA", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 5.0 (1.2.1)", - "CIS M365 5.0 (1.1.1)", - "CIS M365 5.0 (1.1.2)", "CISA (MS.AAD.1.1v1)", "CISA (MS.AAD.1.2v1)", "Essential 8 (1504)", "Essential 8 (1173)", "Essential 8 (1401)", "NIST CSF 2.0 (PR.AA-03)", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], + "appliesToTest": [ + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9", "ZTNA21780", "ZTNA21782", "ZTNA21796" @@ -1359,11 +1886,7 @@ "creatable": false, "name": "standards.UserPreferredLanguage.preferredLanguage", "label": "Preferred Language", - "api": { - "url": "/languageList.json", - "labelField": "tag", - "valueField": "tag" - } + "api": { "url": "/languageList.json", "labelField": "tag", "valueField": "tag" } } ], "label": "Preferred language for all users", @@ -1376,7 +1899,21 @@ { "name": "standards.AppManagementPolicy", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": [ + "CIS M365 7.0.0 (5.1.5.3)", + "CIS M365 7.0.0 (5.1.5.4)", + "CIS M365 7.0.0 (5.1.5.5)", + "CIS M365 7.0.0 (5.1.5.6)" + ], + "appliesToTest": [ + "CIS_5_1_5_3", + "CIS_5_1_5_4", + "CIS_5_1_5_5", + "CIS_5_1_5_6", + "ZTNA21773", + "ZTNA21896", + "ZTNA21992" + ], "helpText": "Configures the default app management policy to control application and service principal credential restrictions such as password and key credential lifetimes.", "docsDescription": "Configures the default app management policy to control application and service principal credential restrictions. This includes password addition restrictions, custom password addition, symmetric key addition, and credential lifetime limits for both applications and service principals.", "executiveText": "Enforces credential restrictions on application registrations and service principals to limit how secrets and certificates are created and how long they remain valid. This reduces the risk of long-lived or unmanaged credentials being used to access your tenant.", @@ -1389,14 +1926,8 @@ "name": "standards.AppManagementPolicy.passwordCredentialsPasswordAddition", "label": "Disable Password Addition", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] }, { @@ -1407,14 +1938,8 @@ "name": "standards.AppManagementPolicy.passwordCredentialsCustomPasswordAddition", "label": "Disable Custom Password", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] }, { @@ -1440,7 +1965,8 @@ { "name": "standards.OutBoundSpamAlert", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (2.1.6)"], + "tag": ["CIS M365 7.0.0 (2.1.6)"], + "appliesToTest": ["CIS_2_1_6"], "helpText": "Set the Outbound Spam Alert e-mail address", "docsDescription": "Sets the e-mail address to which outbound spam alerts are sent.", "addedComponent": [ @@ -1455,7 +1981,14 @@ "impactColour": "info", "addedDate": "2023-05-03", "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.MessageExpiration", @@ -1469,7 +2002,14 @@ "impactColour": "info", "addedDate": "2024-02-23", "powershellEquivalent": "Set-TransportConfig -MessageExpirationTimeout 12.00:00:00", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.GlobalQuarantineNotifications", @@ -1484,18 +2024,9 @@ "label": "Select value", "name": "standards.GlobalQuarantineNotifications.NotificationInterval", "options": [ - { - "label": "4 hours", - "value": "04:00:00" - }, - { - "label": "1 day/Daily", - "value": "1.00:00:00" - }, - { - "label": "7 days/Weekly", - "value": "7.00:00:00" - } + { "label": "4 hours", "value": "04:00:00" }, + { "label": "1 day/Daily", "value": "1.00:00:00" }, + { "label": "7 days/Weekly", "value": "7.00:00:00" } ] } ], @@ -1504,7 +2035,14 @@ "impactColour": "info", "addedDate": "2024-05-03", "powershellEquivalent": "Set-QuarantinePolicy -EndUserSpamNotificationFrequency", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.DisableTNEF", @@ -1519,7 +2057,14 @@ "impactColour": "info", "addedDate": "2024-04-26", "powershellEquivalent": "Set-RemoteDomain -Identity 'Default' -TNEFEnabled $false", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.FocusedInbox", @@ -1535,14 +2080,8 @@ "label": "Select value", "name": "standards.FocusedInbox.state", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] } ], @@ -1551,7 +2090,14 @@ "impactColour": "info", "addedDate": "2024-04-26", "powershellEquivalent": "Set-OrganizationConfig -FocusedInboxOn $true or $false", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.CloudMessageRecall", @@ -1567,14 +2113,8 @@ "label": "Select value", "name": "standards.CloudMessageRecall.state", "options": [ - { - "label": "Enabled", - "value": "true" - }, - { - "label": "Disabled", - "value": "false" - } + { "label": "Enabled", "value": "true" }, + { "label": "Disabled", "value": "false" } ] } ], @@ -1583,7 +2123,14 @@ "impactColour": "info", "addedDate": "2024-05-31", "powershellEquivalent": "Set-OrganizationConfig -MessageRecallEnabled", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.AutoExpandArchive", @@ -1598,7 +2145,14 @@ "impactColour": "info", "addedDate": "2021-11-16", "powershellEquivalent": "Set-OrganizationConfig -AutoExpandingArchive", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.TwoClickEmailProtection", @@ -1615,14 +2169,8 @@ "label": "Select value", "name": "standards.TwoClickEmailProtection.state", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] } ], @@ -1631,12 +2179,20 @@ "impactColour": "info", "addedDate": "2025-06-13", "powershellEquivalent": "Set-OrganizationConfig -TwoClickMailPreviewEnabled $true | $false", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.EnableOnlineArchiving", "cat": "Exchange Standards", - "tag": ["Essential 8 (1511)", "NIST CSF 2.0 (PR.DS-11)"], + "tag": ["Essential 8 (1511)", "NIST CSF 2.0 (PR.DS-11)", "SMB1001 (3.1)"], + "appliesToTest": ["SMB1001_3_1"], "helpText": "Enables the In-Place Online Archive for all UserMailboxes with a valid license.", "executiveText": "Automatically enables online email archiving for all licensed employees, providing additional storage for older emails while maintaining easy access. This helps manage mailbox sizes, improves email performance, and supports compliance with data retention requirements.", "addedComponent": [], @@ -1645,12 +2201,20 @@ "impactColour": "info", "addedDate": "2024-01-20", "powershellEquivalent": "Enable-Mailbox -Archive $true", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.EnableLitigationHold", "cat": "Exchange Standards", - "tag": [], + "tag": ["SMB1001 (3.1)"], + "appliesToTest": ["SMB1001_3_1"], "helpText": "Enables litigation hold for all UserMailboxes with a valid license.", "executiveText": "Preserves all email content for legal and compliance purposes by preventing permanent deletion of emails, even when users attempt to delete them. This is essential for organizations subject to legal discovery requirements or regulatory compliance mandates.", "addedComponent": [ @@ -1667,12 +2231,20 @@ "impactColour": "info", "addedDate": "2024-06-25", "powershellEquivalent": "Set-Mailbox -LitigationHoldEnabled $true", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.SpoofWarn", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.2.3)", "ORCA111", "ORCA240", "CISAMSEXO71"], + "tag": ["CIS M365 7.0.0 (6.2.3)"], + "appliesToTest": ["CISAMSEXO71", "CIS_6_2_3", "ORCA111", "ORCA240"], "helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA", "docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)", "executiveText": "Displays visual warnings in Outlook when emails come from external senders, helping employees identify potentially suspicious messages and reducing the risk of phishing attacks. This security feature makes it easier for staff to distinguish between internal and external communications.", @@ -1683,14 +2255,8 @@ "label": "Select value", "name": "standards.SpoofWarn.state", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] }, { @@ -1706,13 +2272,21 @@ "impact": "Low Impact", "impactColour": "info", "addedDate": "2021-11-16", - "powershellEquivalent": "Set-ExternalInOutlook \u2013Enabled $true or $false", - "recommendedBy": ["CIS", "CIPP"] + "powershellEquivalent": "Set-ExternalInOutlook –Enabled $true or $false", + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.EnableMailTips", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.5.2)", "exo_mailtipsenabled"], + "tag": ["CIS M365 7.0.0 (6.5.2)", "exo_mailtipsenabled"], + "appliesToTest": ["CIS_6_5_2"], "helpText": "Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements", "executiveText": "Enables helpful notifications in Outlook that warn users about potential email issues, such as sending to large groups, external recipients, or invalid addresses. This reduces email mistakes and improves communication efficiency by providing real-time guidance to employees.", "addedComponent": [ @@ -1729,7 +2303,14 @@ "impactColour": "info", "addedDate": "2024-01-14", "powershellEquivalent": "Set-OrganizationConfig", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.TeamsMeetingsByDefault", @@ -1745,14 +2326,8 @@ "label": "Select value", "name": "standards.TeamsMeetingsByDefault.state", "options": [ - { - "label": "Enabled", - "value": "true" - }, - { - "label": "Disabled", - "value": "false" - } + { "label": "Enabled", "value": "true" }, + { "label": "Disabled", "value": "false" } ] } ], @@ -1761,7 +2336,14 @@ "impactColour": "info", "addedDate": "2024-05-31", "powershellEquivalent": "Set-OrganizationConfig -OnlineMeetingsByDefaultEnabled", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.DisableViva", @@ -1781,7 +2363,8 @@ { "name": "standards.RotateDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (2.1.9)"], + "tag": ["CIS M365 7.0.0 (2.1.9)", "SMB1001 (2.12)"], + "appliesToTest": ["CIS_2_1_9", "SMB1001_2_12"], "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", "executiveText": "Upgrades email security by replacing older 1024-bit encryption keys with stronger 2048-bit keys for email authentication. This improves the organization's email security posture and helps prevent email spoofing and tampering, maintaining trust with email recipients.", "addedComponent": [], @@ -1790,12 +2373,52 @@ "impactColour": "info", "addedDate": "2023-03-14", "powershellEquivalent": "Rotate-DkimSigningConfig", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, + { + "name": "standards.EnableExchangeCloudManagement", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Configures cloud-based management of Exchange attributes for directory-synced users with remote mailboxes in Exchange Online. This allows you to enable or disable management of Exchange attributes directly in the cloud without requiring an on-premises Exchange server. More information can be found [here](https://learn.microsoft.com/da-dk/exchange/hybrid-deployment/enable-exchange-attributes-cloud-management).", + "docsDescription": "Configures the IsExchangeCloudManaged property for mailboxes, allowing Exchange attributes (aliases, mailbox flags, custom attributes, etc.) to be managed directly in Exchange Online or revert back to on-premises management. This feature helps organizations retire their last on-premises Exchange server in hybrid deployments while maintaining the ability to manage recipient attributes. Identity attributes (names, UPN) remain managed on-premises via Active Directory. More information can be found [here](https://learn.microsoft.com/da-dk/exchange/hybrid-deployment/enable-exchange-attributes-cloud-management).", + "executiveText": "Configures cloud-based management of Exchange mailbox attributes for hybrid organizations. When enabled, eliminates the dependency on on-premises Exchange servers for attribute management. This modernizes email administration, reduces infrastructure complexity, and allows direct management of mailbox properties through cloud portals and PowerShell. When disabled, returns management to on-premises Exchange servers.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "name": "standards.EnableExchangeCloudManagement.state", + "label": "Cloud Management State", + "options": [ + { "label": "Cloud Management", "value": true }, + { "label": "On-Premises Management", "value": false } + ] + } + ], + "label": "Configure Exchange Cloud Management for Remote/On-Premises Mailboxes", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-03-28", + "powershellEquivalent": "Set-Mailbox -Identity user@domain.com -IsExchangeCloudManaged $true or $false", + "recommendedBy": ["Microsoft", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV" + ] }, { "name": "standards.AddDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (2.1.9)", "ORCA108", "CISAMSEXO31"], + "tag": ["CIS M365 7.0.0 (2.1.9)", "SMB1001 (2.12)"], + "appliesToTest": ["CISAMSEXO31", "CIS_2_1_9", "ORCA108", "ORCA108_1", "SMB1001_2_12"], "helpText": "Enables DKIM for all domains that currently support it", "executiveText": "Enables email authentication technology that digitally signs outgoing emails to verify they actually came from your organization. This prevents email spoofing, improves email deliverability, and protects the company's reputation by ensuring recipients can trust emails from your domains.", "addedComponent": [], @@ -1804,12 +2427,20 @@ "impactColour": "info", "addedDate": "2023-03-14", "powershellEquivalent": "New-DkimSigningConfig and Set-DkimSigningConfig", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.AddDMARCToMOERA", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (2.1.10)", "Security", "PhishingProtection"], + "tag": ["CIS M365 7.0.0 (2.1.10)", "Security", "PhishingProtection", "SMB1001 (2.12)"], + "appliesToTest": ["CIS_2_1_10", "SMB1001_2_12"], "helpText": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "docsDescription": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "executiveText": "Implements advanced email security for Microsoft's default domain names (onmicrosoft.com) to prevent criminals from impersonating your organization. This blocks fraudulent emails that could damage your company's reputation and protects partners and customers from phishing attacks using your domain names.", @@ -1823,10 +2454,7 @@ "label": "Value", "name": "standards.AddDMARCToMOERA.RecordValue", "options": [ - { - "label": "v=DMARC1; p=reject; (recommended)", - "value": "v=DMARC1; p=reject;" - } + { "label": "v=DMARC1; p=reject; (recommended)", "value": "v=DMARC1; p=reject;" } ] } ], @@ -1836,23 +2464,21 @@ "addedDate": "2025-06-16", "powershellEquivalent": "Portal only", "recommendedBy": ["CIS", "Microsoft"], - "disabledFeatures": { - "remediate": true - } + "disabledFeatures": { "remediate": true } }, { "name": "standards.EnableMailboxAuditing", "cat": "Exchange Standards", "tag": [ - "CIS M365 5.0 (6.1.1)", - "CIS M365 5.0 (6.1.2)", - "CIS M365 5.0 (6.1.3)", + "CIS M365 7.0.0 (6.1.1)", + "CIS M365 7.0.0 (6.1.2)", + "CIS M365 7.0.0 (6.1.3)", "exo_mailboxaudit", "Essential 8 (1509)", "Essential 8 (1683)", - "NIST CSF 2.0 (DE.CM-09)", - "CISAMSEXO131" + "NIST CSF 2.0 (DE.CM-09)" ], + "appliesToTest": ["CISAMSEXO131", "CIS_6_1_1", "CIS_6_1_2", "CIS_6_1_3"], "helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "executiveText": "Enables comprehensive logging of all email access and modifications across all employee mailboxes, providing detailed audit trails for security investigations and compliance requirements. This helps detect unauthorized access, data breaches, and supports regulatory compliance efforts.", @@ -1862,7 +2488,14 @@ "impactColour": "info", "addedDate": "2024-01-08", "powershellEquivalent": "Set-OrganizationConfig -AuditDisabled $false", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.AutoArchive", @@ -1888,7 +2521,14 @@ "impactColour": "info", "addedDate": "2025-12-11", "powershellEquivalent": "Set-OrganizationConfig -AutoArchivingThresholdPercentage 80-100", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.AutoArchiveMailbox", @@ -1905,14 +2545,8 @@ "label": "Select value", "name": "standards.AutoArchiveMailbox.state", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] } ], @@ -1921,7 +2555,14 @@ "impactColour": "info", "addedDate": "2026-01-16", "powershellEquivalent": "Set-OrganizationConfig -AutoEnableArchiveMailbox $true|$false", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.SendReceiveLimitTenant", @@ -1956,7 +2597,14 @@ "impactColour": "info", "addedDate": "2023-11-16", "powershellEquivalent": "Set-MailboxPlan", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.calDefault", @@ -1965,11 +2613,7 @@ "helpText": "Sets the default sharing level for the default calendar, for all users", "docsDescription": "Sets the default sharing level for the default calendar for all users in the tenant. You can read about the different sharing levels [here.](https://learn.microsoft.com/en-us/powershell/module/exchange/set-mailboxfolderpermission?view=exchange-ps#-accessrights)", "executiveText": "Configures how much calendar information employees share by default with colleagues, balancing collaboration needs with privacy. This setting determines whether others can see meeting details, free/busy times, or just availability, helping optimize scheduling while protecting sensitive meeting information.", - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, + "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "addedComponent": [ { "type": "autoComplete", @@ -2001,10 +2645,7 @@ "label": "Non Editing Author - The user has full read access and create items. Can can delete only own items.", "value": "NonEditingAuthor" }, - { - "label": "Reviewer - The user can read all items in the folder.", - "value": "Reviewer" - }, + { "label": "Reviewer - The user can read all items in the folder.", "value": "Reviewer" }, { "label": "Contributor - The user can create items and folders.", "value": "Contributor" @@ -2017,10 +2658,7 @@ "label": "Limited Details - The user can view free/busy time within the calendar and the subject and location of appointments.", "value": "LimitedDetails" }, - { - "label": "None - The user has no permissions on the folder.", - "value": "none" - } + { "label": "None - The user has no permissions on the folder.", "value": "none" } ] } ], @@ -2029,12 +2667,20 @@ "impactColour": "info", "addedDate": "2023-04-27", "powershellEquivalent": "Set-MailboxFolderPermission", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.EXOOutboundSpamLimits", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (2.1.6)"], + "tag": ["CIS M365 7.0.0 (2.1.15)"], + "appliesToTest": ["CIS_2_1_15"], "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.", "executiveText": "Sets limits on how many emails employees can send per hour and per day to prevent spam and protect the organization's email reputation. When limits are exceeded, the system can alert administrators or temporarily block the user, helping detect compromised accounts or prevent abuse.", @@ -2076,14 +2722,8 @@ "name": "standards.EXOOutboundSpamLimits.ActionWhenThresholdReached", "label": "Action When Threshold Reached", "options": [ - { - "label": "Alert", - "value": "Alert" - }, - { - "label": "Block User", - "value": "BlockUser" - }, + { "label": "Alert", "value": "Alert" }, + { "label": "Block User", "value": "BlockUser" }, { "label": "Block user from sending mail for the rest of the day", "value": "BlockUserForToday" @@ -2096,17 +2736,20 @@ "impactColour": "info", "addedDate": "2025-05-13", "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy", - "recommendedBy": ["CIPP", "CIS"] + "recommendedBy": ["CIPP", "CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", - "tag": [ - "CIS M365 5.0 (1.3.3)", - "exo_individualsharing", - "ZTNA21803", - "CISAMSEXO62" - ], + "tag": ["CIS M365 7.0.0 (1.3.3)", "exo_individualsharing"], + "appliesToTest": ["CISAMSEXO62", "CIS_1_3_3", "ZTNA21803"], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", "executiveText": "Prevents employees from sharing their calendars with external parties, protecting sensitive meeting information and internal schedules from unauthorized access. This security measure helps maintain confidentiality of business activities while still allowing internal collaboration.", @@ -2116,7 +2759,14 @@ "impactColour": "info", "addedDate": "2024-01-08", "powershellEquivalent": "Get-SharingPolicy | Set-SharingPolicy -Enabled $False", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.AutoAddProxy", @@ -2132,20 +2782,13 @@ "addedDate": "2025-02-07", "powershellEquivalent": "Set-Mailbox -EmailAddresses @{add=$EmailAddress}", "recommendedBy": [], - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - } + "disabledFeatures": { "report": false, "warn": true, "remediate": false } }, { "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", - "tag": [ - "CIS M365 5.0 (6.5.3)", - "exo_storageproviderrestricted", - "ZTNA21817" - ], + "tag": ["CIS M365 7.0.0 (6.5.3)", "exo_storageproviderrestricted"], + "appliesToTest": ["CIS_6_5_3", "ZTNA21817"], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", "executiveText": "Prevents employees from accessing personal cloud storage services like Dropbox or Google Drive through Outlook on the web, reducing data security risks and ensuring company information stays within approved corporate systems. This helps maintain data governance and prevents accidental data leaks.", @@ -2155,12 +2798,20 @@ "impactColour": "info", "addedDate": "2024-01-17", "powershellEquivalent": "Get-OwaMailboxPolicy | Set-OwaMailboxPolicy -AdditionalStorageProvidersEnabled $False", - "recommendedBy": ["CIS"] - }, + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, { "name": "standards.AntiSpamSafeList", "cat": "Defender Standards", - "tag": ["CIS M365 5.0 (2.1.13)"], + "tag": ["CIS M365 7.0.0 (2.1.13)"], + "appliesToTest": ["CIS_2_1_13"], "helpText": "Sets the anti-spam connection filter policy option 'safe list' in Defender.", "docsDescription": "Sets [Microsoft's built-in 'safe list'](https://learn.microsoft.com/en-us/powershell/module/exchange/set-hostedconnectionfilterpolicy?view=exchange-ps#-enablesafelist) in the anti-spam connection filter policy, rather than setting a custom safe/block list of IPs.", "executiveText": "Enables Microsoft's pre-approved list of trusted email servers to improve email delivery from legitimate sources while maintaining spam protection. This reduces false positives where legitimate emails might be blocked while still protecting against spam and malicious emails.", @@ -2176,7 +2827,14 @@ "impactColour": "info", "addedDate": "2025-02-15", "powershellEquivalent": "Set-HostedConnectionFilterPolicy \"Default\" -EnableSafeList $true", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.ShortenMeetings", @@ -2191,18 +2849,9 @@ "label": "Select value", "name": "standards.ShortenMeetings.ShortenEventScopeDefault", "options": [ - { - "label": "Disabled/None", - "value": "None" - }, - { - "label": "End early", - "value": "EndEarly" - }, - { - "label": "Start late", - "value": "StartLate" - } + { "label": "Disabled/None", "value": "None" }, + { "label": "End early", "value": "EndEarly" }, + { "label": "Start late", "value": "StartLate" } ] }, { @@ -2231,12 +2880,20 @@ "impactColour": "warning", "addedDate": "2024-05-27", "powershellEquivalent": "Set-OrganizationConfig -ShortenEventScopeDefault -DefaultMinutesToReduceShortEventsBy -DefaultMinutesToReduceLongEventsBy", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.Bookings", "cat": "Exchange Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (1.3.9)"], + "appliesToTest": ["CIS_1_3_9"], "helpText": "Sets the state of Bookings on the tenant. Bookings is a scheduling tool that allows users to book appointments with others both internal and external.", "docsDescription": "", "executiveText": "Controls whether employees can use Microsoft Bookings to create online appointment scheduling pages for internal and external clients. This feature can improve customer service and streamline appointment management, but may need to be controlled for security or business process reasons.", @@ -2247,14 +2904,8 @@ "label": "Select value", "name": "standards.Bookings.state", "options": [ - { - "label": "Enabled", - "value": "true" - }, - { - "label": "Disabled", - "value": "false" - } + { "label": "Enabled", "value": "true" }, + { "label": "Disabled", "value": "false" } ] } ], @@ -2263,12 +2914,20 @@ "impactColour": "warning", "addedDate": "2024-05-31", "powershellEquivalent": "Set-OrganizationConfig -BookingsEnabled", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.EXODirectSend", "cat": "Exchange Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (6.5.5)"], + "appliesToTest": ["CIS_6_5_5"], "helpText": "Sets the state of Direct Send in Exchange Online. Direct Send allows applications to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication.", "docsDescription": "Controls whether applications can use Direct Send to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication. A detailed explanation from Microsoft can be found [here.](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365)", "executiveText": "Controls whether business applications and devices (like printers or scanners) can send emails through the company's email system without authentication. While this enables convenient features like scan-to-email, it may pose security risks and should be carefully managed.", @@ -2280,14 +2939,8 @@ "label": "Select value", "name": "standards.EXODirectSend.state", "options": [ - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] } ], @@ -2302,12 +2955,12 @@ "name": "standards.DisableOutlookAddins", "cat": "Exchange Standards", "tag": [ - "CIS M365 5.0 (6.3.1)", + "CIS M365 7.0.0 (6.3.1)", "exo_outlookaddins", "NIST CSF 2.0 (PR.AA-05)", - "NIST CSF 2.0 (PR.PS-05)", - "ZTNA21817" + "NIST CSF 2.0 (PR.PS-05)" ], + "appliesToTest": ["CIS_6_3_1", "ZTNA21817"], "helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.", "docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.", "executiveText": "Prevents employees from installing third-party add-ins in Outlook without administrative approval, reducing security risks from potentially malicious extensions. This ensures only vetted and approved tools can access company email data while maintaining centralized control over email functionality.", @@ -2317,7 +2970,14 @@ "impactColour": "warning", "addedDate": "2024-02-05", "powershellEquivalent": "Get-ManagementRoleAssignment | Remove-ManagementRoleAssignment", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.SafeSendersDisable", @@ -2326,17 +2986,20 @@ "helpText": "Loops through all users and removes the Safe Senders list. This is to prevent SPF bypass attacks, as the Safe Senders list is not checked by SPF.", "executiveText": "Removes user-defined safe sender lists to prevent security bypasses where malicious emails could avoid spam filtering. This ensures all emails go through proper security screening, even if users have previously marked senders as 'safe', improving overall email security.", "addedComponent": [], - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, + "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "label": "Remove Safe Senders to prevent SPF bypass", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2023-10-26", "powershellEquivalent": "Set-MailboxJunkEmailConfiguration", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.DelegateSentItems", @@ -2357,7 +3020,14 @@ "impactColour": "warning", "addedDate": "2021-11-16", "powershellEquivalent": "Set-Mailbox", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.SendFromAlias", @@ -2384,12 +3054,54 @@ "impactColour": "warning", "addedDate": "2022-05-25", "powershellEquivalent": "Set-Mailbox", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { - "name": "standards.UserSubmissions", + "name": "standards.DlpViaDcsEnabled", "cat": "Exchange Standards", "tag": [], + "helpText": "Sets whether Outlook on the web uses Data Classification Services for DLP evaluation. See [Microsoft's policy tip reference](https://learn.microsoft.com/en-us/purview/dlp-ol365-win32-policy-tips#sensitive-information-types-that-support-policy-tips-for-outlook-perpetual-users).", + "docsDescription": "Configures whether Outlook on the web uses Data Classification Services (DCS)-based Data Loss Prevention (DLP) policy evaluation instead of Exchange-based evaluation. Review DLP policies before enabling this setting, as some legacy Exchange-based predicates are not supported with DCS-based evaluation. See [Microsoft's policy tip reference](https://learn.microsoft.com/en-us/purview/dlp-ol365-win32-policy-tips#sensitive-information-types-that-support-policy-tips-for-outlook-perpetual-users).", + "executiveText": "Improves how Outlook on the web applies Data Loss Prevention policies, giving users clearer guidance when sensitive information may be shared and helping reduce accidental data exposure.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Select value", + "name": "standards.DlpViaDcsEnabled.state", + "options": [ + { "label": "Enabled", "value": "true" }, + { "label": "Disabled", "value": "false" } + ] + } + ], + "label": "Set OWA DLP evaluation via DCS", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-20", + "powershellEquivalent": "Set-OrganizationConfig -DlpViaDcsEnabled", + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, + { + "name": "standards.UserSubmissions", + "cat": "Exchange Standards", + "tag": ["CIS M365 7.0.0 (8.6.1)"], + "appliesToTest": ["CIS_8_6_1"], "helpText": "Set the state of the spam submission button in Outlook", "docsDescription": "Set the state of the built-in Report button in Outlook. This gives the users the ability to report emails as spam or phish.", "executiveText": "Enables employees to easily report suspicious emails directly from Outlook, helping improve the organization's spam and phishing detection systems. This crowdsourced approach to security allows users to contribute to threat detection while providing valuable feedback to enhance email security filters.", @@ -2400,14 +3112,8 @@ "label": "Select value", "name": "standards.UserSubmissions.state", "options": [ - { - "label": "Enabled", - "value": "enable" - }, - { - "label": "Disabled", - "value": "disable" - } + { "label": "Enabled", "value": "enable" }, + { "label": "Disabled", "value": "disable" } ] }, { @@ -2422,16 +3128,25 @@ "impactColour": "warning", "addedDate": "2024-06-28", "powershellEquivalent": "New-ReportSubmissionPolicy or Set-ReportSubmissionPolicy and New-ReportSubmissionRule or Set-ReportSubmissionRule", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.DisableSharedMailbox", "cat": "Exchange Standards", "tag": [ - "CIS M365 5.0 (1.2.2)", + "CIS M365 7.0.0 (1.2.2)", "CISA (MS.AAD.10.1v1)", - "NIST CSF 2.0 (PR.AA-01)" + "NIST CSF 2.0 (PR.AA-01)", + "SMB1001 (2.3)" ], + "appliesToTest": ["CIS_1_2_2", "SMB1001_2_3"], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.", @@ -2446,7 +3161,8 @@ { "name": "standards.DisableResourceMailbox", "cat": "Exchange Standards", - "tag": ["NIST CSF 2.0 (PR.AA-01)"], + "tag": ["NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)"], + "appliesToTest": ["SMB1001_2_3"], "helpText": "Blocks login for all accounts that are marked as a resource mailbox and does not have a license assigned. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.", "docsDescription": "Resource mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for resource mailboxes. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.", "executiveText": "Prevents direct login to resource mailbox accounts (like conference rooms or equipment), ensuring they can only be managed through proper administrative channels. This security measure eliminates potential unauthorized access to resource scheduling systems while maintaining proper booking functionality.", @@ -2456,18 +3172,26 @@ "impactColour": "warning", "addedDate": "2025-06-01", "powershellEquivalent": "Get-Mailbox & Update-MgUser", - "recommendedBy": ["Microsoft", "CIPP"] + "recommendedBy": ["Microsoft", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.EXODisableAutoForwarding", "cat": "Exchange Standards", "tag": [ - "CIS M365 5.0 (6.2.1)", + "CIS M365 7.0.0 (6.2.1)", "mdo_autoforwardingmode", "mdo_blockmailforward", "CISA (MS.EXO.4.1v1)", "NIST CSF 2.0 (PR.DS-02)" ], + "appliesToTest": ["CIS_6_2_1"], "helpText": "Disables the ability for users to automatically forward e-mails to external recipients.", "docsDescription": "Disables the ability for users to automatically forward e-mails to external recipients. This is to prevent data exfiltration. Please check if there are any legitimate use cases for this feature before implementing, like forwarding invoices and such.", "executiveText": "Prevents employees from automatically forwarding company emails to external addresses, protecting against data leaks and unauthorized information sharing. This security measure helps maintain control over sensitive business communications while preventing both accidental and intentional data exfiltration.", @@ -2477,12 +3201,20 @@ "impactColour": "danger", "addedDate": "2024-07-26", "powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy -AutoForwardingMode 'Off'", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.RetentionPolicyTag", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.4.1)"], + "tag": ["SMB1001 (3.1)"], + "appliesToTest": ["SMB1001_3_1"], "helpText": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", "docsDescription": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", "executiveText": "Automatically and permanently removes deleted emails after a specified number of days, helping manage storage costs and ensuring compliance with data retention policies. This prevents accumulation of unnecessary deleted items while maintaining a reasonable recovery window for accidentally deleted emails.", @@ -2499,7 +3231,14 @@ "impactColour": "danger", "addedDate": "2025-02-02", "powershellEquivalent": "Set-RetentionPolicyTag", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.QuarantineRequestAlert", @@ -2520,7 +3259,14 @@ "impactColour": "info", "addedDate": "2024-07-15", "powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.SharePointMassDeletionAlert", @@ -2556,18 +3302,15 @@ "impactColour": "info", "addedDate": "2025-04-07", "powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["RMS_S_PREMIUM2"] }, { "name": "standards.SafeLinksTemplatePolicy", "label": "SafeLinks Policy Template", "cat": "Templates", "multiple": false, - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Medium Impact", "addedDate": "2025-04-29", "helpText": "Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.", @@ -2586,16 +3329,29 @@ "queryKey": "ListSafeLinksPolicyTemplates" } } + ], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" ] }, { "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", "tag": [ - "CIS M365 5.0 (2.1.1)", + "CIS M365 7.0.0 (2.1.1)", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps", - "NIST CSF 2.0 (DE.CM-09)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CISAMSEXO151", + "CISAMSEXO152", + "CISAMSEXO153", + "CIS_2_1_1", "ORCA105", "ORCA106", "ORCA107", @@ -2606,13 +3362,11 @@ "ORCA119", "ORCA156", "ORCA179", + "ORCA189_2", "ORCA226", "ORCA236", "ORCA237", - "ORCA238", - "CISAMSEXO151", - "CISAMSEXO152", - "CISAMSEXO153" + "ORCA238" ], "helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", "addedComponent": [ @@ -2652,7 +3406,14 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-SafeLinksPolicy or New-SafeLinksPolicy", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.AntiPhishPolicy", @@ -2665,8 +3426,14 @@ "mdo_spam_notifications_only_for_admins", "mdo_antiphishingpolicies", "mdo_phishthresholdlevel", - "CIS M365 5.0 (2.1.7)", - "NIST CSF 2.0 (DE.CM-09)", + "CIS M365 7.0.0 (2.1.7)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CISAMSEXO111", + "CISAMSEXO112", + "CISAMSEXO113", + "CIS_2_1_7", "ORCA104", "ORCA115", "ORCA180", @@ -2686,10 +3453,7 @@ "ORCA244", "ZTNA21784", "ZTNA21817", - "ZTNA21819", - "CISAMSEXO111", - "CISAMSEXO112", - "CISAMSEXO113" + "ZTNA21819" ], "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mail tips.", "addedComponent": [ @@ -2740,14 +3504,8 @@ "label": "If the message is detected as spoof by spoof intelligence", "name": "standards.AntiPhishPolicy.AuthenticationFailAction", "options": [ - { - "label": "Quarantine the message", - "value": "Quarantine" - }, - { - "label": "Move to Junk Folder", - "value": "MoveToJmf" - } + { "label": "Quarantine the message", "value": "Quarantine" }, + { "label": "Move to Junk Folder", "value": "MoveToJmf" } ] }, { @@ -2757,14 +3515,8 @@ "label": "Quarantine policy for Spoof", "name": "standards.AntiPhishPolicy.SpoofQuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -2777,18 +3529,9 @@ "label": "If a message is detected as user impersonation", "name": "standards.AntiPhishPolicy.TargetedUserProtectionAction", "options": [ - { - "label": "Move to Junk Folder", - "value": "MoveToJmf" - }, - { - "label": "Delete the message before its delivered", - "value": "Delete" - }, - { - "label": "Quarantine the message", - "value": "Quarantine" - } + { "label": "Move to Junk Folder", "value": "MoveToJmf" }, + { "label": "Delete the message before its delivered", "value": "Delete" }, + { "label": "Quarantine the message", "value": "Quarantine" } ] }, { @@ -2798,14 +3541,8 @@ "label": "Quarantine policy for user impersonation", "name": "standards.AntiPhishPolicy.TargetedUserQuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -2818,18 +3555,9 @@ "label": "If a message is detected as domain impersonation", "name": "standards.AntiPhishPolicy.TargetedDomainProtectionAction", "options": [ - { - "label": "Move to Junk Folder", - "value": "MoveToJmf" - }, - { - "label": "Delete the message before its delivered", - "value": "Delete" - }, - { - "label": "Quarantine the message", - "value": "Quarantine" - } + { "label": "Move to Junk Folder", "value": "MoveToJmf" }, + { "label": "Delete the message before its delivered", "value": "Delete" }, + { "label": "Quarantine the message", "value": "Quarantine" } ] }, { @@ -2843,14 +3571,8 @@ "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" }, - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - } + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" } ] }, { @@ -2859,18 +3581,9 @@ "label": "If Mailbox Intelligence detects an impersonated user", "name": "standards.AntiPhishPolicy.MailboxIntelligenceProtectionAction", "options": [ - { - "label": "Move to Junk Folder", - "value": "MoveToJmf" - }, - { - "label": "Delete the message before its delivered", - "value": "Delete" - }, - { - "label": "Quarantine the message", - "value": "Quarantine" - } + { "label": "Move to Junk Folder", "value": "MoveToJmf" }, + { "label": "Delete the message before its delivered", "value": "Delete" }, + { "label": "Quarantine the message", "value": "Quarantine" } ] }, { @@ -2880,14 +3593,8 @@ "label": "Apply quarantine policy", "name": "standards.AntiPhishPolicy.MailboxIntelligenceQuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -2900,20 +3607,26 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-AntiPhishPolicy or New-AntiPhishPolicy", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.SafeAttachmentPolicy", "cat": "Defender Standards", "tag": [ - "CIS M365 5.0 (2.1.4)", + "CIS M365 7.0.0 (2.1.4)", "mdo_safedocuments", "mdo_commonattachmentsfilter", "mdo_safeattachmentpolicy", - "NIST CSF 2.0 (DE.CM-09)", - "ORCA158", - "ORCA227" + "NIST CSF 2.0 (DE.CM-09)" ], + "appliesToTest": ["CIS_2_1_4", "ORCA158", "ORCA189", "ORCA227"], "helpText": "This creates a Safe Attachment policy", "addedComponent": [ { @@ -2929,18 +3642,9 @@ "label": "Safe Attachment Action", "name": "standards.SafeAttachmentPolicy.SafeAttachmentAction", "options": [ - { - "label": "Allow", - "value": "Allow" - }, - { - "label": "Block", - "value": "Block" - }, - { - "label": "DynamicDelivery", - "value": "DynamicDelivery" - } + { "label": "Allow", "value": "Allow" }, + { "label": "Block", "value": "Block" }, + { "label": "DynamicDelivery", "value": "DynamicDelivery" } ] }, { @@ -2950,25 +3654,15 @@ "label": "QuarantineTag", "name": "standards.SafeAttachmentPolicy.QuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" } ] }, - { - "type": "switch", - "label": "Redirect", - "name": "standards.SafeAttachmentPolicy.Redirect" - }, + { "type": "switch", "label": "Redirect", "name": "standards.SafeAttachmentPolicy.Redirect" }, { "type": "textField", "name": "standards.SafeAttachmentPolicy.RedirectAddress", @@ -2986,12 +3680,20 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-SafeAttachmentPolicy or New-SafeAttachmentPolicy", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.AtpPolicyForO365", "cat": "Defender Standards", - "tag": ["CIS M365 5.0 (2.1.5)", "NIST CSF 2.0 (DE.CM-09)"], + "tag": ["CIS M365 7.0.0 (2.1.5)", "NIST CSF 2.0 (DE.CM-09)"], + "appliesToTest": ["CIS_2_1_5", "ORCA225"], "helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.", "addedComponent": [ { @@ -3007,12 +3709,21 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-AtpPolicyForO365", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.PhishingSimulations", "cat": "Defender Standards", - "tag": [], + "tag": ["SMB1001 (1.11)", "SMB1001 (5.1)"], + "appliesToTest": ["SMB1001_1_11", "SMB1001_5_1"], "helpText": "This creates a phishing simulation policy that enables phishing simulations for the entire tenant.", "addedComponent": [ { @@ -3052,27 +3763,42 @@ "impactColour": "info", "addedDate": "2025-03-27", "powershellEquivalent": "New-TenantAllowBlockListItems, New-PhishSimOverridePolicy and New-ExoPhishSimOverrideRule", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.MalwareFilterPolicy", "cat": "Defender Standards", "tag": [ - "CIS M365 5.0 (2.1.2)", - "CIS M365 5.0 (2.1.3)", + "CIS M365 7.0.0 (2.1.2)", + "CIS M365 7.0.0 (2.1.3)", + "CIS M365 7.0.0 (2.1.11)", "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware", - "NIST CSF 2.0 (DE.CM-09)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CISAMSEXO101", + "CISAMSEXO102", + "CISAMSEXO103", + "CISAMSEXO95", + "CIS_2_1_11", + "CIS_2_1_2", + "CIS_2_1_3", + "ORCA120_malware", "ORCA121", "ORCA124", + "ORCA205", "ORCA232", "ZTNA21817", - "ZTNA21819", - "CISAMSEXO95", - "CISAMSEXO101", - "CISAMSEXO102", - "CISAMSEXO103" + "ZTNA21819" ], "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", "addedComponent": [ @@ -3089,14 +3815,8 @@ "label": "FileTypeAction", "name": "standards.MalwareFilterPolicy.FileTypeAction", "options": [ - { - "label": "Reject", - "value": "Reject" - }, - { - "label": "Quarantine the message", - "value": "Quarantine" - } + { "label": "Reject", "value": "Reject" }, + { "label": "Quarantine the message", "value": "Quarantine" } ] }, { @@ -3112,14 +3832,8 @@ "label": "QuarantineTag", "name": "standards.MalwareFilterPolicy.QuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -3166,7 +3880,14 @@ "impactColour": "info", "addedDate": "2024-03-25", "powershellEquivalent": "Set-MalwareFilterPolicy or New-MalwareFilterPolicy", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.PhishSimSpoofIntelligence", @@ -3195,17 +3916,34 @@ "impactColour": "info", "addedDate": "2025-03-28", "powershellEquivalent": "New-TenantAllowBlockListSpoofItems", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.SpamFilterPolicy", "cat": "Defender Standards", - "tag": [ + "tag": [], + "appliesToTest": [ + "CISAMSEXO141", + "CISAMSEXO142", + "CISAMSEXO143", "ORCA100", "ORCA101", "ORCA102", "ORCA103", "ORCA104", + "ORCA109", + "ORCA110", + "ORCA118_1", + "ORCA118_3", + "ORCA120_phish", + "ORCA120_spam", "ORCA123", "ORCA139", "ORCA140", @@ -3214,10 +3952,7 @@ "ORCA143", "ORCA224", "ORCA231", - "ORCA241", - "CISAMSEXO141", - "CISAMSEXO142", - "CISAMSEXO143" + "ORCA241" ], "helpText": "This standard creates a Spam filter policy similar to the default strict policy.", "docsDescription": "This standard creates a Spam filter policy similar to the default strict policy, the following settings are configured to on by default: IncreaseScoreWithNumericIps, IncreaseScoreWithRedirectToOtherPort, MarkAsSpamEmptyMessages, MarkAsSpamJavaScriptInHtml, MarkAsSpamSpfRecordHardFail, MarkAsSpamFromAddressAuthFail, MarkAsSpamNdrBackscatter, MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled", @@ -3247,14 +3982,8 @@ "label": "Spam Action", "name": "standards.SpamFilterPolicy.SpamAction", "options": [ - { - "label": "Quarantine the message", - "value": "Quarantine" - }, - { - "label": "Move message to Junk Email folder", - "value": "MoveToJmf" - } + { "label": "Quarantine the message", "value": "Quarantine" }, + { "label": "Move message to Junk Email folder", "value": "MoveToJmf" } ] }, { @@ -3265,14 +3994,8 @@ "label": "Spam Quarantine Tag", "name": "standards.SpamFilterPolicy.SpamQuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -3287,14 +4010,8 @@ "label": "High Confidence Spam Action", "name": "standards.SpamFilterPolicy.HighConfidenceSpamAction", "options": [ - { - "label": "Quarantine the message", - "value": "Quarantine" - }, - { - "label": "Move message to Junk Email folder", - "value": "MoveToJmf" - } + { "label": "Quarantine the message", "value": "Quarantine" }, + { "label": "Move message to Junk Email folder", "value": "MoveToJmf" } ] }, { @@ -3305,14 +4022,8 @@ "label": "High Confidence Spam Quarantine Tag", "name": "standards.SpamFilterPolicy.HighConfidenceSpamQuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -3327,14 +4038,8 @@ "label": "Bulk Spam Action", "name": "standards.SpamFilterPolicy.BulkSpamAction", "options": [ - { - "label": "Quarantine the message", - "value": "Quarantine" - }, - { - "label": "Move message to Junk Email folder", - "value": "MoveToJmf" - } + { "label": "Quarantine the message", "value": "Quarantine" }, + { "label": "Move message to Junk Email folder", "value": "MoveToJmf" } ] }, { @@ -3345,14 +4050,8 @@ "label": "Bulk Quarantine Tag", "name": "standards.SpamFilterPolicy.BulkQuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -3367,14 +4066,8 @@ "label": "Phish Spam Action", "name": "standards.SpamFilterPolicy.PhishSpamAction", "options": [ - { - "label": "Quarantine the message", - "value": "Quarantine" - }, - { - "label": "Move message to Junk Email folder", - "value": "MoveToJmf" - } + { "label": "Quarantine the message", "value": "Quarantine" }, + { "label": "Move message to Junk Email folder", "value": "MoveToJmf" } ] }, { @@ -3385,14 +4078,8 @@ "label": "Phish Quarantine Tag", "name": "standards.SpamFilterPolicy.PhishQuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -3407,14 +4094,8 @@ "label": "High Confidence Phish Quarantine Tag", "name": "standards.SpamFilterPolicy.HighConfidencePhishQuarantineTag", "options": [ - { - "label": "AdminOnlyAccessPolicy", - "value": "AdminOnlyAccessPolicy" - }, - { - "label": "DefaultFullAccessPolicy", - "value": "DefaultFullAccessPolicy" - }, + { "label": "AdminOnlyAccessPolicy", "value": "AdminOnlyAccessPolicy" }, + { "label": "DefaultFullAccessPolicy", "value": "DefaultFullAccessPolicy" }, { "label": "DefaultFullAccessWithNotificationPolicy", "value": "DefaultFullAccessWithNotificationPolicy" @@ -3521,16 +4202,19 @@ "impactColour": "warning", "addedDate": "2024-07-15", "powershellEquivalent": "New-HostedContentFilterPolicy or Set-HostedContentFilterPolicy", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.QuarantineTemplate", "cat": "Defender Standards", - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "tag": [], "helpText": "This standard creates a Custom Quarantine Policies that can be used in Anti-Spam and all MDO365 policies. Quarantine Policies can be used to specify recipients permissions, enable end-user spam notifications, and specify the release action preference", "executiveText": "Creates standardized quarantine policies that define how employees can interact with quarantined emails, including permissions to release, delete, or preview suspicious messages. This ensures consistent security handling across the organization while providing appropriate user access to manage quarantined content.", @@ -3608,7 +4292,14 @@ "impactColour": "info", "addedDate": "2025-05-16", "powershellEquivalent": "Set-QuarantinePolicy or New-QuarantinePolicy", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.IntuneWindowsDiagnostic", @@ -3636,7 +4327,8 @@ "impactColour": "info", "addedDate": "2026-01-27", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.WindowsBackupRestore", @@ -3664,7 +4356,8 @@ "impactColour": "info", "addedDate": "2026-02-26", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.intuneDeviceRetirementDays", @@ -3684,7 +4377,8 @@ "impactColour": "info", "addedDate": "2023-05-19", "powershellEquivalent": "Graph API", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.intuneBrandingProfile", @@ -3758,12 +4452,14 @@ "impactColour": "info", "addedDate": "2024-06-20", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.IntuneComplianceSettings", "cat": "Intune Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (4.1)"], + "appliesToTest": ["CIS_4_1"], "helpText": "Sets the mark devices with no compliance policy assigned as compliance/non compliant and Compliance status validity period.", "executiveText": "Configures how the system treats devices that don't have specific compliance policies and sets how often devices must check in to maintain their compliance status. This ensures proper security oversight of all corporate devices and maintains current compliance information.", "addedComponent": [ @@ -3775,14 +4471,8 @@ "name": "standards.IntuneComplianceSettings.secureByDefault", "label": "Mark devices with no compliance policy as", "options": [ - { - "label": "Compliant", - "value": "false" - }, - { - "label": "Non-Compliant", - "value": "true" - } + { "label": "Compliant", "value": "false" }, + { "label": "Non-Compliant", "value": "true" } ] }, { @@ -3801,7 +4491,8 @@ "impactColour": "info", "addedDate": "2024-11-12", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.MDMScope", @@ -3816,18 +4507,9 @@ "label": "MDM User Scope?", "type": "radio", "options": [ - { - "label": "All", - "value": "all" - }, - { - "label": "None", - "value": "none" - }, - { - "label": "Custom Group", - "value": "selected" - } + { "label": "All", "value": "all" }, + { "label": "None", "value": "none" }, + { "label": "Custom Group", "value": "selected" } ] }, { @@ -3842,12 +4524,14 @@ "impactColour": "info", "addedDate": "2025-02-18", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.DefaultPlatformRestrictions", "cat": "Intune Standards", - "tag": ["CISA (MS.AAD.19.1v1)"], + "tag": ["CIS M365 7.0.0 (4.2)", "CISA (MS.AAD.19.1v1)"], + "appliesToTest": ["CIS_4_2"], "helpText": "Sets the default platform restrictions for enrolling devices into Intune. Note: Do not block personally owned if platform is blocked.", "executiveText": "Controls which types of devices (iOS, Android, Windows, macOS) and ownership models (corporate vs. personal) can be enrolled in the company's device management system. This helps maintain security standards while supporting necessary business device types and usage scenarios.", "addedComponent": [ @@ -3917,7 +4601,8 @@ "impactColour": "info", "addedDate": "2025-04-01", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.MDMEnrollmentDuringRegistration", @@ -3938,7 +4623,8 @@ "impactColour": "warning", "addedDate": "2025-12-15", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration", @@ -3953,18 +4639,9 @@ "label": "Configure Windows Hello for Business", "multiple": false, "options": [ - { - "label": "Not configured", - "value": "notConfigured" - }, - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Not configured", "value": "notConfigured" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] }, { @@ -3999,18 +4676,9 @@ "label": "Lowercase letters in PIN", "multiple": false, "options": [ - { - "label": "Not allowed", - "value": "disallowed" - }, - { - "label": "Allowed", - "value": "allowed" - }, - { - "label": "Required", - "value": "required" - } + { "label": "Not allowed", "value": "disallowed" }, + { "label": "Allowed", "value": "allowed" }, + { "label": "Required", "value": "required" } ] }, { @@ -4019,18 +4687,9 @@ "label": "Uppercase letters in PIN", "multiple": false, "options": [ - { - "label": "Not allowed", - "value": "disallowed" - }, - { - "label": "Allowed", - "value": "allowed" - }, - { - "label": "Required", - "value": "required" - } + { "label": "Not allowed", "value": "disallowed" }, + { "label": "Allowed", "value": "allowed" }, + { "label": "Required", "value": "required" } ] }, { @@ -4039,18 +4698,9 @@ "label": "Special characters in PIN", "multiple": false, "options": [ - { - "label": "Not allowed", - "value": "disallowed" - }, - { - "label": "Allowed", - "value": "allowed" - }, - { - "label": "Required", - "value": "required" - } + { "label": "Not allowed", "value": "disallowed" }, + { "label": "Allowed", "value": "allowed" }, + { "label": "Required", "value": "required" } ] }, { @@ -4077,18 +4727,9 @@ "label": "Use enhanced anti-spoofing when available", "multiple": false, "options": [ - { - "label": "Not configured", - "value": "notConfigured" - }, - { - "label": "Enabled", - "value": "enabled" - }, - { - "label": "Disabled", - "value": "disabled" - } + { "label": "Not configured", "value": "notConfigured" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } ] }, { @@ -4096,6 +4737,28 @@ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.remotePassportEnabled", "label": "Allow phone sign-in", "default": true + }, + { + "type": "autoComplete", + "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.enhancedSignInSecurity", + "label": "Enable enhanced sign-in security", + "multiple": false, + "options": [ + { "label": "Not configured", "value": "0" }, + { "label": "Enabled on capable hardware", "value": "1" }, + { "label": "Disabled on all systems", "value": "2" } + ] + }, + { + "type": "autoComplete", + "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.securityKeyForSignIn", + "label": "Use security keys for sign-in", + "multiple": false, + "options": [ + { "label": "Not configured", "value": "notConfigured" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ] } ], "label": "Windows Hello for Business enrollment configuration", @@ -4103,12 +4766,14 @@ "impactColour": "info", "addedDate": "2025-09-25", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.intuneDeviceReg", "cat": "Intune Standards", - "tag": ["CISA (MS.AAD.17.1v1)", "ZTNA21801", "ZTNA21802"], + "tag": ["CIS M365 7.0.0 (5.1.4.2)", "CISA (MS.AAD.17.1v1)"], + "appliesToTest": ["CIS_5_1_4_2", "ZTNA21801", "ZTNA21802", "ZTNA21837"], "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users", "executiveText": "Limits how many devices each employee can register for corporate access, preventing excessive device proliferation while accommodating legitimate business needs. This helps maintain security oversight and prevents potential abuse of device registration privileges.", "addedComponent": [ @@ -4124,12 +4789,14 @@ "impactColour": "warning", "addedDate": "2023-03-27", "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.intuneDeviceRegLocalAdmins", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (5.1.4.3)", "CIS M365 7.0.0 (5.1.4.4)", "SMB1001 (2.2)"], + "appliesToTest": ["CIS_5_1_4_3", "CIS_5_1_4_4", "SMB1001_2_2"], "helpText": "Controls whether users who register Microsoft Entra joined devices are granted local administrator rights on those devices and if Global Administrators are added as local admins.", "docsDescription": "Configures the Device Registration Policy local administrator behavior for registering users. When enabled, users who register devices are not granted local administrator rights, you can also configure if Global Administrators are added as local admins.", "executiveText": "Controls whether employees who enroll devices automatically receive local administrator access. Disabling registering-user admin rights follows least-privilege principles and reduces security risk from over-privileged endpoints.", @@ -4158,6 +4825,7 @@ "name": "standards.intuneRestrictUserDeviceRegistration", "cat": "Entra (AAD) Standards", "tag": [], + "appliesToTest": [], "helpText": "Controls whether users can register devices with Entra.", "docsDescription": "Configures whether users can register devices with Entra. When disabled, users are unable to register devices with Entra.", "executiveText": "Controls whether employees can register their devices for corporate access. Disabling user device registration prevents unauthorized or unmanaged devices from connecting to company resources, enhancing overall security posture.", @@ -4176,10 +4844,34 @@ "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", "recommendedBy": [] }, + { + "name": "standards.intuneRestrictUserDeviceJoin", + "cat": "Entra (AAD) Standards", + "tag": ["CIS M365 7.0.0 (5.1.4.1)", "SMB1001 (2.8)"], + "appliesToTest": ["CIS_5_1_4_1", "SMB1001_2_8"], + "helpText": "Controls whether users can join devices to Entra.", + "docsDescription": "Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities.", + "executiveText": "Controls whether employees can join their devices to the corporate Entra directory. Disabling user device join prevents unauthorized or unmanaged devices from becoming corporate-managed identities, enhancing overall security posture.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.intuneRestrictUserDeviceJoin.disableUserDeviceJoin", + "label": "Disable users from joining devices", + "defaultValue": true + } + ], + "label": "Configure user restriction for Entra device join", + "impact": "High Impact", + "impactColour": "warning", + "addedDate": "2026-05-15", + "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", + "recommendedBy": [] + }, { "name": "standards.intuneRequireMFA", "cat": "Intune Standards", - "tag": ["ZTNA21782", "ZTNA21796", "ZTNA21872"], + "tag": [], + "appliesToTest": ["ZTNA21782", "ZTNA21796", "ZTNA21872"], "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.", "executiveText": "Requires employees to use multi-factor authentication when registering devices for corporate access, adding an extra security layer to prevent unauthorized device enrollment. This helps ensure only legitimate users can connect their devices to company systems.", "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra", @@ -4192,7 +4884,8 @@ { "name": "standards.DeletedUserRentention", "cat": "SharePoint Standards", - "tag": [], + "tag": ["SMB1001 (3.1)"], + "appliesToTest": ["SMB1001_3_1"], "helpText": "Sets the retention period for deleted users OneDrive to the specified period of time. The default is 30 days.", "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected amount of time that data can be retrieved from it.", "executiveText": "Preserves departed employees' OneDrive files for a specified period, allowing time to recover important business documents before permanent deletion. This helps prevent data loss while managing storage costs and maintaining compliance with data retention policies.", @@ -4203,54 +4896,18 @@ "name": "standards.DeletedUserRentention.Days", "label": "Retention time (Default 30 days)", "options": [ - { - "label": "30 days", - "value": "30" - }, - { - "label": "90 days", - "value": "90" - }, - { - "label": "1 year", - "value": "365" - }, - { - "label": "2 years", - "value": "730" - }, - { - "label": "3 years", - "value": "1095" - }, - { - "label": "4 years", - "value": "1460" - }, - { - "label": "5 years", - "value": "1825" - }, - { - "label": "6 years", - "value": "2190" - }, - { - "label": "7 years", - "value": "2555" - }, - { - "label": "8 years", - "value": "2920" - }, - { - "label": "9 years", - "value": "3285" - }, - { - "label": "10 years", - "value": "3650" - } + { "label": "30 days", "value": "30" }, + { "label": "90 days", "value": "90" }, + { "label": "1 year", "value": "365" }, + { "label": "2 years", "value": "730" }, + { "label": "3 years", "value": "1095" }, + { "label": "4 years", "value": "1460" }, + { "label": "5 years", "value": "1825" }, + { "label": "6 years", "value": "2190" }, + { "label": "7 years", "value": "2555" }, + { "label": "8 years", "value": "2920" }, + { "label": "9 years", "value": "3285" }, + { "label": "10 years", "value": "3650" } ] } ], @@ -4259,7 +4916,15 @@ "impactColour": "info", "addedDate": "2022-06-15", "powershellEquivalent": "Update-MgBetaAdminSharePointSetting", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.SPFileRequests", @@ -4290,7 +4955,15 @@ "impactColour": "warning", "addedDate": "2025-07-30", "powershellEquivalent": "Set-SPOTenant -CoreRequestFilesLinkEnabled $true -OneDriveRequestFilesLinkEnabled $true -CoreRequestFilesLinkExpirationInDays 30 -OneDriveRequestFilesLinkExpirationInDays 30", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.TenantDefaultTimezone", @@ -4310,12 +4983,21 @@ "impactColour": "info", "addedDate": "2024-04-20", "powershellEquivalent": "Update-MgBetaAdminSharePointSetting", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.SPAzureB2B", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.2)"], + "tag": ["CIS M365 7.0.0 (7.2.2)"], + "appliesToTest": ["CIS_7_2_2"], "helpText": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled", "executiveText": "Enables secure collaboration with external partners through SharePoint and OneDrive by integrating with Azure B2B guest access. This allows controlled sharing with external organizations while maintaining security oversight and proper access management.", "addedComponent": [], @@ -4324,17 +5006,21 @@ "impactColour": "info", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -EnableAzureADB2BIntegration $true", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 5.0 (7.3.1)", - "CISA (MS.SPO.3.1v1)", - "NIST CSF 2.0 (DE.CM-09)", - "ZTNA21817" - ], + "tag": ["CIS M365 7.0.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"], + "appliesToTest": ["CIS_7_3_1", "ZTNA21817"], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.", "addedComponent": [], @@ -4343,7 +5029,15 @@ "impactColour": "info", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -DisallowInfectedFileDownload $true", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.SPDisableLegacyWorkflows", @@ -4357,7 +5051,15 @@ "impactColour": "info", "addedDate": "2024-07-15", "powershellEquivalent": "Set-SPOTenant -DisableWorkflow2010 $true -DisableWorkflow2013 $true -DisableBackToClassic $true", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.SPDirectSharing", @@ -4371,18 +5073,21 @@ "impactColour": "warning", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -DefaultSharingLinkType Direct", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 5.0 (7.2.9)", - "CISA (MS.SPO.1.5v1)", - "ZTNA21803", - "ZTNA21804", - "ZTNA21858" - ], + "tag": ["CIS M365 7.0.0 (7.2.9)", "CISA (MS.SPO.1.5v1)"], + "appliesToTest": ["CIS_7_2_9", "ZTNA21803", "ZTNA21804", "ZTNA21858"], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.", "addedComponent": [ @@ -4402,17 +5107,21 @@ "impactColour": "warning", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -ExternalUserExpireInDays 30 -ExternalUserExpirationRequired $True", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 5.0 (7.2.10)", - "CISA (MS.SPO.1.6v1)", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 7.0.0 (7.2.10)", "CISA (MS.SPO.1.6v1)"], + "appliesToTest": ["CIS_7_2_10", "ZTNA21803", "ZTNA21804"], "helpText": "Ensure re-authentication with verification code is restricted", "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.", "addedComponent": [ @@ -4432,18 +5141,21 @@ "impactColour": "warning", "addedDate": "2024-07-09", "powershellEquivalent": "Set-SPOTenant -EmailAttestationRequired $true -EmailAttestationReAuthDays 15", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.DefaultSharingLink", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 5.0 (7.2.7)", - "CIS M365 5.0 (7.2.11)", - "CISA (MS.SPO.1.4v1)", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 7.0.0 (7.2.7)", "CIS M365 7.0.0 (7.2.11)", "CISA (MS.SPO.1.4v1)"], + "appliesToTest": ["CIS_7_2_11", "CIS_7_2_7", "ZTNA21803", "ZTNA21804"], "helpText": "Configure the SharePoint default sharing link type and permission. This setting controls both the type of sharing link created by default and the permission level assigned to those links.", "docsDescription": "Sets the default sharing link type (Direct or Internal) and permission (View) in SharePoint and OneDrive. Direct sharing means links only work for specific people, while Internal sharing means links work for anyone in the organization. Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link, reducing the risk of unintentionally granting edit privileges.", "executiveText": "Configures SharePoint default sharing links to implement the principle of least privilege for document sharing. This security measure reduces the risk of accidental data modification while maintaining collaboration functionality, requiring users to explicitly select Edit permissions when necessary. The sharing type setting controls whether links are restricted to specific recipients or available to the entire organization. This reduces the risk of accidental data exposure through link sharing.", @@ -4456,14 +5168,8 @@ "label": "Default Sharing Link Type", "name": "standards.DefaultSharingLink.SharingLinkType", "options": [ - { - "label": "Direct - Only the people the user specifies", - "value": "Direct" - }, - { - "label": "Internal - Only people in your organization", - "value": "Internal" - } + { "label": "Direct - Only the people the user specifies", "value": "Direct" }, + { "label": "Internal - Only people in your organization", "value": "Internal" } ] } ], @@ -4472,7 +5178,15 @@ "impactColour": "info", "addedDate": "2025-06-13", "powershellEquivalent": "Set-SPOTenant -DefaultSharingLinkType [Direct|Internal] -DefaultLinkPermission View", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.DisableAddShortcutsToOneDrive", @@ -4488,14 +5202,8 @@ "label": "Add Shortcuts To OneDrive button state", "name": "standards.DisableAddShortcutsToOneDrive.state", "options": [ - { - "label": "Disabled", - "value": "true" - }, - { - "label": "Enabled", - "value": "false" - } + { "label": "Disabled", "value": "true" }, + { "label": "Enabled", "value": "false" } ] } ], @@ -4504,7 +5212,15 @@ "impactColour": "warning", "addedDate": "2023-07-25", "powershellEquivalent": "Set-SPOTenant -DisableAddShortcutsToOneDrive $true or $false", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.SPSyncButtonState", @@ -4520,14 +5236,8 @@ "label": "SharePoint Sync Button state", "name": "standards.SPSyncButtonState.state", "options": [ - { - "label": "Disabled", - "value": "true" - }, - { - "label": "Enabled", - "value": "false" - } + { "label": "Disabled", "value": "true" }, + { "label": "Enabled", "value": "false" } ] } ], @@ -4536,20 +5246,26 @@ "impactColour": "warning", "addedDate": "2024-07-26", "powershellEquivalent": "Set-SPOTenant -HideSyncButtonOnTeamSite $true or $false", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.DisableSharePointLegacyAuth", "cat": "SharePoint Standards", "tag": [ - "CIS M365 5.0 (6.5.1)", - "CIS M365 5.0 (7.2.1)", + "CIS M365 7.0.0 (7.2.1)", "spo_legacy_auth", "CISA (MS.AAD.3.1v1)", - "NIST CSF 2.0 (PR.IR-01)", - "ZTNA21776", - "ZTNA21797" + "NIST CSF 2.0 (PR.IR-01)" ], + "appliesToTest": ["CIS_7_2_1", "ZTNA21776", "ZTNA21797"], "helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.", "docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.", "executiveText": "Disables outdated authentication methods for SharePoint access, forcing applications and users to use modern, more secure authentication protocols. This significantly improves security by eliminating vulnerable authentication pathways while requiring updates to older applications.", @@ -4559,18 +5275,26 @@ "impactColour": "warning", "addedDate": "2024-02-05", "powershellEquivalent": "Set-SPOTenant -LegacyAuthProtocolsEnabled $false", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.sharingCapability", "cat": "SharePoint Standards", "tag": [ - "CIS M365 5.0 (7.2.3)", + "CIS M365 7.0.0 (7.2.3)", + "CIS M365 7.0.0 (7.2.4)", "CISA (MS.AAD.14.1v1)", - "CISA (MS.SPO.1.1v1)", - "ZTNA21803", - "ZTNA21804" + "CISA (MS.SPO.1.1v1)" ], + "appliesToTest": ["CIS_7_2_3", "CIS_7_2_4", "ZTNA21803", "ZTNA21804"], "helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Defines the organization's default policy for sharing files and folders in SharePoint and OneDrive, balancing collaboration needs with security requirements. This fundamental setting determines whether employees can share with external users, anonymous links, or only internal colleagues.", "addedComponent": [ @@ -4604,18 +5328,21 @@ "impactColour": "danger", "addedDate": "2022-06-15", "powershellEquivalent": "Update-MgBetaAdminSharePointSetting", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.DisableReshare", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 5.0 (7.2.5)", - "CISA (MS.AAD.14.2v1)", - "CISA (MS.SPO.1.2v1)", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 7.0.0 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"], + "appliesToTest": ["CIS_7_2_5", "ZTNA21803", "ZTNA21804"], "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Prevents external users from sharing company documents with additional people, maintaining control over document distribution and preventing unauthorized access expansion. This security measure ensures that external sharing remains within intended boundaries set by internal employees.", @@ -4625,12 +5352,21 @@ "impactColour": "danger", "addedDate": "2022-06-15", "powershellEquivalent": "Update-MgBetaAdminSharePointSetting", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.DisableUserSiteCreate", "cat": "SharePoint Standards", - "tag": [], + "tag": ["SMB1001 (2.8)"], + "appliesToTest": ["SMB1001_2_8"], "helpText": "Disables users from creating new SharePoint sites", "docsDescription": "Disables standard users from creating SharePoint sites, also disables the ability to fully create teams", "executiveText": "Restricts the creation of new SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces and ensuring proper governance. This maintains organized information architecture while requiring approval for new collaborative environments.", @@ -4640,7 +5376,15 @@ "impactColour": "danger", "addedDate": "2022-06-15", "powershellEquivalent": "Update-MgAdminSharePointSetting", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.ExcludedfileExt", @@ -4660,7 +5404,15 @@ "impactColour": "danger", "addedDate": "2022-06-15", "powershellEquivalent": "Update-MgAdminSharePointSetting", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.disableMacSync", @@ -4674,17 +5426,21 @@ "impactColour": "danger", "addedDate": "2022-06-15", "powershellEquivalent": "Update-MgAdminSharePointSetting", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.unmanagedSync", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 5.0 (7.2.3)", - "CISA (MS.SPO.2.1v1)", - "NIST CSF 2.0 (PR.AA-05)", - "ZTNA24824" - ], + "tag": ["CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"], + "appliesToTest": ["ZTNA24824"], "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.", "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)", "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.", @@ -4696,14 +5452,8 @@ "name": "standards.unmanagedSync.state", "label": "State", "options": [ - { - "label": "Allow limited, web-only access", - "value": "1" - }, - { - "label": "Block access", - "value": "2" - } + { "label": "Allow limited, web-only access", "value": "1" }, + { "label": "Block access", "value": "2" } ], "required": false } @@ -4713,18 +5463,14 @@ "impactColour": "danger", "addedDate": "2025-06-13", "powershellEquivalent": "Set-SPOTenant -ConditionalAccessPolicy AllowFullAccess | AllowLimitedAccess | BlockAccess", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.sharingDomainRestriction", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 5.0 (7.2.6)", - "CISA (MS.AAD.14.3v1)", - "CISA (MS.SPO.1.3v1)", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 7.0.0 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"], + "appliesToTest": ["CIS_7_2_6", "ZTNA21803", "ZTNA21804"], "helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.", "executiveText": "Controls which external domains employees can share files with, enabling secure collaboration with trusted partners while blocking sharing with unauthorized organizations. This targeted approach maintains necessary business relationships while preventing data exposure to unknown entities.", "addedComponent": [ @@ -4734,18 +5480,9 @@ "name": "standards.sharingDomainRestriction.Mode", "label": "Limit external sharing by domains", "options": [ - { - "label": "Off", - "value": "none" - }, - { - "label": "Restrict sharing to specific domains", - "value": "allowList" - }, - { - "label": "Block sharing to specific domains", - "value": "blockList" - } + { "label": "Off", "value": "none" }, + { "label": "Restrict sharing to specific domains", "value": "allowList" }, + { "label": "Block sharing to specific domains", "value": "blockList" } ] }, { @@ -4760,18 +5497,40 @@ "impactColour": "danger", "addedDate": "2024-06-20", "powershellEquivalent": "Update-MgAdminSharePointSetting", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] }, { "name": "standards.TeamsGlobalMeetingPolicy", "cat": "Teams Standards", "tag": [ - "CIS M365 5.0 (8.5.1)", - "CIS M365 5.0 (8.5.2)", - "CIS M365 5.0 (8.5.3)", - "CIS M365 5.0 (8.5.4)", - "CIS M365 5.0 (8.5.5)", - "CIS M365 5.0 (8.5.6)" + "CIS M365 7.0.0 (8.5.1)", + "CIS M365 7.0.0 (8.5.2)", + "CIS M365 7.0.0 (8.5.3)", + "CIS M365 7.0.0 (8.5.4)", + "CIS M365 7.0.0 (8.5.5)", + "CIS M365 7.0.0 (8.5.6)", + "CIS M365 7.0.0 (8.5.7)", + "CIS M365 7.0.0 (8.5.8)", + "CIS M365 7.0.0 (8.5.9)" + ], + "appliesToTest": [ + "CIS_8_5_1", + "CIS_8_5_2", + "CIS_8_5_3", + "CIS_8_5_4", + "CIS_8_5_5", + "CIS_8_5_6", + "CIS_8_5_7", + "CIS_8_5_8", + "CIS_8_5_9" ], "helpText": "Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl, AllowParticipantGiveRequestControl", "executiveText": "Establishes security-focused default settings for Teams meetings, controlling who can join meetings, present content, and participate in chats. These policies balance collaboration needs with security requirements, ensuring meetings remain productive while protecting against unauthorized access and disruption.", @@ -4784,22 +5543,13 @@ "name": "standards.TeamsGlobalMeetingPolicy.DesignatedPresenterRoleMode", "label": "Default value of the `Who can present?`", "options": [ - { - "label": "Everyone", - "value": "EveryoneUserOverride" - }, - { - "label": "People in my organization", - "value": "EveryoneInCompanyUserOverride" - }, + { "label": "Everyone", "value": "EveryoneUserOverride" }, + { "label": "People in my organization", "value": "EveryoneInCompanyUserOverride" }, { "label": "People in my organization and trusted organizations", "value": "EveryoneInSameAndFederatedCompanyUserOverride" }, - { - "label": "Only organizer", - "value": "OrganizerOnlyUserOverride" - } + { "label": "Only organizer", "value": "OrganizerOnlyUserOverride" } ] }, { @@ -4821,10 +5571,7 @@ "label": "Who can bypass the lobby?", "helperText": "If left blank, the current value will not be changed.", "options": [ - { - "label": "Only organizers and co-organizers", - "value": "OrganizerOnly" - }, + { "label": "Only organizers and co-organizers", "value": "OrganizerOnly" }, { "label": "People in organization excluding guests", "value": "EveryoneInCompanyExcludingGuests" @@ -4833,14 +5580,8 @@ "label": "People in same or federated organizations", "value": "EveryoneInSameAndFederatedCompany" }, - { - "label": "People who were invited", - "value": "InvitedUsers" - }, - { - "label": "Everyone", - "value": "Everyone" - } + { "label": "People who were invited", "value": "InvitedUsers" }, + { "label": "Everyone", "value": "Everyone" } ] }, { @@ -4856,18 +5597,9 @@ "name": "standards.TeamsGlobalMeetingPolicy.MeetingChatEnabledType", "label": "Meeting chat policy", "options": [ - { - "label": "On for everyone", - "value": "Enabled" - }, - { - "label": "On for everyone but anonymous users", - "value": "EnabledExceptAnonymous" - }, - { - "label": "Off for everyone", - "value": "Disabled" - } + { "label": "On for everyone", "value": "Enabled" }, + { "label": "On for everyone but anonymous users", "value": "EnabledExceptAnonymous" }, + { "label": "Off for everyone", "value": "Disabled" } ] }, { @@ -4886,7 +5618,8 @@ "impactColour": "info", "addedDate": "2024-11-12", "powershellEquivalent": "Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting $false -AllowAnonymousUsersToStartMeeting $false -AutoAdmittedUsers $AutoAdmittedUsers -AllowPSTNUsersToBypassLobby $false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode $DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl $false -AllowParticipantGiveRequestControl $false", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsChatProtection", @@ -4914,12 +5647,14 @@ "impactColour": "info", "addedDate": "2025-10-02", "powershellEquivalent": "Set-CsTeamsMessagingConfiguration -FileTypeCheck 'Enabled' -UrlReputationCheck 'Enabled' -ReportIncorrectSecurityDetections 'Enabled'", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsExternalChatWithAnyone", "cat": "Teams Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (8.2.3)"], + "appliesToTest": ["CIS_8_2_3"], "helpText": "Controls whether users can start Teams chats with any email address, inviting external recipients as guests via email.", "docsDescription": "Manages the Teams messaging policy setting UseB2BInvitesToAddExternalUsers. When enabled, users can start chats with any email address and recipients receive an invitation to join the chat as guests. Disabling the setting prevents these external email chats from being created, keeping conversations limited to internal users and approved guests.", "executiveText": "Allows organizations to decide if employees can launch Microsoft Teams chats with anyone on the internet using just an email address. Disabling the feature keeps conversations inside trusted boundaries and helps prevent accidental data exposure through unexpected external invitations.", @@ -4929,14 +5664,8 @@ "name": "standards.TeamsExternalChatWithAnyone.UseB2BInvitesToAddExternalUsers", "label": "Allow chatting with anyone via email", "options": [ - { - "label": "Enabled", - "value": "true" - }, - { - "label": "Disabled", - "value": "false" - } + { "label": "Enabled", "value": "true" }, + { "label": "Disabled", "value": "false" } ], "defaultValue": "Disabled" } @@ -4946,7 +5675,8 @@ "impactColour": "info", "addedDate": "2025-11-03", "powershellEquivalent": "Set-CsTeamsMessagingPolicy -Identity Global -UseB2BInvitesToAddExternalUsers $false/$true", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsEmailIntegration", @@ -4967,7 +5697,9 @@ "addedDate": "2024-07-30", "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false", "recommendedBy": ["CIS"], - "tag": ["CIS M365 5.0 (8.1.2)"] + "tag": ["CIS M365 7.0.0 (8.1.2)"], + "appliesToTest": ["CIS_8_1_2"], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsGuestAccess", @@ -4988,7 +5720,8 @@ "impactColour": "info", "addedDate": "2025-06-03", "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowGuestUser $true", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsMeetingVerification", @@ -5005,10 +5738,7 @@ "label": "CAPTCHA Verification Setting", "name": "standards.TeamsMeetingVerification.CaptchaVerificationForMeetingJoin", "options": [ - { - "label": "Not Required", - "value": "NotRequired" - }, + { "label": "Not Required", "value": "NotRequired" }, { "label": "Anonymous Users and Untrusted Organizations", "value": "AnonymousUsersAndUntrustedOrganizations" @@ -5021,12 +5751,14 @@ "impactColour": "info", "addedDate": "2025-06-14", "powershellEquivalent": "Set-CsTeamsMeetingPolicy -CaptchaVerificationForMeetingJoin", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsExternalFileSharing", "cat": "Teams Standards", - "tag": ["CIS M365 5.0 (8.4.1)"], + "tag": ["CIS M365 7.0.0 (8.1.1)"], + "appliesToTest": ["CIS_8_1_1"], "helpText": "Ensure external file sharing in Teams is enabled for only approved cloud storage services.", "executiveText": "Controls which external cloud storage services (like Google Drive, Dropbox, Box) employees can access through Teams, ensuring file sharing occurs only through approved and secure platforms. This helps maintain data governance while supporting necessary business integrations.", "addedComponent": [ @@ -5061,7 +5793,8 @@ "impactColour": "info", "addedDate": "2024-07-28", "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowGoogleDrive $false -AllowShareFile $false -AllowBox $false -AllowDropBox $false -AllowEgnyte $false", - "recommendedBy": ["CIS"] + "recommendedBy": ["CIS"], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsEnrollUser", @@ -5079,14 +5812,8 @@ "name": "standards.TeamsEnrollUser.EnrollUserOverride", "label": "Voice and Face Enrollment", "options": [ - { - "label": "Disabled", - "value": "Disabled" - }, - { - "label": "Enabled", - "value": "Enabled" - } + { "label": "Disabled", "value": "Disabled" }, + { "label": "Enabled", "value": "Enabled" } ] } ], @@ -5095,12 +5822,14 @@ "impactColour": "info", "addedDate": "2024-11-12", "powershellEquivalent": "Set-CsTeamsMeetingPolicy -Identity Global -EnrollUserOverride $false", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsExternalAccessPolicy", "cat": "Teams Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (8.2.1)", "CIS M365 7.0.0 (8.2.2)"], + "appliesToTest": ["CIS_8_2_1", "CIS_8_2_2"], "helpText": "Sets the properties of the Global external access policy.", "docsDescription": "Sets the properties of the Global external access policy. External access policies determine whether or not your users can: 1) communicate with users who have Session Initiation Protocol (SIP) accounts with a federated organization; 2) communicate with users who are using custom applications built with Azure Communication Services; 3) access Skype for Business Server over the Internet, without having to log on to your internal network; 4) communicate with users who have SIP accounts with a public instant messaging (IM) provider such as Skype; and, 5) communicate with people who are using Teams with an account that's not managed by an organization.", "executiveText": "Defines the organization's policy for communicating with external users through Teams, including other organizations, Skype users, and unmanaged accounts. This fundamental setting determines the scope of external collaboration while maintaining security boundaries for business communications.", @@ -5121,12 +5850,14 @@ "impactColour": "warning", "addedDate": "2024-07-30", "powershellEquivalent": "Set-CsExternalAccessPolicy", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsFederationConfiguration", "cat": "Teams Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (8.2.1)"], + "appliesToTest": ["CIS_8_2_1", "CIS_8_2_4"], "helpText": "Sets the properties of the Global federation configuration.", "docsDescription": "Sets the properties of the Global federation configuration. Federation configuration settings determine whether or not your users can communicate with users who have SIP accounts with a federated organization.", "executiveText": "Configures how the organization federates with external organizations for Teams communication, controlling whether employees can communicate with specific external domains or all external organizations. This setting enables secure inter-organizational collaboration while maintaining control over external communications.", @@ -5144,22 +5875,10 @@ "name": "standards.TeamsFederationConfiguration.DomainControl", "label": "Communication Mode", "options": [ - { - "label": "Allow all external domains", - "value": "AllowAllExternal" - }, - { - "label": "Block all external domains", - "value": "BlockAllExternal" - }, - { - "label": "Allow specific external domains", - "value": "AllowSpecificExternal" - }, - { - "label": "Block specific external domains", - "value": "BlockSpecificExternal" - } + { "label": "Allow all external domains", "value": "AllowAllExternal" }, + { "label": "Block all external domains", "value": "BlockAllExternal" }, + { "label": "Allow specific external domains", "value": "AllowSpecificExternal" }, + { "label": "Block specific external domains", "value": "BlockSpecificExternal" } ] }, { @@ -5179,7 +5898,8 @@ "impactColour": "warning", "addedDate": "2024-07-31", "powershellEquivalent": "Set-CsTenantFederationConfiguration", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsMeetingRecordingExpiration", @@ -5206,12 +5926,14 @@ "impactColour": "warning", "addedDate": "2025-04-17", "powershellEquivalent": "Set-CsTeamsMeetingPolicy -Identity Global -MeetingRecordingExpirationDays ", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.TeamsMessagingPolicy", "cat": "Teams Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (8.6.1)"], + "appliesToTest": ["CIS_8_6_1"], "helpText": "Sets the properties of the Global messaging policy.", "docsDescription": "Sets the properties of the Global messaging policy. Messaging policies control which chat and channel messaging features are available to users in Teams.", "executiveText": "Defines what messaging capabilities employees have in Teams, including the ability to edit or delete messages, create custom emojis, and report inappropriate content. These policies help maintain professional communication standards while enabling necessary collaboration features.", @@ -5248,18 +5970,9 @@ "name": "standards.TeamsMessagingPolicy.ReadReceiptsEnabledType", "label": "Read Receipts Enabled Type", "options": [ - { - "label": "User controlled", - "value": "UserPreference" - }, - { - "label": "Turned on for everyone", - "value": "Everyone" - }, - { - "label": "Turned off for everyone", - "value": "None" - } + { "label": "User controlled", "value": "UserPreference" }, + { "label": "Turned on for everyone", "value": "Everyone" }, + { "label": "Turned off for everyone", "value": "None" } ] }, { @@ -5292,17 +6005,14 @@ "impactColour": "warning", "addedDate": "2025-01-10", "powershellEquivalent": "Set-CsTeamsMessagingPolicy", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { "name": "standards.AutopilotStatusPage", "cat": "Device Management Standards", "tag": [], - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "helpText": "Deploy the Autopilot Status Page, which shows progress during device setup through Autopilot.", "docsDescription": "This standard allows configuration of the Autopilot Status Page, providing users with a visual representation of the progress during device setup. It includes options like timeout, logging, and retry settings.", "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.", @@ -5370,17 +6080,15 @@ "impact": "Low Impact", "addedDate": "2023-12-30", "impactColour": "info", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.AutopilotProfile", "cat": "Device Management Standards", - "tag": [], - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "tag": ["SMB1001 (2.2)"], + "appliesToTest": ["SMB1001_2_2"], + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "helpText": "Assign the appropriate Autopilot profile to streamline device deployment.", "docsDescription": "This standard allows the deployment of Autopilot profiles to devices, including settings such as unique name templates, language options, and local admin privileges.", "addedComponent": [ @@ -5407,11 +6115,7 @@ "required": false, "name": "standards.AutopilotProfile.Languages", "label": "Languages", - "api": { - "url": "/languageList.json", - "labelField": "languageTag", - "valueField": "tag" - } + "api": { "url": "/languageList.json", "labelField": "languageTag", "valueField": "tag" } }, { "type": "switch", @@ -5472,20 +6176,165 @@ "impact": "Low Impact", "impactColour": "info", "addedDate": "2023-12-30", - "recommendedBy": [] + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] + }, + { + "name": "standards.DevicePrepProfile", + "cat": "Device Management Standards", + "tag": ["autopilot", "device_prep", "enrollment"], + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, + "helpText": "Creates and manages a Windows Autopilot Device Preparation profile for streamlined device enrollment.", + "docsDescription": "Deploys a Windows Autopilot Device Preparation profile through Intune configuration policies. This standard manages deployment mode, join type, account type, timeout, error messages, and optional device security group assignment. Optionally creates a new security group with the Intune Provisioning Client as owner.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.DevicePrepProfile.ProfileName", + "label": "Profile Display Name", + "required": true + }, + { + "type": "textField", + "name": "standards.DevicePrepProfile.ProfileDescription", + "label": "Profile Description", + "required": false + }, + { + "type": "select", + "multiple": false, + "name": "standards.DevicePrepProfile.DeploymentType", + "label": "Deployment Type", + "options": [ + { "label": "Single user", "value": "0" }, + { "label": "Shared", "value": "1" } + ] + }, + { + "type": "select", + "multiple": false, + "name": "standards.DevicePrepProfile.JoinType", + "label": "Join Type", + "options": [ + { "label": "Microsoft Entra join", "value": "0" }, + { "label": "Microsoft Entra hybrid join", "value": "1" } + ] + }, + { + "type": "select", + "multiple": false, + "name": "standards.DevicePrepProfile.AccountType", + "label": "Account Type", + "options": [ + { "label": "Standard user", "value": "0" }, + { "label": "Administrator", "value": "1" } + ] + }, + { + "type": "number", + "name": "standards.DevicePrepProfile.Timeout", + "label": "Timeout (minutes)", + "defaultValue": 60 + }, + { + "type": "textField", + "name": "standards.DevicePrepProfile.CustomErrorMessage", + "label": "Custom Error Message", + "required": false + }, + { + "type": "switch", + "name": "standards.DevicePrepProfile.AllowSkip", + "label": "Allow users to skip setup after failure", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DevicePrepProfile.AllowDiagnostics", + "label": "Allow users to collect diagnostics", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.DevicePrepProfile.DeviceGroupName", + "label": "Device Security Group Name (wildcard match)", + "required": false + }, + { + "type": "switch", + "name": "standards.DevicePrepProfile.CreateNewGroup", + "label": "Create new group if group is not found", + "defaultValue": false + }, + { + "type": "radio", + "name": "standards.DevicePrepProfile.AssignTo", + "label": "Policy Assignment", + "options": [ + { "label": "Do not assign", "value": "none" }, + { "label": "All devices", "value": "AllDevices" }, + { "label": "All users and devices", "value": "AllDevicesAndUsers" } + ] + } + ], + "label": "Deploy Device Prep Profile", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-05-25", + "recommendedBy": [], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.IntuneTemplate", "cat": "Templates", "label": "Intune Template", "multiple": true, - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "High Impact", "addedDate": "2023-12-30", + "tag": [ + "SMB1001 (1.2)", + "SMB1001 (1.3)", + "SMB1001 (1.4)", + "SMB1001 (1.8)", + "SMB1001 (1.9)", + "SMB1001 (1.10)", + "SMB1001 (1.12)", + "SMB1001 (2.2)", + "SMB1001 (4.7)" + ], + "appliesToTest": [ + "SMB1001_1_10", + "SMB1001_1_12", + "SMB1001_1_2", + "SMB1001_1_3", + "SMB1001_1_4", + "SMB1001_1_8", + "SMB1001_1_9", + "SMB1001_2_2", + "SMB1001_4_7", + "ZTNA24540", + "ZTNA24541", + "ZTNA24542", + "ZTNA24543", + "ZTNA24545", + "ZTNA24547", + "ZTNA24548", + "ZTNA24549", + "ZTNA24550", + "ZTNA24552", + "ZTNA24553", + "ZTNA24564", + "ZTNA24568", + "ZTNA24569", + "ZTNA24572", + "ZTNA24574", + "ZTNA24575", + "ZTNA24576", + "ZTNA24784", + "ZTNA24839", + "ZTNA24840", + "ZTNA24870" + ], "helpText": "Deploy and manage Intune templates across devices.", "executiveText": "Deploys standardized device management configurations across all corporate devices, ensuring consistent security policies, application settings, and compliance requirements. This template-based approach streamlines device management while maintaining uniform security standards across the organization.", "addedComponent": [ @@ -5502,11 +6351,7 @@ "labelField": "Displayname", "valueField": "GUID", "showRefresh": true, - "templateView": { - "title": "Intune Template", - "property": "RAWJson", - "type": "intune" - } + "templateView": { "title": "Intune Template", "property": "RAWJson", "type": "intune" } } }, { @@ -5528,26 +6373,11 @@ "label": "Who should this template be assigned to?", "type": "radio", "options": [ - { - "label": "Do not assign", - "value": "On" - }, - { - "label": "Assign to all users", - "value": "allLicensedUsers" - }, - { - "label": "Assign to all devices", - "value": "AllDevices" - }, - { - "label": "Assign to all users and devices", - "value": "AllDevicesAndUsers" - }, - { - "label": "Assign to Custom Group", - "value": "customGroup" - } + { "label": "Do not assign", "value": "On" }, + { "label": "Assign to all users", "value": "allLicensedUsers" }, + { "label": "Assign to all devices", "value": "AllDevices" }, + { "label": "Assign to all users and devices", "value": "AllDevicesAndUsers" }, + { "label": "Assign to Custom Group", "value": "customGroup" } ] }, { @@ -5556,11 +6386,7 @@ "name": "customGroup", "label": "Enter the custom group name if you selected 'Assign to Custom Group'. Wildcards are allowed." }, - { - "type": "switch", - "name": "verifyAssignments", - "label": "Verify policy assignments" - }, + { "type": "switch", "name": "verifyAssignments", "label": "Verify policy assignments" }, { "name": "excludeGroup", "label": "Exclude Groups", @@ -5582,15 +6408,21 @@ "required": false, "helpText": "Choose whether to include or exclude devices matching the filter. Only applies if you specified a filter name above. Defaults to Include if not specified.", "options": [ - { - "label": "Include - Assign to devices matching the filter", - "value": "include" - }, - { - "label": "Exclude - Assign to devices NOT matching the filter", - "value": "exclude" - } + { "label": "Include - Assign to devices matching the filter", "value": "include" }, + { "label": "Exclude - Assign to devices NOT matching the filter", "value": "exclude" } ] + }, + { + "type": "number", + "required": false, + "name": "levenshteinDistance", + "label": "Fuzzy Match Distance (0 = exact name match only, higher values allow replacing policies with similar names based on Levenshtein distance)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" } + }, + "warningThreshold": 5, + "warningMessage": "Warning: values above 5 can match unrelated policies. Use with caution." } ] }, @@ -5599,14 +6431,34 @@ "cat": "Templates", "label": "Reusable Settings Template", "multiple": true, - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "High Impact", "impactColour": "info", "addedDate": "2026-01-02", + "tag": [ + "SMB1001 (1.2)", + "SMB1001 (1.3)", + "SMB1001 (1.4)", + "SMB1001 (1.8)", + "SMB1001 (1.9)", + "SMB1001 (1.10)", + "SMB1001 (1.12)" + ], + "appliesToTest": [ + "SMB1001_1_10", + "SMB1001_1_12", + "SMB1001_1_2", + "SMB1001_1_3", + "SMB1001_1_4", + "SMB1001_1_8", + "SMB1001_1_9", + "ZTNA24540", + "ZTNA24550", + "ZTNA24552", + "ZTNA24574", + "ZTNA24575", + "ZTNA24784" + ], "helpText": "Deploy and maintain Intune reusable settings templates that can be referenced by multiple policies.", "executiveText": "Creates and keeps reusable Intune settings templates consistent so common firewall and configuration blocks can be reused across many policies.", "addedComponent": [ @@ -5623,11 +6475,7 @@ "labelField": "displayName", "valueField": "GUID", "showRefresh": true, - "templateView": { - "title": "Reusable Settings", - "property": "RawJSON", - "type": "intune" - } + "templateView": { "title": "Reusable Settings", "property": "RawJSON", "type": "intune" } } } ], @@ -5638,11 +6486,7 @@ "name": "standards.TransportRuleTemplate", "label": "Transport Rule Template", "cat": "Templates", - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, + "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "impact": "Medium Impact", "addedDate": "2023-12-30", "helpText": "Deploy transport rules to manage email flow.", @@ -5653,7 +6497,7 @@ "name": "transportRuleTemplate", "label": "Select Transport Rule Template", "api": { - "url": "/api/ListTransportRulesTemplates", + "url": "/api/ListTransportRulesTemplates?noJson=true", "labelField": "name", "valueField": "GUID", "queryKey": "ListTransportRulesTemplates" @@ -5665,6 +6509,13 @@ "name": "overwrite", "defaultValue": true } + ], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" ] }, { @@ -5672,13 +6523,57 @@ "label": "Conditional Access Template", "cat": "Templates", "multiple": true, - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, + "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "impact": "High Impact", "addedDate": "2023-12-30", + "tag": [ + "CIS M365 7.0.0 (5.2.2.1)", + "CIS M365 7.0.0 (5.2.2.2)", + "CIS M365 7.0.0 (5.2.2.3)", + "CIS M365 7.0.0 (5.2.2.4)", + "CIS M365 7.0.0 (5.2.2.5)", + "CIS M365 7.0.0 (5.2.2.6)", + "CIS M365 7.0.0 (5.2.2.7)", + "CIS M365 7.0.0 (5.2.2.8)", + "CIS M365 7.0.0 (5.2.2.9)", + "CIS M365 7.0.0 (5.2.2.10)", + "CIS M365 7.0.0 (5.2.2.11)", + "CIS M365 7.0.0 (5.2.2.12)", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.8)", + "SMB1001 (2.9)" + ], + "appliesToTest": [ + "CIS_5_2_2_1", + "CIS_5_2_2_10", + "CIS_5_2_2_11", + "CIS_5_2_2_12", + "CIS_5_2_2_2", + "CIS_5_2_2_3", + "CIS_5_2_2_4", + "CIS_5_2_2_5", + "CIS_5_2_2_6", + "CIS_5_2_2_7", + "CIS_5_2_2_8", + "CIS_5_2_2_9", + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_8", + "SMB1001_2_9", + "ZTNA21783", + "ZTNA21786", + "ZTNA21806", + "ZTNA21808", + "ZTNA21824", + "ZTNA21825", + "ZTNA21828", + "ZTNA21830", + "ZTNA21883", + "ZTNA21892", + "ZTNA21941", + "ZTNA24827" + ], "helpText": "Manage conditional access policies for better security.", "executiveText": "Deploys standardized conditional access policies that automatically enforce security requirements based on user location, device compliance, and risk factors. These templates ensure consistent security controls across the organization while enabling secure access to business resources.", "addedComponent": [ @@ -5686,6 +6581,8 @@ "type": "autoComplete", "name": "TemplateList", "multiple": false, + "required": false, + "creatable": false, "label": "Select Conditional Access Template", "api": { "url": "/api/ListCATemplates", @@ -5693,8 +6590,23 @@ "valueField": "GUID", "queryKey": "ListCATemplates", "showRefresh": true, - "templateView": { - "title": "Conditional Access Policy" + "templateView": { "title": "Conditional Access Policy" } + } + }, + { + "type": "autoComplete", + "multiple": false, + "required": false, + "creatable": false, + "name": "TemplateList-Tags", + "label": "Or select a package of CA Templates", + "api": { + "queryKey": "ListCATemplates-tag-autocomplete", + "url": "/api/ListCATemplates?mode=Tag", + "labelField": "label", + "valueField": "value", + "addedField": { + "templates": "templates" } } }, @@ -5703,22 +6615,10 @@ "label": "What state should we deploy this template in?", "type": "radio", "options": [ - { - "value": "donotchange", - "label": "Do not change state" - }, - { - "value": "Enabled", - "label": "Set to enabled" - }, - { - "value": "Disabled", - "label": "Set to disabled" - }, - { - "value": "enabledForReportingButNotEnforced", - "label": "Set to report only" - } + { "value": "donotchange", "label": "Do not change state" }, + { "value": "Enabled", "label": "Set to enabled" }, + { "value": "Disabled", "label": "Set to disabled" }, + { "value": "enabledForReportingButNotEnforced", "label": "Set to report only" } ] }, { @@ -5726,22 +6626,15 @@ "name": "DisableSD", "label": "Disable Security Defaults when deploying policy" }, - { - "type": "switch", - "name": "CreateGroups", - "label": "Create groups if they do not exist" - } - ] + { "type": "switch", "name": "CreateGroups", "label": "Create groups if they do not exist" } + ], + "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2"] }, { "name": "standards.ExchangeConnectorTemplate", "label": "Exchange Connector Template", "cat": "Templates", - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, + "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "impact": "Medium Impact", "addedDate": "2023-12-30", "helpText": "Deploy and manage Exchange connectors.", @@ -5758,6 +6651,13 @@ "queryKey": "ListExConnectorTemplates" } } + ], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" ] }, { @@ -5765,11 +6665,9 @@ "label": "Group Template", "multi": true, "cat": "Templates", - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, + "tag": [], + "appliesToTest": [], + "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "impact": "Medium Impact", "addedDate": "2023-12-30", "helpText": "Deploy and manage group templates.", @@ -5787,21 +6685,18 @@ "queryKey": "ListGroupTemplates" } } - ] + ], + "requiredCapabilities": ["EXCHANGE_S_STANDARD", "EXCHANGE_S_ENTERPRISE", "EXCHANGE_LITE"] }, { "name": "standards.DlpCompliancePolicyTemplate", "label": "DLP Compliance Policy Template", "multi": true, "cat": "Templates", - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Medium Impact", "addedDate": "2026-05-10", - "helpText": "Deploy Microsoft Purview DLP compliance policies from CIPP templates. Existing policies are overwritten in place.", + "helpText": "Deploy Microsoft Purview DLP compliance policies from CIPP templates.", "executiveText": "Deploys Data Loss Prevention policies from a standardized template library. Ensures consistent DLP coverage across tenants for sensitive data such as financial, identity, and regulated content.", "addedComponent": [ { @@ -5824,11 +6719,7 @@ "label": "Retention Compliance Policy Template", "multi": true, "cat": "Templates", - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Medium Impact", "addedDate": "2026-05-10", "helpText": "Deploy Microsoft Purview retention compliance policies from CIPP templates.", @@ -5854,11 +6745,7 @@ "label": "Sensitivity Label Template", "multi": true, "cat": "Templates", - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Medium Impact", "addedDate": "2026-05-10", "helpText": "Deploy Microsoft Purview sensitivity labels from CIPP templates.", @@ -5884,11 +6771,7 @@ "label": "Sensitive Information Type Template", "multi": true, "cat": "Templates", - "disabledFeatures": { - "report": false, - "warn": false, - "remediate": false - }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Low Impact", "addedDate": "2026-05-10", "helpText": "Deploy custom Microsoft Purview Sensitive Information Types from CIPP templates.", @@ -5914,11 +6797,7 @@ "label": "Assignment Filter Template", "multi": true, "cat": "Templates", - "disabledFeatures": { - "report": true, - "warn": true, - "remediate": false - }, + "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "impact": "Medium Impact", "addedDate": "2025-10-04", "helpText": "Deploy and manage assignment filter templates.", @@ -5936,6 +6815,40 @@ "queryKey": "ListAssignmentFilterTemplates" } } + ], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] + }, + { + "name": "standards.TenantAllowBlockListTemplate", + "label": "Tenant Allow/Block List Template", + "cat": "Exchange Standards", + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, + "impact": "Medium Impact", + "addedDate": "2026-04-02", + "helpText": "Deploy tenant allow/block list entries from a saved template.", + "executiveText": "Deploys standardized tenant allow/block list entries across tenants. These templates ensure consistent email filtering rules are applied, managing which senders, URLs, file hashes, and IP addresses are allowed or blocked across the organization.", + "addedComponent": [ + { + "type": "autoComplete", + "name": "TenantAllowBlockListTemplate", + "required": false, + "multiple": true, + "label": "Select Tenant Allow/Block List Template", + "api": { + "url": "/api/ListTenantAllowBlockListTemplates", + "labelField": "templateName", + "valueField": "GUID", + "queryKey": "ListTenantAllowBlockListTemplates", + "showRefresh": true + } + } + ], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" ] }, { @@ -5962,12 +6875,19 @@ "impactColour": "info", "addedDate": "2025-05-28", "powershellEquivalent": "Set-Mailbox -RecipientLimits", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.DisableExchangeOnlinePowerShell", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.1.1)", "Security", "NIST CSF 2.0 (PR.AA-05)"], + "tag": ["Security", "NIST CSF 2.0 (PR.AA-05)"], "helpText": "Disables Exchange Online PowerShell access for non-admin users by setting the RemotePowerShellEnabled property to false for each user. This helps prevent attackers from using PowerShell to run malicious commands, access file systems, registry, and distribute ransomware throughout networks. Users with admin roles are automatically excluded.", "docsDescription": "Disables Exchange Online PowerShell access for non-admin users by setting the RemotePowerShellEnabled property to false for each user. This security measure follows a least privileged access approach, preventing potential attackers from using PowerShell to execute malicious commands, access sensitive systems, or distribute malware. Users with management roles containing 'Admin' are automatically excluded to ensure administrators retain PowerShell access to perform necessary management tasks.", "executiveText": "Restricts PowerShell access to Exchange Online for regular employees while maintaining access for administrators, significantly reducing security risks from compromised accounts. This prevents attackers from using PowerShell to execute malicious commands or distribute ransomware while preserving necessary administrative capabilities.", @@ -5976,12 +6896,19 @@ "impactColour": "warning", "addedDate": "2025-06-19", "powershellEquivalent": "Set-User -Identity $user -RemotePowerShellEnabled $false", - "recommendedBy": ["CIS", "CIPP"] + "recommendedBy": ["CIS", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.OWAAttachmentRestrictions", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.1.2)", "Security", "NIST CSF 2.0 (PR.AA-05)"], + "tag": ["Security", "NIST CSF 2.0 (PR.AA-05)"], "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.", "executiveText": "Restricts access to email attachments on personal or unmanaged devices while allowing full functionality on corporate-managed devices. This security measure prevents data theft through email attachments while maintaining productivity for employees using approved company devices.", @@ -5991,10 +6918,7 @@ "name": "standards.OWAAttachmentRestrictions.ConditionalAccessPolicy", "label": "Attachment Restriction Policy", "options": [ - { - "label": "Read Only (View/Edit via Office Online, no download)", - "value": "ReadOnly" - }, + { "label": "Read Only (View/Edit via Office Online, no download)", "value": "ReadOnly" }, { "label": "Read Only Plus Attachments Blocked (Cannot see attachments)", "value": "ReadOnlyPlusAttachmentsBlocked" @@ -6008,7 +6932,14 @@ "impactColour": "warning", "addedDate": "2025-08-22", "powershellEquivalent": "Set-OwaMailboxPolicy -Identity \"OwaMailboxPolicy-Default\" -ConditionalAccessPolicy ReadOnlyPlusAttachmentsBlocked", - "recommendedBy": ["Microsoft Zero Trust", "CIPP"] + "recommendedBy": ["Microsoft Zero Trust", "CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] }, { "name": "standards.LegacyEmailReportAddins", @@ -6115,6 +7046,12 @@ "placeholder": "e.g. https://example.com/*", "helperText": "Enter URLs to allowlist in the extension. Press enter to add each URL. Wildcards are allowed. This should be used for sites that are being blocked by the extension but are known to be safe." }, + { + "type": "switch", + "name": "standards.DeployCheckChromeExtension.domainSquattingEnabled", + "label": "Enable domain squatting detection", + "defaultValue": true + }, { "type": "textField", "name": "standards.DeployCheckChromeExtension.companyName", @@ -6122,13 +7059,6 @@ "placeholder": "YOUR-COMPANY", "required": false }, - { - "type": "textField", - "name": "standards.DeployCheckChromeExtension.companyURL", - "label": "Company URL", - "placeholder": "https://yourcompany.com", - "required": false - }, { "type": "textField", "name": "standards.DeployCheckChromeExtension.productName", @@ -6143,6 +7073,27 @@ "placeholder": "support@yourcompany.com", "required": false }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.supportUrl", + "label": "Support URL", + "placeholder": "https://support.yourcompany.com", + "required": false + }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.privacyPolicyUrl", + "label": "Privacy Policy URL", + "placeholder": "https://yourcompany.com/privacy", + "required": false + }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.aboutUrl", + "label": "About URL", + "placeholder": "https://yourcompany.com/about", + "required": false + }, { "type": "textField", "name": "standards.DeployCheckChromeExtension.primaryColor", @@ -6162,26 +7113,11 @@ "label": "Who should this app be assigned to?", "type": "radio", "options": [ - { - "label": "Do not assign", - "value": "On" - }, - { - "label": "Assign to all users", - "value": "allLicensedUsers" - }, - { - "label": "Assign to all devices", - "value": "AllDevices" - }, - { - "label": "Assign to all users and devices", - "value": "AllDevicesAndUsers" - }, - { - "label": "Assign to Custom Group", - "value": "customGroup" - } + { "label": "Do not assign", "value": "On" }, + { "label": "Assign to all users", "value": "allLicensedUsers" }, + { "label": "Assign to all devices", "value": "AllDevices" }, + { "label": "Assign to all users and devices", "value": "AllDevicesAndUsers" }, + { "label": "Assign to Custom Group", "value": "customGroup" } ] }, { @@ -6196,7 +7132,8 @@ "impactColour": "info", "addedDate": "2025-09-18", "powershellEquivalent": "Add-CIPPW32ScriptApplication", - "recommendedBy": ["CIPP"] + "recommendedBy": ["CIPP"], + "requiredCapabilities": ["INTUNE_A", "MDM_Services", "EMS", "SCCM", "MICROSOFTINTUNEPLAN1"] }, { "name": "standards.SecureScoreRemediation", @@ -6211,11 +7148,7 @@ "required": false, "name": "standards.SecureScoreRemediation.Default", "label": "Controls to set to Default", - "api": { - "url": "/secureScore.json", - "labelField": "title", - "valueField": "id" - } + "api": { "url": "/secureScore.json", "labelField": "title", "valueField": "id" } }, { "type": "autoComplete", @@ -6224,11 +7157,7 @@ "required": false, "name": "standards.SecureScoreRemediation.Ignored", "label": "Controls to set to Ignored", - "api": { - "url": "/secureScore.json", - "labelField": "title", - "valueField": "id" - } + "api": { "url": "/secureScore.json", "labelField": "title", "valueField": "id" } }, { "type": "autoComplete", @@ -6237,11 +7166,7 @@ "required": false, "name": "standards.SecureScoreRemediation.ThirdParty", "label": "Controls to set to Third-Party", - "api": { - "url": "/secureScore.json", - "labelField": "title", - "valueField": "id" - } + "api": { "url": "/secureScore.json", "labelField": "title", "valueField": "id" } }, { "type": "autoComplete", @@ -6250,11 +7175,7 @@ "creatable": true, "name": "standards.SecureScoreRemediation.Reviewed", "label": "Controls to set to Reviewed", - "api": { - "url": "/secureScore.json", - "labelField": "title", - "valueField": "id" - } + "api": { "url": "/secureScore.json", "labelField": "title", "valueField": "id" } } ], "label": "Update Secure Score Control Profiles", @@ -6271,22 +7192,12 @@ "docsDescription": "Creates five Exchange Online transport rules grouped by the first letter of user display names (A-E, F-J, K-O, P-T, U-Z). Each rule fires when an external sender's From header matches a display name in that group, prepends a configurable HTML warning banner, and skips emails from accepted organisational domains. Any manually configured sender or domain exemptions on existing rules are preserved when the standard runs. The disclaimer HTML is fully customisable via the standard settings.", "executiveText": "Protects staff from display-name impersonation attacks by injecting a visible warning banner on emails that appear to come from a colleague but originate externally. Rules are maintained automatically across all letter groups and updated whenever the standard runs.", "addedComponent": [ - { - "type": "heading", - "label": "Alert Banner (HTML)", - "required": false - }, { "type": "textField", "name": "standards.ColleagueImpersonationAlert.disclaimerHtml", "label": "Disclaimer HTML – Paste the full HTML for the warning banner", "required": true }, - { - "type": "heading", - "label": "Keyword Exclusions (Exclude certain users by keywords)", - "required": false - }, { "type": "autoComplete", "name": "standards.ColleagueImpersonationAlert.excludedMailboxes", @@ -6295,11 +7206,6 @@ "creatable": true, "required": false }, - { - "type": "heading", - "label": "Exempt Senders (Email Accounts)", - "required": false - }, { "type": "autoComplete", "name": "standards.ColleagueImpersonationAlert.additionalExemptSenders", @@ -6314,6 +7220,890 @@ "impactColour": "warning", "addedDate": "2026-03-22", "powershellEquivalent": "New-TransportRule / Set-TransportRule", + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, + { + "name": "standards.DefenderCompliancePolicy", + "cat": "Defender Standards", + "tag": ["defender_mde_connector", "defender_intune_compliance"], + "helpText": "Configures the Microsoft Defender for Endpoint connector with Intune, enabling compliance evaluation for mobile and desktop platforms (Android, iOS, macOS, Windows). Controls which platforms connect to MDE and whether devices are blocked when partner data is missing.", + "docsDescription": "Configures the Microsoft Defender for Endpoint mobile threat defense connector with Intune. This enables compliance evaluation across platforms (Android, iOS/iPadOS, macOS, Windows) and controls settings like blocking unsupported OS versions, requiring partner data for compliance, and enabling mobile application management. The connector must be enabled before platform-specific compliance policies can evaluate device risk from MDE.", + "executiveText": "Establishes the critical link between Microsoft Defender for Endpoint and Intune, enabling security risk data from MDE to be used in device compliance policies. This ensures that only devices meeting your organization's security standards can access corporate resources, providing a foundational layer of Zero Trust security across all platforms.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.ConnectAndroid", + "label": "Connect Android devices to MDE", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.ConnectAndroidCompliance", + "label": "Connect Android 6.0.0+ (App-based MAM)", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.androidDeviceBlockedOnMissingPartnerData", + "label": "Block Android if partner data unavailable", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.ConnectIos", + "label": "Connect iOS/iPadOS devices to MDE", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.ConnectIosCompliance", + "label": "Connect iOS 13.0+ (App-based MAM)", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.appSync", + "label": "Enable App Sync for iOS", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.iosDeviceBlockedOnMissingPartnerData", + "label": "Block iOS if partner data unavailable", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.allowPartnerToCollectIosCertificateMetadata", + "label": "Collect certificate metadata from iOS", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.allowPartnerToCollectIosPersonalCertificateMetadata", + "label": "Collect personal certificate metadata from iOS", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.ConnectMac", + "label": "Connect macOS devices to MDE", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.macDeviceBlockedOnMissingPartnerData", + "label": "Block macOS if partner data unavailable", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.ConnectWindows", + "label": "Connect Windows 10.0.15063+ to MDE (Note: enabling this forces 'Block Windows if partner data unavailable' to on)", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.windowsMobileApplicationManagementEnabled", + "label": "Connect Windows (MAM)", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.windowsDeviceBlockedOnMissingPartnerData", + "label": "Block Windows if partner data unavailable (Note: Microsoft enforces this to on when Connect Windows 10.0.15063+ to MDE is on)", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.BlockunsupportedOS", + "label": "Block unsupported OS versions", + "defaultValue": false + }, + { + "type": "switch", + "name": "standards.DefenderCompliancePolicy.AllowMEMEnforceCompliance", + "label": "Allow MEM enforcement of compliance", + "defaultValue": false + } + ], + "label": "Defender for Endpoint - Intune Compliance Connector", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-04-02", + "powershellEquivalent": "Graph API - deviceManagement/mobileThreatDefenseConnectors", "recommendedBy": [] + }, + { + "name": "standards.GlobalQuarantineSettings", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Configures the Global Quarantine Policy settings including sender name, custom subject, disclaimer, from address, and org branding.", + "docsDescription": "Configures the Global Quarantine Policy branding and notification settings for the tenant. This includes the quarantine notification sender display name, custom subject line, disclaimer text, the from address used for notifications, and whether to use org branding. Notification frequency is managed separately by the GlobalQuarantineNotifications standard.", + "executiveText": "Ensures quarantine notification emails are branded and configured consistently, so end users receive clear, professional alerts about quarantined messages and know how to request release.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.GlobalQuarantineSettings.SenderName", + "label": "Sender Display Name (e.g. Contoso-Office365Alerts)", + "helperText": "Will be overridden if an active sender address with an existing display name is used.", + "required": false + }, + { + "type": "textField", + "name": "standards.GlobalQuarantineSettings.CustomSubject", + "label": "Subject", + "required": false + }, + { + "type": "textField", + "name": "standards.GlobalQuarantineSettings.CustomDisclaimer", + "label": "Disclaimer (max 200 characters)", + "required": false + }, + { + "type": "textField", + "name": "standards.GlobalQuarantineSettings.FromAddress", + "label": "Specify Sender Address (must be an internal mailbox)", + "required": false + }, + { + "type": "switch", + "name": "standards.GlobalQuarantineSettings.OrganizationBrandingEnabled", + "label": "Use Organization Branding (logo)", + "helperText": "Requires branding to be configured in the Microsoft 365 admin centre." + } + ], + "label": "Configure Global Quarantine Notification Settings", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-04-02", + "powershellEquivalent": "Set-QuarantinePolicy (GlobalQuarantinePolicy)", + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, + { + "name": "standards.SPDisableCustomScripts", + "cat": "SharePoint Standards", + "tag": [], + "helpText": "Prevents users from running custom scripts on SharePoint and OneDrive sites. Custom scripts can modify site behaviors and bypass governance controls.", + "docsDescription": "Disables the ability to add and run custom scripts on SharePoint and OneDrive sites at the tenant level. When custom scripts are allowed, governance cannot be enforced, and the capabilities of inserted code cannot be scoped or blocked. Microsoft recommends using the SharePoint Framework instead of custom scripts.", + "executiveText": "Blocks custom scripts from being added to SharePoint and OneDrive sites, enforcing governance controls and preventing unscoped code execution. This aligns with Microsoft's Baseline Security Mode recommendation to permanently remove the ability to add new custom scripts, directing organizations to use the SharePoint Framework instead.", + "addedComponent": [], + "label": "Disable custom scripts on SharePoint sites", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-04-28", + "powershellEquivalent": "Set-SPOTenant -CustomScriptsRestrictMode $true", + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] + }, + { + "name": "standards.SPDisableStoreAccess", + "cat": "SharePoint Standards", + "tag": [], + "helpText": "Disables end users from installing applications from the Microsoft Store into SharePoint sites.", + "docsDescription": "Removes the ability for end users to install applications directly from the Microsoft Store into SharePoint. This prevents uncontrolled app installations that can increase governance costs and go against organizational policies.", + "executiveText": "Prevents end users from installing applications from the Microsoft Store into SharePoint sites, ensuring that only approved applications are available. This reduces governance overhead and aligns with Microsoft's Baseline Security Mode recommendations.", + "addedComponent": [], + "label": "Disable SharePoint Store access", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-04-28", + "powershellEquivalent": "Set-SPOTenant -DisableSharePointStoreAccess $true", + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] + }, + { + "name": "standards.DisableEWS", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Disables Exchange Web Services (EWS) organization-wide. This reduces the attack surface by blocking legacy API access to mailbox data. Warning: This may break Office web add-ins on builds older than 16.0.19127.", + "docsDescription": "Disables Exchange Web Services (EWS) at the organization level to reduce attack surface. EWS provides cross-platform API access to sensitive Exchange Online data such as emails, meetings, and contacts. If compromised, attackers can access confidential data, send phishing emails, or spoof identities. Disabling EWS also reduces legacy app usage and minimizes exploitable endpoints. Note that this may break first-party features including web add-ins for Word, Excel, PowerPoint, and Outlook on builds older than 16.0.19127.", + "executiveText": "Disables Exchange Web Services (EWS) across the organization to reduce attack surface and prevent legacy API access to sensitive mailbox data. This aligns with Microsoft's Baseline Security Mode recommendation to minimize exploitable endpoints while requiring updates to applications that depend on EWS.", + "addedComponent": [], + "label": "Disable Exchange Web Services", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-04-28", + "powershellEquivalent": "Set-OrganizationConfig -EwsEnabled $false", + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, + { + "name": "standards.OMEBranding", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Configures the branding applied to Microsoft Purview (OME) encrypted emails, including the logo, background color, and the text recipients see when viewing a protected message. [Read more](https://learn.microsoft.com/en-us/purview/add-your-organization-brand-to-encrypted-messages)", + "docsDescription": "Configures Office Message Encryption (OME) branding settings for the tenant default configuration. Allows organizations to apply a custom logo (via URL), background color, button text, and portal text to encrypted emails viewed by external recipients.", + "executiveText": "Applies organizational branding to encrypted emails so recipients see a professional, on-brand experience when viewing protected messages. Reinforces brand identity while preserving security compliance.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.OMEBranding.BackgroundColor", + "label": "Background Color - Optional", + "placeholder": "#ffffff", + "helpText": "The background color of the encrypted message wrapper. Enter an HTML hex color code (e.g. #ffffff) or a named color value (e.g. white).", + "required": false + }, + { + "type": "textField", + "name": "standards.OMEBranding.LogoUrl", + "label": "Logo Image URL - Optional (Less than 40kb 170x70 pixels)", + "placeholder": "https://example.com/logo.png or %CustomVarable%", + "helpText": "URL to your organization's logo displayed in the encrypted email and the reading portal. Supported formats: PNG, JPG, BMP, TIFF. Optimal size: 170x70 px, max 40 KB.", + "required": false + }, + { + "type": "textField", + "name": "standards.OMEBranding.IntroductionText", + "label": "Text next to the sender's name and email address - Optional", + "placeholder": "has sent you a secure message.", + "helpText": "Text that appears next to the sender's name and email address. Maximum 1024 characters.", + "required": false + }, + { + "type": "textField", + "name": "standards.OMEBranding.ReadButtonText", + "label": "Read Button Text - Optional", + "placeholder": "Read Secure Message.", + "helpText": "Text that appears on the 'Read Message' button. Maximum 1024 characters.", + "required": false + }, + { + "type": "textField", + "name": "standards.OMEBranding.EmailText", + "label": "Email Text below the button - Optional", + "placeholder": "Encrypted message from Contoso secure messaging system.", + "helpText": "Text that appears below the 'Read Message' button. Maximum 1024 characters.", + "required": false + }, + { + "type": "textField", + "name": "standards.OMEBranding.PrivacyStatementUrl", + "label": "Privacy Statement URL - Optional", + "placeholder": "https://contoso.com/privacystatement.html", + "helpText": "URL for the Privacy Statement link in the encrypted email notification. Leave blank to use Microsoft's default privacy statement.", + "required": false + }, + { + "type": "textField", + "name": "standards.OMEBranding.DisclaimerText", + "label": "Disclaimer Statement - Optional", + "placeholder": "This message is confidential for the use of the addressee only.", + "helpText": "Disclaimer statement shown in the email that contains the encrypted message. Maximum 1024 characters.", + "required": false + }, + { + "type": "textField", + "name": "standards.OMEBranding.PortalText", + "label": "Text appears at the top of the encrypted mail viewing portal - Optional", + "placeholder": "Contoso secure email portal.", + "helpText": "Text that appears at the top of the encrypted mail viewing portal. Maximum 128 characters.", + "required": false + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "name": "standards.OMEBranding.OTPEnabled", + "label": "One-Time Pass Code - Required", + "helpText": "Enable or disable authentication with a one-time pass code. When enabled, recipients without a Microsoft account can verify their identity via a code sent to their email.", + "options": [ + { + "label": "Enabled", + "value": true + }, + { + "label": "Disabled", + "value": false + } + ] + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "name": "standards.OMEBranding.SocialIdSignIn", + "label": "Social ID Sign-In - Required", + "helpText": "Enable or disable authentication with Microsoft, Google, or Yahoo identities. When enabled, recipients can sign in with an existing social account to view the encrypted message.", + "options": [ + { + "label": "Enabled", + "value": true + }, + { + "label": "Disabled", + "value": false + } + ] + } + ], + "label": "Configure Encrypted Message Branding (OME)", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-04-25", + "powershellEquivalent": "Set-OMEConfiguration", + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, + { + "name": "standards.EnforcePrivateGroups", + "cat": "Entra (AAD) Standards", + "tag": ["CIS M365 7.0.0 (1.2.1)"], + "appliesToTest": ["CIS_1_2_1"], + "helpText": "Sets all public Microsoft 365 groups to private automatically. Groups can be excluded by display name keyword.", + "docsDescription": "Ensures only organisation-managed or approved public groups exist by automatically switching public Microsoft 365 (Unified) groups to private visibility. Groups whose display name matches any of the configured exclusion keywords are left unchanged. This aligns with CIS M365 7.0.0 benchmark control 1.2.1.", + "executiveText": "Enforces private visibility on all Microsoft 365 groups to prevent unauthorised external access to group resources such as Teams, SharePoint sites, and Planner boards. Approved public groups can be excluded by name, ensuring governance while retaining flexibility for intentionally public collaboration spaces.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": true, + "required": false, + "label": "Exclude groups by display name keyword", + "name": "standards.EnforcePrivateGroups.ExcludedGroupNames" + } + ], + "label": "Enforce Private M365 Groups", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-06", + "powershellEquivalent": "Update-MgGroup -GroupId -Visibility Private", + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "SHAREPOINTENTERPRISE_GOV", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] + }, + { + "name": "standards.EmptyFilterIPAllowList", + "cat": "Defender Standards", + "tag": ["CIS M365 7.0.0 (2.1.12)"], + "appliesToTest": ["CIS_2_1_12"], + "helpText": "Ensures the connection filter IP allow list is not used. IPs on this list bypass spam, spoof, and authentication checks.", + "docsDescription": "IPs on the connection filter allow list bypass spam, spoof, and authentication checks. CIS recommends keeping this list empty to ensure all inbound email is properly scanned. This standard checks that the IPAllowList on the Default hosted connection filter policy is empty and can remediate by clearing it.", + "executiveText": "Ensures the Exchange Online connection filter IP allow list is empty, preventing any IP addresses from bypassing spam filtering, spoofing checks, and sender authentication. Keeping this list empty ensures all inbound email undergoes full security scanning, reducing the risk of phishing and malware delivery through trusted-but-compromised sources.", + "addedComponent": [], + "label": "Ensure connection filter IP allow list is empty", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-06", + "powershellEquivalent": "Set-HostedConnectionFilterPolicy -Identity Default -IPAllowList @()", + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, + { + "name": "standards.TeamsZAP", + "cat": "Defender Standards", + "tag": ["CIS M365 7.0.0 (2.4.4)"], + "appliesToTest": ["CIS_2_4_4"], + "helpText": "Ensures Zero-hour auto purge (ZAP) is enabled for Microsoft Teams, automatically removing malicious messages after delivery.", + "docsDescription": "Zero-hour auto purge (ZAP) for Microsoft Teams retroactively detects and neutralises malicious messages that have already been delivered in Teams chats. Enabling ZAP ensures that phishing, malware, and high confidence phishing messages are automatically purged even after initial delivery, aligning with CIS M365 7.0.0 benchmark control 2.4.4.", + "executiveText": "Enables Zero-hour auto purge for Microsoft Teams to automatically detect and remove malicious messages after delivery. This provides an additional layer of protection against phishing and malware that may bypass initial scanning, ensuring threats are neutralised even after they reach users.", + "addedComponent": [], + "label": "Ensure Zero-hour auto purge for Microsoft Teams is on", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-05-06", + "powershellEquivalent": "Set-TeamsProtectionPolicy -Identity 'Teams Protection Policy' -ZapEnabled $true", + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, + { + "name": "standards.CollaborationDomainRestriction", + "cat": "Entra (AAD) Standards", + "tag": ["CIS M365 7.0.0 (5.1.6.1)"], + "appliesToTest": ["CIS_5_1_6_1"], + "helpText": "Restricts B2B collaboration invitations to a specified list of allowed domains. If no domains are provided, the standard will alert and report on whether any domain restrictions are currently configured.", + "docsDescription": "By default, Microsoft Entra ID allows collaboration invitations to be sent to any external domain. CIS recommends restricting B2B collaboration invitations to only approved domains to reduce the risk of data exfiltration and unauthorized access. This standard checks the B2B management policy for an allow list of domains and can remediate by setting the allowed domains list.", + "executiveText": "Restricts external collaboration invitations to approved domains only, preventing users from sharing data with unapproved external organizations. This reduces the risk of data exfiltration and ensures that collaboration occurs only with trusted business partners.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.CollaborationDomainRestriction.allowedDomains", + "label": "Allowed domains (comma separated)", + "required": false, + "placeholder": "contoso.com, fabrikam.com" + } + ], + "label": "Restrict collaboration invitations to allowed domains only", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-06", + "powershellEquivalent": "Graph API PATCH https://graph.microsoft.com/beta/policies/b2bManagementPolicies/default", + "recommendedBy": ["CIS"] + }, + { + "name": "standards.IntuneAppTemplateDeploy", + "cat": "Intune Standards", + "tag": [], + "helpText": "Deploys selected Intune application templates to the tenant. Supports WinGet/Store apps, Office apps, Chocolatey apps, Win32 script apps, and MSP apps.", + "docsDescription": "Uses CIPP Intune Application Templates to deploy applications across tenants as a standard. Each template can contain multiple applications of different types which will be queued for deployment.", + "executiveText": "Automatically deploys approved Intune applications across all managed tenants, ensuring consistent software availability and reducing manual deployment overhead. Supports WinGet, Office, Chocolatey, Win32, and MSP application types.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "label": "Select Application Templates", + "name": "standards.IntuneAppTemplateDeploy.templateIds", + "api": { + "url": "/api/ListAppTemplates", + "labelField": "displayName", + "valueField": "GUID", + "queryKey": "StdIntuneAppTemplateList" + } + } + ], + "label": "Deploy Intune Application Template", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-23", + "powershellEquivalent": "Graph API - /deviceAppManagement/mobileApps", + "recommendedBy": [] + }, + { + "name": "standards.AutopatchGroup", + "cat": "Intune Standards", + "tag": [], + "beta": true, + "deprecated": true, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, + "helpText": "Deploys a Windows Autopatch group with configurable deployment ring settings for quality updates, feature updates, Edge, and Office.", + "docsDescription": "Creates or updates a Windows Autopatch deployment group with Test and Last deployment rings. Configures quality update deferrals, feature update targeting, Edge and Office update channels per ring. Uses the Autopatch API proxy to manage the group configuration.", + "executiveText": "Configures Windows Autopatch deployment groups to manage update delivery across devices. Autopatch automates Windows quality updates, feature updates, Edge, and Office updates using deployment rings with configurable deferrals and deadlines.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.AutopatchGroup.GroupName", + "label": "Group Name", + "required": true, + "defaultValue": "Autopatch default group" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TargetOSVersion", + "label": "Target OS Version", + "required": true, + "options": [ + { "label": "Windows 11, version 24H2", "value": "Windows 11, version 24H2" }, + { "label": "Windows 11, version 25H2", "value": "Windows 11, version 25H2" } + ], + "defaultValue": "Windows 11, version 25H2" + }, + { + "type": "switch", + "name": "standards.AutopatchGroup.EnableDriverUpdate", + "label": "Enable Driver Updates", + "defaultValue": true + }, + { + "type": "switch", + "name": "standards.AutopatchGroup.InstallWin10OnWin11Ineligible", + "label": "Install latest Windows 10 on Windows 11 ineligible devices", + "defaultValue": false + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityDeferral", + "label": "Test Ring - Quality Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityDeadline", + "label": "Test Ring - Quality Update Deadline (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityGracePeriod", + "label": "Test Ring - Quality Update Grace Period (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 7, "message": "Maximum value is 7" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestFeatureDeferral", + "label": "Test Ring - Feature Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 365, "message": "Maximum value is 365" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestFeatureDeadline", + "label": "Test Ring - Feature Update Deadline (days)", + "defaultValue": 5, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TestEdgeChannel", + "label": "Test Ring - Edge Update Channel", + "options": [ + { "label": "Stable", "value": "Stable" }, + { "label": "Beta", "value": "Beta" }, + { "label": "Dev", "value": "Dev" } + ], + "defaultValue": "Beta" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TestOfficeChannel", + "label": "Test Ring - Office Update Channel", + "options": [ + { "label": "Current", "value": "Current" }, + { "label": "Monthly Enterprise", "value": "MonthlyEnterprise" }, + { "label": "Semi-Annual Enterprise", "value": "SemiAnnual" } + ], + "defaultValue": "MonthlyEnterprise" + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestDnfDeferral", + "label": "Test Ring - Driver & Firmware Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityDeferral", + "label": "Last Ring - Quality Update Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityDeadline", + "label": "Last Ring - Quality Update Deadline (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityGracePeriod", + "label": "Last Ring - Quality Update Grace Period (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 7, "message": "Maximum value is 7" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastFeatureDeferral", + "label": "Last Ring - Feature Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 365, "message": "Maximum value is 365" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastFeatureDeadline", + "label": "Last Ring - Feature Update Deadline (days)", + "defaultValue": 5, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.LastEdgeChannel", + "label": "Last Ring - Edge Update Channel", + "options": [ + { "label": "Stable", "value": "Stable" }, + { "label": "Beta", "value": "Beta" }, + { "label": "Dev", "value": "Dev" } + ], + "defaultValue": "Stable" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.LastOfficeChannel", + "label": "Last Ring - Office Update Channel", + "options": [ + { "label": "Current", "value": "Current" }, + { "label": "Monthly Enterprise", "value": "MonthlyEnterprise" }, + { "label": "Semi-Annual Enterprise", "value": "SemiAnnual" } + ], + "defaultValue": "MonthlyEnterprise" + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastOfficeDeferral", + "label": "Last Ring - Office Update Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastOfficeDeadline", + "label": "Last Ring - Office Update Deadline (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastDnfDeferral", + "label": "Last Ring - Driver & Firmware Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + } + ], + "label": "Deploy Windows Autopatch Group", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-05-27", + "powershellEquivalent": "Autopatch API - POST /api/autoPatch", + "recommendedBy": [] + }, + { + "name": "standards.FIDO2PasskeyProfiles", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Configures FIDO2 passkey profiles including AAGUID allowlists, attestation enforcement, and passkey types for the tenant.", + "docsDescription": "Manages FIDO2 passkey profiles on the tenant authentication methods policy. Allows defining passkey profiles that control which authenticators (hardware keys, password managers, Microsoft Authenticator) are permitted via AAGUID allowlists, whether attestation is enforced, and which passkey types (device-bound, synced, or both) are allowed. This enables MSPs to centrally deploy phishing-resistant MFA configurations across tenants.", + "executiveText": "Configures passkey (FIDO2) profiles that control which authenticators users can register for phishing-resistant MFA. Supports allowlisting specific hardware keys (e.g., YubiKey models), password managers (e.g., 1Password), and Microsoft Authenticator by AAGUID, with control over attestation enforcement and passkey types.", + "addedComponent": [ + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.PasskeyTypes", + "label": "Allowed Passkey Types", + "options": [ + { "label": "Device-bound only", "value": "deviceBound" }, + { "label": "Synced only", "value": "synced" }, + { "label": "Both device-bound and synced", "value": "deviceBound,synced" } + ], + "required": true + }, + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.AttestationEnforcement", + "label": "Attestation Enforcement", + "options": [ + { "label": "Disabled (required for synced passkeys)", "value": "disabled" }, + { "label": "Registration only", "value": "registrationOnly" } + ], + "required": true + }, + { + "type": "switch", + "name": "standards.FIDO2PasskeyProfiles.EnforceKeyRestrictions", + "label": "Enforce AAGUID Key Restrictions" + }, + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.EnforcementType", + "label": "Key Restriction Type", + "options": [ + { "label": "Allow listed AAGUIDs only", "value": "allow" }, + { "label": "Block listed AAGUIDs", "value": "block" } + ], + "required": false + }, + { + "type": "textField", + "name": "standards.FIDO2PasskeyProfiles.AAGUIDs", + "label": "AAGUIDs (comma-separated list of authenticator AAGUIDs)", + "required": false + } + ], + "label": "Configure FIDO2 Passkey Profile", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-25", + "powershellEquivalent": "Graph API PATCH /policies/authenticationMethodsPolicy/authenticationMethodConfigurations/fido2", + "recommendedBy": ["CIPP"] + }, + { + "name": "standards.SmartLockout", + "cat": "Entra (AAD) Standards", + "tag": ["CIS M365 7.0.0 (5.2.3.8)", "CIS M365 7.0.0 (5.2.3.9)"], + "appliesToTest": ["CIS_5_2_3_8", "CIS_5_2_3_9"], + "helpText": "**Requires Entra ID P1.** Configures the Entra ID Smart Lockout settings including lockout duration, lockout threshold, and on-premises integration mode.", + "docsDescription": "Configures the Entra ID Smart Lockout policy which protects against brute-force password attacks. Smart Lockout locks out bad actors who try to guess user passwords or use brute-force methods. It recognizes sign-ins from valid users and treats them differently from attackers. Settings include lockout duration (seconds), lockout threshold (failed attempts before lockout), and on-premises password protection mode (Audit or Enforced).", + "addedComponent": [ + { + "type": "number", + "name": "standards.SmartLockout.LockoutDurationInSeconds", + "label": "Lockout Duration (seconds)", + "default": 60, + "required": true + }, + { + "type": "number", + "name": "standards.SmartLockout.LockoutThreshold", + "label": "Lockout Threshold (failed attempts)", + "default": 10, + "required": true + }, + { + "type": "switch", + "name": "standards.SmartLockout.EnableBannedPasswordCheckOnPremises", + "label": "Enable On-Premises Password Protection" + }, + { + "type": "radio", + "name": "standards.SmartLockout.BannedPasswordCheckOnPremisesMode", + "label": "On-Premises Mode", + "options": [ + { "label": "Audit", "value": "Audit" }, + { "label": "Enforced", "value": "Enforced" } + ] + } + ], + "label": "Configure Entra ID Smart Lockout", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-05-27", + "powershellEquivalent": "Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting", + "recommendedBy": ["CIS"], + "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2"] + }, + { + "name": "standards.SPOVersionControl", + "cat": "SharePoint Standards", + "tag": [], + "helpText": "Configures SharePoint Online file versioning to either use automatic version trimming managed by Microsoft, or enforce a fixed major version limit with optional version expiration.", + "docsDescription": "Configures the SharePoint Online tenant-level file versioning policy. When automatic version trimming is enabled, Microsoft intelligently manages version cleanup. When disabled, you can set a fixed maximum number of major versions to retain and optionally expire versions after a specified number of days. This helps manage storage consumption while preserving version history as needed.", + "executiveText": "Controls how SharePoint Online manages file version history at the tenant level. Automatic trimming lets Microsoft optimize storage by cleaning up old versions intelligently. Manual limits give administrators precise control over the maximum number of versions retained and their expiration, ensuring predictable storage usage and compliance with data retention policies.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.SPOVersionControl.EnableAutoTrim", + "label": "Enable Automatic Version Trimming (Microsoft managed)" + }, + { + "type": "number", + "name": "standards.SPOVersionControl.MajorVersionLimit", + "label": "Maximum Major Versions (when auto trim is off)", + "default": 50 + }, + { + "type": "number", + "name": "standards.SPOVersionControl.ExpireVersionsAfterDays", + "label": "Expire Versions After Days (0 = never, when auto trim is off)", + "default": 0 + }, + { + "type": "switch", + "name": "standards.SPOVersionControl.ApplyToExistingSites", + "label": "Apply to all existing sites and document libraries" + } + ], + "label": "Set SharePoint File Version Limits", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-27", + "powershellEquivalent": "Set-SPOTenant -EnableAutoExpirationVersionTrim $true or Set-SPOTenant -EnableAutoExpirationVersionTrim $false -MajorVersionLimit 50 -ExpireVersionsAfterDays 365", + "recommendedBy": [], + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + } } ] From 49395156df423a2cc656607cd44a3d4a082568b3 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:17:01 +0800 Subject: [PATCH 07/14] repair GDAP role mapping actions --- .../Push-ExecOnboardTenantQueue.ps1 | 24 ++ .../Public/Test-CIPPGDAPGroupMappings.ps1 | 226 ++++++++++++++++++ .../Public/Test-CIPPGDAPRelationships.ps1 | 44 +++- .../GDAP/Invoke-ExecGDAPAccessAssignment.ps1 | 29 ++- .../Invoke-ExecGDAPRepairRoleMappings.ps1 | 74 ++++++ 5 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 Modules/CIPPCore/Public/Test-CIPPGDAPGroupMappings.ps1 create mode 100644 Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPRepairRoleMappings.ps1 diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index ead11b0ca7c0..05f2edba58ae 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -118,6 +118,30 @@ function Push-ExecOnboardTenantQueue { $OnboardingSteps.Step2.Status = 'succeeded' $OnboardingSteps.Step2.Message = 'Your GDAP relationship has the required roles' } + + # Validate (and correct) that the mapped security groups still exist in the partner tenant before + # Step 3 tries to POST the access assignments - a missing group surfaces as a raw Graph + # "access container does not exist" error otherwise. + if ($OnboardingSteps.Step2.Status -ne 'failed' -and ($Item.Roles | Measure-Object).Count -gt 0) { + $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Validating GDAP security group mappings against the partner tenant' }) + $GroupCheck = Test-CIPPGDAPGroupMappings -RoleMappings $Item.Roles -CreateMissing:([bool]$Item.AddMissingGroups) -WriteBack + foreach ($GroupResult in $GroupCheck.Results) { + if ($GroupResult.Status -in @('Stale', 'Created', 'Missing')) { + $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = $GroupResult.Message }) + } + } + # Use the corrected mappings for the remainder of the onboarding (group mapping, SAM membership, retries) + $Item.Roles = @($GroupCheck.RoleMappings) + + if (-not $GroupCheck.Valid) { + $MissingGroupNames = ($GroupCheck.MissingGroups.Name | Sort-Object -Unique) -join ', ' + $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = "Missing GDAP security groups in the partner tenant: $MissingGroupNames" }) + $TenantOnboarding.Status = 'failed' + $OnboardingSteps.Step2.Status = 'failed' + $OnboardingSteps.Step2.Message = "The following GDAP security groups are missing in the partner tenant, recreate the GDAP roles and retry: $MissingGroupNames" + } + } + $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop diff --git a/Modules/CIPPCore/Public/Test-CIPPGDAPGroupMappings.ps1 b/Modules/CIPPCore/Public/Test-CIPPGDAPGroupMappings.ps1 new file mode 100644 index 000000000000..ec23d6ba8fb1 --- /dev/null +++ b/Modules/CIPPCore/Public/Test-CIPPGDAPGroupMappings.ps1 @@ -0,0 +1,226 @@ +function Test-CIPPGDAPGroupMappings { + <# + .SYNOPSIS + Validate (and optionally repair) the security groups referenced by GDAP role mappings in the partner tenant. + + .DESCRIPTION + GDAP access assignments link a security group in the partner (CSP) tenant to a unified role. If the GroupId + stored in a role mapping no longer points at a real group, Graph rejects the access assignment with an + "access container does not exist" error. This helper fetches the partner tenant security groups once and, for + each mapping: + + 1. GroupId still resolves to a group -> kept as-is (Valid). + 2. GroupId is gone but a group with the expected name ("M365 GDAP " / the stored GroupName) exists + -> the mapping is resolved to that group's id (Stale - the stored id was stale). + 3. Neither exists -> recreated via the standard "M365 GDAP" group when -CreateMissing is set (Created), + otherwise reported as Missing with an actionable message instead of letting the raw Graph error surface. + + Corrections/creations can be persisted back to the GDAPRoles table (-WriteBack), a GDAP role template + (-TemplateId) and/or a GDAP invite entry (-InviteRowKey) so subsequent syncs use the corrected GroupIds. + + Returns the corrected mapping set, a per-mapping result list, the still-missing groups and an overall Valid flag. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)] + $RoleMappings, + $PartnerGroups, + [switch]$CreateMissing, + [switch]$WriteBack, + $TemplateId, + $InviteRowKey, + $APIName = 'GDAP Group Check', + $Headers + ) + + # Normalise input into a mutable copy so we never mutate the caller's objects in place + $Mappings = @(foreach ($Mapping in $RoleMappings) { + [PSCustomObject]@{ + RoleName = $Mapping.RoleName + GroupName = $Mapping.GroupName + GroupId = $Mapping.GroupId + roleDefinitionId = $Mapping.roleDefinitionId + } + }) + + if (($Mappings | Measure-Object).Count -eq 0) { + return [PSCustomObject]@{ + RoleMappings = @() + Results = @() + Valid = $true + MissingGroups = @() + } + } + + # Fetch partner tenant security groups once if the caller did not already hand them to us + if (-not $PartnerGroups) { + $PartnerGroups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$filter=securityEnabled eq true&$select=id,displayName&$top=999' -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true + } + + $Results = [System.Collections.Generic.List[object]]::new() + $MissingGroups = [System.Collections.Generic.List[object]]::new() + $Corrections = [System.Collections.Generic.List[object]]::new() + $CreateRequests = [System.Collections.Generic.List[object]]::new() + $CreateLookup = @{} + + foreach ($Mapping in $Mappings) { + $ExpectedName = if ($Mapping.GroupName) { $Mapping.GroupName } else { "M365 GDAP $($Mapping.RoleName)" } + + # 1. GroupId still valid in the partner tenant + if ($Mapping.GroupId -and $PartnerGroups.id -contains $Mapping.GroupId) { + $Results.Add([PSCustomObject]@{ + RoleName = $Mapping.RoleName + GroupName = $ExpectedName + GroupId = $Mapping.GroupId + Status = 'Valid' + Message = '' + OldGroupId = $null + }) + continue + } + + # 2. Remap to an existing group that matches the expected name + $MatchByName = $PartnerGroups | Where-Object { $_.displayName -eq $ExpectedName } | Select-Object -First 1 + if ($MatchByName) { + $OldGroupId = $Mapping.GroupId + $Mapping.GroupId = $MatchByName.id + $Mapping.GroupName = $MatchByName.displayName + $Results.Add([PSCustomObject]@{ + RoleName = $Mapping.RoleName + GroupName = $MatchByName.displayName + GroupId = $MatchByName.id + Status = 'Stale' + Message = "Group '$ExpectedName' exists but the stored group id '$OldGroupId' is stale; the correct group id is '$($MatchByName.id)'" + OldGroupId = $OldGroupId + }) + $Corrections.Add([PSCustomObject]@{ OldGroupId = $OldGroupId; Mapping = $Mapping }) + continue + } + + # 3. Neither the id nor a matching group exists - recreate or report as missing + if ($CreateMissing) { + $MailNickname = 'M365GDAP{0}' -f (($ExpectedName -replace '^M365 GDAP ', '') -replace '[^a-zA-Z0-9]', '') + $RequestId = "create-$($Mapping.roleDefinitionId)" + $CreateLookup[$RequestId] = $Mapping + $CreateRequests.Add(@{ + id = $RequestId + url = '/groups' + method = 'POST' + headers = @{ 'Content-Type' = 'application/json' } + body = @{ + displayName = $ExpectedName + description = "This group is used to manage M365 partner tenants at the $($Mapping.RoleName) level." + securityEnabled = $true + mailEnabled = $false + mailNickname = $MailNickname + } + }) + } else { + $Results.Add([PSCustomObject]@{ + RoleName = $Mapping.RoleName + GroupName = $ExpectedName + GroupId = $Mapping.GroupId + Status = 'Missing' + Message = "Group '$ExpectedName' is missing in the partner tenant, recreate the GDAP roles before retrying" + OldGroupId = $Mapping.GroupId + }) + $MissingGroups.Add([PSCustomObject]@{ Name = $ExpectedName; Type = 'Role Mapping' }) + } + } + + # Execute any group recreations and fold the new ids back into the mappings + if ($CreateRequests.Count -gt 0 -and $PSCmdlet.ShouldProcess('Partner tenant', "Recreate $($CreateRequests.Count) missing GDAP group(s)")) { + $CreateResults = New-GraphBulkRequest -Requests @($CreateRequests) -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true + foreach ($Result in $CreateResults) { + $Mapping = $CreateLookup[$Result.id] + if (-not $Mapping) { continue } + $ExpectedName = if ($Mapping.GroupName) { $Mapping.GroupName } else { "M365 GDAP $($Mapping.RoleName)" } + if ($Result.body.error) { + $Results.Add([PSCustomObject]@{ + RoleName = $Mapping.RoleName + GroupName = $ExpectedName + GroupId = $Mapping.GroupId + Status = 'Missing' + Message = "Failed to recreate group '$ExpectedName': $($Result.body.error.message)" + OldGroupId = $Mapping.GroupId + }) + $MissingGroups.Add([PSCustomObject]@{ Name = $ExpectedName; Type = 'Role Mapping' }) + } else { + $OldGroupId = $Mapping.GroupId + $Mapping.GroupId = $Result.body.id + $Mapping.GroupName = $Result.body.displayName + $Results.Add([PSCustomObject]@{ + RoleName = $Mapping.RoleName + GroupName = $Result.body.displayName + GroupId = $Result.body.id + Status = 'Created' + Message = "Recreated missing group '$($Result.body.displayName)' as '$($Result.body.id)'" + OldGroupId = $OldGroupId + }) + $Corrections.Add([PSCustomObject]@{ OldGroupId = $OldGroupId; Mapping = $Mapping }) + } + } + } + + # Persist corrected/created GroupIds back to the GDAPRoles registry (RowKey is the GroupId) + if ($WriteBack -and $Corrections.Count -gt 0) { + try { + $RolesTable = Get-CIPPTable -TableName 'GDAPRoles' + foreach ($Correction in $Corrections) { + $Mapping = $Correction.Mapping + if ($Correction.OldGroupId -and $Correction.OldGroupId -ne $Mapping.GroupId) { + $OldEntity = Get-CIPPAzDataTableEntity @RolesTable -Filter "PartitionKey eq 'Roles' and RowKey eq '$($Correction.OldGroupId)'" + if ($OldEntity) { + Remove-AzDataTableEntity -Force @RolesTable -Entity $OldEntity + } + } + Add-CIPPAzDataTableEntity @RolesTable -Entity @{ + PartitionKey = 'Roles' + RowKey = [string]$Mapping.GroupId + RoleName = [string]$Mapping.RoleName + GroupName = [string]$Mapping.GroupName + GroupId = [string]$Mapping.GroupId + roleDefinitionId = [string]$Mapping.roleDefinitionId + } -Force + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Failed to write corrected GDAP group mappings to GDAPRoles: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + } + } + + # Optionally push the corrected mappings back to the source template/invite + if ($Corrections.Count -gt 0) { + if ($TemplateId) { + try { + Add-CIPPGDAPRoleTemplate -TemplateId $TemplateId -RoleMappings ($Mappings | Select-Object -Property RoleName, GroupName, GroupId, roleDefinitionId) -Overwrite + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Failed to write corrected GDAP group mappings to template '$TemplateId': $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + } + } + if ($InviteRowKey) { + try { + $InviteTable = Get-CIPPTable -TableName 'GDAPInvites' + $Invite = Get-CIPPAzDataTableEntity @InviteTable -Filter "RowKey eq '$InviteRowKey'" + if ($Invite) { + $Invite.RoleMappings = [string](@($Mappings | Select-Object -Property RoleName, GroupName, GroupId, roleDefinitionId) | ConvertTo-Json -Depth 10 -Compress) + Add-CIPPAzDataTableEntity @InviteTable -Entity $Invite -Force + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Failed to write corrected GDAP group mappings to invite '$InviteRowKey': $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + } + } + } + + return [PSCustomObject]@{ + RoleMappings = @($Mappings) + Results = @($Results) + Valid = (($MissingGroups | Measure-Object).Count -eq 0) + MissingGroups = @($MissingGroups) + } +} diff --git a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 index 590fef5f8ddf..d0c801560be6 100644 --- a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 @@ -8,6 +8,7 @@ function Test-CIPPGDAPRelationships { $GDAPissues = [System.Collections.Generic.List[object]]@() $MissingGroups = [System.Collections.Generic.List[object]]@() + $RoleMappingResults = [System.Collections.Generic.List[object]]@() try { #Get graph request to list all relationships. $Relationships = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships?`$filter=status eq 'active'" -tenantid $env:TenantID -NoAuthCheck $true @@ -99,16 +100,51 @@ function Test-CIPPGDAPRelationships { }) | Out-Null } + # Validate that every stored GDAP role mapping still points at a group that exists in the partner tenant. + # A drifted/deleted GroupId is what causes the "access container does not exist" error during onboarding. + # Problems are added to GDAPIssues as errors (so they count toward the Errors total) and tagged with + # Category 'RoleMapping' so the frontend can keep them out of the GDAP Issues table (the detail/repair view + # lives in RoleMappingResults). + $RolesTable = Get-CIPPTable -TableName 'GDAPRoles' + $StoredRoleMappings = Get-CIPPAzDataTableEntity @RolesTable -Filter "PartitionKey eq 'Roles'" + if (($StoredRoleMappings | Measure-Object).Count -gt 0) { + # Read-only check: do not write back or recreate groups from the access check card + $MappingCheck = Test-CIPPGDAPGroupMappings -RoleMappings $StoredRoleMappings -Headers $Headers + $RoleMappingResults.AddRange(@($MappingCheck.Results)) + foreach ($MappingResult in $MappingCheck.Results) { + if ($MappingResult.Status -eq 'Missing') { + $GDAPissues.add([PSCustomObject]@{ + Type = 'Error' + Category = 'RoleMapping' + Issue = "The GDAP role mapping for '$($MappingResult.GroupName)' references a security group that no longer exists in the partner tenant. Onboarding group mapping will fail until the GDAP roles are recreated." + Tenant = '*Partner Tenant' + Relationship = 'None' + Link = 'https://docs.cipp.app/setup/installation/recommended-roles' + }) | Out-Null + } elseif ($MappingResult.Status -eq 'Stale') { + $GDAPissues.add([PSCustomObject]@{ + Type = 'Error' + Category = 'RoleMapping' + Issue = "The GDAP role mapping for '$($MappingResult.GroupName)' points at a stale group id but a matching group still exists. Use 'Repair Role Mappings' under Details to correct the stored group id." + Tenant = '*Partner Tenant' + Relationship = 'None' + Link = 'https://docs.cipp.app/setup/installation/recommended-roles' + }) | Out-Null + } + } + } + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Headers -API $APINAME -message "Failed to run GDAP check for $($TenantFilter): $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } $GDAPRelationships = [PSCustomObject]@{ - GDAPIssues = @($GDAPissues) - MissingGroups = @($MissingGroups) - Memberships = @($SAMUserMemberships + $NestedGroups) - CIPPGroupCount = $CIPPGroupCount + GDAPIssues = @($GDAPissues) + MissingGroups = @($MissingGroups) + Memberships = @($SAMUserMemberships + $NestedGroups) + CIPPGroupCount = $CIPPGroupCount + RoleMappingResults = @($RoleMappingResults) } $Table = Get-CIPPTable -TableName AccessChecks diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPAccessAssignment.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPAccessAssignment.ps1 index b2e3803ea41f..41155b7e5d81 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPAccessAssignment.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPAccessAssignment.ps1 @@ -48,9 +48,29 @@ function Invoke-ExecGDAPAccessAssignment { $Groups = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/groups?`$top=999&`$select=id,displayName&`$filter=securityEnabled eq true" -asApp $true -NoAuthCheck $true + # Validate/correct the template's group mappings against the partner tenant before creating any + # access assignments - a stale GroupId would otherwise fail with "access container does not exist". + $GroupCheck = Test-CIPPGDAPGroupMappings -RoleMappings $Mappings -PartnerGroups $Groups -WriteBack -TemplateId $RoleTemplateId -Headers $Request.Headers + $Mappings = $GroupCheck.RoleMappings + $Requests = [System.Collections.Generic.List[object]]::new() $Messages = [System.Collections.Generic.List[object]]::new() + $MappingResults = [System.Collections.Generic.List[object]]::new() + foreach ($GroupResult in $GroupCheck.Results) { + if ($GroupResult.Status -eq 'Stale') { + $MappingResults.Add(@{ resultText = $GroupResult.Message; state = 'success' }) + } elseif ($GroupResult.Status -eq 'Missing') { + $MappingResults.Add(@{ resultText = $GroupResult.Message; state = 'error' }) + } + } + + # Drop mappings whose group could not be resolved so we never POST a non-existent access container + $MissingGroupIds = @($GroupCheck.Results | Where-Object { $_.Status -eq 'Missing' } | Select-Object -ExpandProperty GroupId) + if ($MissingGroupIds.Count -gt 0) { + $Mappings = @($Mappings | Where-Object { $_.GroupId -notin $MissingGroupIds }) + } + foreach ($AccessAssignment in $AccessAssignments) { $RoleCount = ($AccessAssignment.accessDetails.unifiedRoles | Measure-Object).Count if ($Mappings.GroupId -notcontains $AccessAssignment.accessContainer.accessContainerId -and $AccessAssignment.status -notin @('deleting', 'deleted', 'error')) { @@ -159,11 +179,18 @@ function Invoke-ExecGDAPAccessAssignment { } } - } else { + } elseif ($MappingResults.Count -eq 0) { $Results = @{ resultText = 'This relationship already has the correct access assignments' state = 'success' } + } else { + $Results = @() + } + + # Surface any group mapping corrections / missing groups alongside the assignment changes + if ($MappingResults.Count -gt 0) { + $Results = @($MappingResults) + @($Results) } $Body = @{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPRepairRoleMappings.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPRepairRoleMappings.ps1 new file mode 100644 index 000000000000..2421e3a14218 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPRepairRoleMappings.ps1 @@ -0,0 +1,74 @@ +function Invoke-ExecGDAPRepairRoleMappings { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.Relationship.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[object]]::new() + + try { + # Fetch the partner tenant security groups once and reuse them for every store we repair + $PartnerGroups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$filter=securityEnabled eq true&$select=id,displayName&$top=999' -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true + + # Repair the GDAPRoles registry (stale group ids are remapped to the existing "M365 GDAP" group) + $RolesTable = Get-CIPPTable -TableName 'GDAPRoles' + $StoredRoles = Get-CIPPAzDataTableEntity @RolesTable -Filter "PartitionKey eq 'Roles'" + if (($StoredRoles | Measure-Object).Count -gt 0) { + $RoleCheck = Test-CIPPGDAPGroupMappings -RoleMappings $StoredRoles -PartnerGroups $PartnerGroups -WriteBack -APIName $APIName -Headers $Headers + foreach ($Result in $RoleCheck.Results) { + if ($Result.Status -eq 'Stale') { + $Results.Add(@{ resultText = "GDAP Roles: $($Result.Message)"; state = 'success' }) + } elseif ($Result.Status -eq 'Missing') { + $Results.Add(@{ resultText = "GDAP Roles: $($Result.Message)"; state = 'error' }) + } + } + } + + # Repair every saved role template so onboarding/reset use the corrected group ids + $TemplatesTable = Get-CIPPTable -TableName 'GDAPRoleTemplates' + $Templates = Get-CIPPAzDataTableEntity @TemplatesTable -Filter "PartitionKey eq 'RoleTemplate'" + foreach ($Template in $Templates) { + try { + $TemplateMappings = $Template.RoleMappings | ConvertFrom-Json + } catch { + $TemplateMappings = @() + } + if (($TemplateMappings | Measure-Object).Count -eq 0) { continue } + + $TemplateCheck = Test-CIPPGDAPGroupMappings -RoleMappings $TemplateMappings -PartnerGroups $PartnerGroups -TemplateId $Template.RowKey -APIName $APIName -Headers $Headers + foreach ($Result in $TemplateCheck.Results) { + if ($Result.Status -eq 'Stale') { + $Results.Add(@{ resultText = "Template '$($Template.RowKey)': $($Result.Message)"; state = 'success' }) + } elseif ($Result.Status -eq 'Missing') { + $Results.Add(@{ resultText = "Template '$($Template.RowKey)': $($Result.Message)"; state = 'error' }) + } + } + } + + if ($Results.Count -eq 0) { + $Results.Add(@{ resultText = 'All GDAP role mappings already reference existing security groups'; state = 'success' }) + } + + # Refresh the cached GDAP access check so the card reflects the repair immediately + $null = Test-CIPPGDAPRelationships -Headers $Headers + + Write-LogMessage -headers $Headers -API $APIName -message 'Repaired GDAP role mappings' -Sev 'Info' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results.Add(@{ resultText = "Failed to repair GDAP role mappings: $($ErrorMessage.NormalizedError)"; state = 'error' }) + Write-LogMessage -headers $Headers -API $APIName -message "Failed to repair GDAP role mappings: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ Results = @($Results) } + }) +} From 2c63d3174d06ef71b3540a08e9db4db976c5c332 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:29:46 +0800 Subject: [PATCH 08/14] Schema backed standards repair action, repaired standards are prefixed with repaired and have all remediation actions disabled --- .../Tools/Repair-CippStandardsTemplate.ps1 | 247 ++++++++++++++++++ .../Invoke-listStandardTemplates.ps1 | 25 +- 2 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 Modules/CIPPCore/Public/Tools/Repair-CippStandardsTemplate.ps1 diff --git a/Modules/CIPPCore/Public/Tools/Repair-CippStandardsTemplate.ps1 b/Modules/CIPPCore/Public/Tools/Repair-CippStandardsTemplate.ps1 new file mode 100644 index 000000000000..e1f59d93e3d0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tools/Repair-CippStandardsTemplate.ps1 @@ -0,0 +1,247 @@ +function Repair-CippStandardsTemplate { + <# + .SYNOPSIS + Recovers a standards template whose JSON failed to parse because of case-insensitive + duplicate property names, using the standards catalog to decide which key is real, then + marks the repaired template as safe-by-default. Returns the repaired JSON string, or THROWS + a descriptive error if it cannot be safely recovered. + .DESCRIPTION + PowerShell's ConvertFrom-Json treats property names case-insensitively and throws + ("...keys with different casing" / "...duplicated keys") when a single object contains two + names that differ only by case. The known offender is the legacy 'calDefault' standard, + which was saved with both 'permissionlevel' and 'permissionLevel'. + + This is a targeted recovery routine - call it ONLY from a ConvertFrom-Json catch block. It + reparses with System.Text.Json (which tolerates duplicate property names) and, for every + object that has case-colliding keys, consults the standards catalog (Config\standards.json) + for the owning standard. The colliding key whose exact casing matches a real catalog field + is kept; the unrecognised duplicate is dropped. So calDefault keeps 'permissionLevel' + (a real field) and drops the corrupt 'permissionlevel' - rather than blindly guessing. + + Because the repair makes a best-effort choice about corrupt data, the recovered template is + also neutered so it cannot silently start remediating from an auto-fixed config: + - templateName is prefixed with "(repaired) ". + - Drift templates (type -eq 'drift'): autoRemediate is forced to $false on every standard. + - Regular templates: runManually is forced to $true (the schedule is disabled). + + Non-colliding fields are otherwise untouched, so there is no risk of dropping legitimately + stored data that the catalog does not enumerate. Arrays, numbers, nulls and nesting are + preserved exactly (single-element arrays are NOT collapsed). + + If a collision cannot be resolved from the catalog (unknown standard / neither casing is a + known field), or the JSON is malformed beyond duplicate keys, this function THROWS a + descriptive terminating error rather than guessing. The caller is expected to log it and + omit the whole template from the response. + .PARAMETER Json + The raw JSON string that failed to parse. + .PARAMETER Reference + Optional identifier (e.g. RowKey or template name) included in error/log context. + .EXAMPLE + try { + $Data = $JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop + } catch { + try { $RepairedJson = Repair-CippStandardsTemplate -Json $JSON -Reference $RowKey } + catch { Write-LogMessage ... -message "Template $RowKey omitted: $($_.Exception.Message)" -Sev Error; return } + $Data = $RepairedJson | ConvertFrom-Json -Depth 100 + } + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Json, + [string]$Reference + ) + + if ([string]::IsNullOrWhiteSpace($Json)) { + throw 'Template JSON is empty.' + } + + # Tolerant reparse. System.Text.Json permits duplicate property names; if even this fails the + # record is malformed beyond the known duplicate-key issue and is genuinely unrecoverable. + try { + $doc = [System.Text.Json.JsonDocument]::Parse($Json) + } catch { + throw "Malformed JSON, not recoverable: $($_.Exception.Message)" + } + + $Schema = Get-CippStandardFieldSchema + + try { + # Determine drift vs regular (matches CIPP: $Template.type -eq 'drift') to decide how to + # neuter the repaired template. + $IsDrift = $false + if ($doc.RootElement.ValueKind -eq 'Object') { + foreach ($p in $doc.RootElement.EnumerateObject()) { + if ($p.Name -eq 'type' -and $p.Value.ValueKind -eq 'String' -and $p.Value.GetString() -eq 'drift') { + $IsDrift = $true; break + } + } + } + + $stream = [System.IO.MemoryStream]::new() + $writer = [System.Text.Json.Utf8JsonWriter]::new($stream) + try { + # Write-CippCleanJsonElement throws if it finds a collision it cannot resolve. + Write-CippCleanJsonElement -Element $doc.RootElement -Writer $writer -Schema $Schema -IsDrift $IsDrift -Context 'root' + $writer.Flush() + $clean = [System.Text.Encoding]::UTF8.GetString($stream.ToArray()) + } finally { $writer.Dispose() } + } finally { $doc.Dispose() } + + # Validate the repaired JSON parses cleanly before handing it back; if not, it's unrecoverable. + try { + $null = $clean | ConvertFrom-Json -Depth 100 -ErrorAction Stop + } catch { + throw "Still unreadable after de-duplicating keys: $($_.Exception.Message)" + } + return $clean +} + +function Get-CippStandardFieldSchema { + # Builds (and caches) a map of standard name -> set of valid field names (canonical casing), + # derived from the addedComponent definitions in Config\standards.json. Used only to decide + # which member of a case-colliding key pair is the legitimate one. + if ($script:CippStandardFieldSchema) { return $script:CippStandardFieldSchema } + + $map = @{} + try { + $Path = Join-Path $env:CIPPRootPath 'Config\standards.json' + if (Test-Path $Path) { + $Catalog = Get-Content $Path -Raw | ConvertFrom-Json -Depth 20 + foreach ($Std in $Catalog) { + if (-not $Std.name -or $Std.name -notlike 'standards.*') { continue } + $StandardKey = $Std.name.Substring('standards.'.Length).ToLowerInvariant() + $Fields = [System.Collections.Generic.HashSet[string]]::new() + $Prefix = "$($Std.name)." + foreach ($Component in @($Std.addedComponent)) { + if (-not $Component.name) { continue } + if ($Component.name -like "$Prefix*") { + foreach ($Segment in ($Component.name.Substring($Prefix.Length) -split '\.')) { + if ($Segment) { [void]$Fields.Add($Segment) } + } + } + } + $map[$StandardKey] = $Fields + } + } else { + Write-Host "Get-CippStandardFieldSchema: standards catalog not found at $Path" + } + } catch { + Write-Host "Get-CippStandardFieldSchema: failed to build schema: $($_.Exception.Message)" + } + + $script:CippStandardFieldSchema = $map + return $map +} + +function Write-CippCleanJsonElement { + # Internal helper for Repair-CippStandardsTemplate. Recursively rewrites a JsonElement, + # resolving case-insensitive duplicate property names by keeping the catalog-valid casing + # (throws if it can't), and neutering the repaired template so it won't auto-remediate: + # renames templateName, forces runManually (regular) or autoRemediate=false (drift). + param( + [System.Text.Json.JsonElement]$Element, + [System.Text.Json.Utf8JsonWriter]$Writer, + [hashtable]$Schema, + [bool]$IsDrift = $false, + [System.Collections.Generic.HashSet[string]]$ValidFields = $null, + [string]$StandardName = $null, + [string]$Context = 'root' + ) + switch ($Element.ValueKind) { + 'Object' { + $Writer.WriteStartObject() + $props = @($Element.EnumerateObject()) + + # Group property indices by case-insensitive name to detect collisions. + $byCi = @{} + for ($i = 0; $i -lt $props.Count; $i++) { + $ci = $props[$i].Name.ToLowerInvariant() + if (-not $byCi.ContainsKey($ci)) { $byCi[$ci] = [System.Collections.Generic.List[int]]::new() } + [void]$byCi[$ci].Add($i) + } + + # For each collision keep the catalog-valid casing; throw if it can't be resolved. + $keep = @{} + foreach ($ci in $byCi.Keys) { + $indices = $byCi[$ci] + if ($indices.Count -eq 1) { $keep[$ci] = $indices[0]; continue } + $chosen = $null + if ($ValidFields) { + foreach ($idx in $indices) { + if ($ValidFields.Contains($props[$idx].Name)) { $chosen = $idx; break } + } + } + if ($null -eq $chosen) { + $where = if ($StandardName) { "standard '$StandardName'" } else { 'the template root' } + $variants = ($indices | ForEach-Object { "'$($props[$_].Name)'" }) -join ', ' + throw "Unresolvable duplicate property '$ci' ($variants) in $where - no matching field in the standards catalog to determine the correct value." + } + $keep[$ci] = $chosen + } + + # Safety-neutering overrides to apply to THIS object. + $forceBool = @{} # canonical name -> bool value to force + if ($Context -eq 'root' -and -not $IsDrift) { $forceBool['runManually'] = $true } + elseif ($Context -eq 'standardEntry' -and $IsDrift) { $forceBool['autoRemediate'] = $false } + $forceLower = @{} + foreach ($k in $forceBool.Keys) { $forceLower[$k.ToLowerInvariant()] = $k } + $pending = [System.Collections.Generic.List[string]]@($forceBool.Keys) + + for ($i = 0; $i -lt $props.Count; $i++) { + $ci = $props[$i].Name.ToLowerInvariant() + if ($keep[$ci] -ne $i) { continue } + + $name = $props[$i].Name + + # Force a boolean value (e.g. runManually / autoRemediate) over the stored one. + if ($forceLower.ContainsKey($ci)) { + $Writer.WriteBoolean($name, [bool]$forceBool[$forceLower[$ci]]) + [void]$pending.Remove($forceLower[$ci]) + continue + } + + # Prefix the template name so it's obvious it was auto-repaired. + if ($Context -eq 'root' -and $ci -eq 'templatename' -and $props[$i].Value.ValueKind -eq 'String') { + $orig = $props[$i].Value.GetString() + $newName = if ($orig.StartsWith('(repaired) ')) { $orig } else { "(repaired) $orig" } + $Writer.WriteString($name, $newName) + continue + } + + $Writer.WritePropertyName($name) + + # Track which standard we are inside so deeper collisions can be resolved/reported + # and so per-standard neutering can be applied at the standard entry object. + $childValid = $ValidFields + $childStandard = $StandardName + $childContext = 'inside' + if ($Context -eq 'root' -and $name -eq 'standards') { + $childContext = 'container' + $childValid = $null + } elseif ($Context -eq 'container') { + $childValid = $Schema[$ci] + $childStandard = $name + $childContext = 'standardEntry' + } + Write-CippCleanJsonElement -Element $props[$i].Value -Writer $Writer -Schema $Schema -IsDrift $IsDrift -ValidFields $childValid -StandardName $childStandard -Context $childContext + } + + # Inject any forced property that wasn't present in the stored object. + foreach ($missing in $pending) { + $Writer.WriteBoolean($missing, [bool]$forceBool[$missing]) + } + + $Writer.WriteEndObject() + } + 'Array' { + $Writer.WriteStartArray() + foreach ($item in $Element.EnumerateArray()) { + Write-CippCleanJsonElement -Element $item -Writer $Writer -Schema $Schema -IsDrift $IsDrift -ValidFields $ValidFields -StandardName $StandardName -Context 'inside' + } + $Writer.WriteEndArray() + } + default { $Element.WriteTo($Writer) } + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 index c7977465b7e0..8f844ddb9de7 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 @@ -14,14 +14,29 @@ function Invoke-listStandardTemplates { $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'StandardsTemplateV2'" $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $RowKey = $_.RowKey $JSON = $_.JSON -replace '"Action":', '"action":' try { - $RowKey = $_.RowKey - $Data = $JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - + $Data = $JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop } catch { - Write-Host "$($RowKey) standard could not be loaded: $($_.Exception.Message)" - return + try { + $RepairedJSON = Repair-CippStandardsTemplate -Json $JSON -Reference $RowKey + } catch { + Write-LogMessage -headers $Request.Headers -API 'Standards' -message "Standards template '$($RowKey)' was omitted from the response: $($_.Exception.Message)" -Sev 'Error' + return + } + $Data = $RepairedJSON | ConvertFrom-Json -Depth 100 + try { + $null = Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$RepairedJSON" + RowKey = "$RowKey" + PartitionKey = 'StandardsTemplateV2' + GUID = "$RowKey" + } -Force + Write-LogMessage -headers $Request.Headers -API 'Standards' -message "Standards template '$($RowKey)' contained corrupt data (case-duplicate keys) and was automatically repaired and re-saved." -Sev 'Warning' + } catch { + Write-LogMessage -headers $Request.Headers -API 'Standards' -message "Standards template '$($RowKey)' was repaired for this response but could not be re-saved: $($_.Exception.Message)" -Sev 'Warning' + } } if ($Data) { $Data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.GUID -Force From 504c21ae37e56ef01c22a190275453ea98374a68 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 17 Jun 2026 16:43:13 -0400 Subject: [PATCH 09/14] fix: patch command for updating redirect uri --- .../Public/Entrypoints/HTTP Functions/Invoke-ExecListAppId.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecListAppId.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecListAppId.ps1 index 2c93e87596c1..69a66fb92bcd 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecListAppId.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecListAppId.ps1 @@ -91,7 +91,7 @@ function Invoke-ExecListAppId { redirectUris = $RedirectUris } } | ConvertTo-Json -Depth 10 - Invoke-GraphRequest -Method PATCH -Url "https://graph.microsoft.com/v1.0/applications/$($AppResponse.body.id)" -Body $AppUpdateBody -tenantid $env:TenantID -NoAuthCheck $true + $null = New-GraphPOSTRequest -type PATCH -Uri "https://graph.microsoft.com/v1.0/applications/$($AppResponse.body.id)" -Body $AppUpdateBody -tenantid $env:TenantID -NoAuthCheck $true Write-LogMessage -message "Updated redirect URIs for application $($env:ApplicationID) to include $NewRedirectUri" -Sev 'Info' } catch { Write-LogMessage -message "Failed to update redirect URIs for application $($env:ApplicationID)" -LogData (Get-CippException -Exception $_) -sev 'Warning' From 1164290345bb51f42aaae5a87d9d2092e8121d33 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:29:53 +0200 Subject: [PATCH 10/14] remove incorrectly added permissions --- Config/AdditionalPermissions.json | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Config/AdditionalPermissions.json b/Config/AdditionalPermissions.json index 8279e8bdca95..ed363855ceac 100644 --- a/Config/AdditionalPermissions.json +++ b/Config/AdditionalPermissions.json @@ -60,26 +60,5 @@ "type": "Scope" } ] - }, - { - "resourceAppId": "00000003-0000-0000-c000-000000000000", - "resourceAccess": [ - { - "id": "CopilotPolicySettings.ReadWrite", - "type": "Scope" - }, - { - "id": "CopilotSettings-LimitedMode.ReadWrite", - "type": "Scope" - }, - { - "id": "CopilotPackages.Read.All", - "type": "Scope" - }, - { - "id": "CopilotPackages.ReadWrite.All", - "type": "Scope" - } - ] } ] \ No newline at end of file From fa8a5894cd77b1cc6d410e2554ee0b295e8d3d84 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 17 Jun 2026 23:10:58 -0400 Subject: [PATCH 11/14] chore: add logging for onboarding --- .../Push-ExecOnboardTenantQueue.ps1 | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index 05f2edba58ae..8b4dddb20c1e 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -7,6 +7,7 @@ function Push-ExecOnboardTenantQueue { param($Item) try { $Id = $Item.id + Write-Information "Onboarding: Starting for relationship $Id" $Start = Get-Date $Logs = [System.Collections.Generic.List[object]]::new() $OnboardTable = Get-CIPPTable -TableName 'TenantOnboarding' @@ -61,6 +62,7 @@ function Push-ExecOnboardTenantQueue { $x++ Start-Sleep -Seconds 30 } while ($Relationship.status -ne 'active' -and $x -lt 6) + Write-Information "Onboarding: Step1 poll completed - status=$($Relationship.status) attempts=$x" if ($Relationship.status -eq 'active') { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'GDAP Invite Accepted' }) @@ -145,6 +147,7 @@ function Push-ExecOnboardTenantQueue { $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop + Write-Information "Onboarding: Step2 completed - status=$($OnboardingSteps.Step2.Status) missingRoles=$($MissingRoles -join ',')" } if ($OnboardingSteps.Step2.Status -eq 'succeeded') { @@ -328,43 +331,57 @@ function Push-ExecOnboardTenantQueue { $TenantOnboarding.OnboardingSteps = [string](ConvertTo-Json -InputObject $OnboardingSteps -Compress) $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop + Write-Information "Onboarding: Step3 completed - status=$($OnboardingSteps.Step3.Status)" } if ($OnboardingSteps.Step3.Status -eq 'succeeded') { # Check if the relationship was recently activated — Microsoft propagation may not have settled yet if ($Relationship.activatedDateTime) { + $MinutesSinceActivation = $null try { $ActivatedTimeUtc = ([DateTimeOffset]$Relationship.activatedDateTime).UtcDateTime $MinutesSinceActivation = ([datetime]::UtcNow - $ActivatedTimeUtc).TotalMinutes - if ($MinutesSinceActivation -lt 15) { - $RetryAtUtc = [Cronos.CronExpression]::Parse('* * * * *').GetNextOccurrence([DateTime]::UtcNow.AddMinutes(15), [TimeZoneInfo]::Utc) - $RetryEpoch = ([DateTimeOffset]$RetryAtUtc).ToUnixTimeSeconds() - $RetryDelayMinutes = ($RetryAtUtc - [DateTime]::UtcNow).TotalMinutes - $MinutesSinceActivationDisplay = ('{0:N1}' -f $MinutesSinceActivation) - $RetryDelayMinutesDisplay = ('{0:N1}' -f $RetryDelayMinutes) - $RetryLogMessage = "GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Rescheduling onboarding in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle." - $Logs.Add([PSCustomObject]@{ - Date = (Get-Date).ToUniversalTime() - Log = $RetryLogMessage - }) - $RetryParams = [PSCustomObject]@{ - Item = [PSCustomObject]@{ - id = $Item.id - Roles = $Item.Roles - AutoMapRoles = $Item.AutoMapRoles - IgnoreMissingRoles = $Item.IgnoreMissingRoles - StandardsExcludeAllTenants = $Item.StandardsExcludeAllTenants - } - } - $RetryTask = [PSCustomObject]@{ - Name = "GDAP Onboarding retry: $($Relationship.customer.displayName)" - Command = [PSCustomObject]@{ value = 'Push-ExecOnboardTenantQueue' } - Parameters = $RetryParams - TenantFilter = $env:TenantID - Recurrence = '' - ScheduledTime = $RetryEpoch + } catch { + Write-Warning "Failed to parse activatedDateTime for relationship ${Id}: $($_.Exception.Message)" + } + Write-Information "Onboarding: activatedDateTime=$($Relationship.activatedDateTime) minutesSinceActivation=$MinutesSinceActivation" + if ($null -ne $MinutesSinceActivation -and $MinutesSinceActivation -lt 15) { + $RetryAtUtc = [Cronos.CronExpression]::Parse('* * * * *').GetNextOccurrence([DateTime]::UtcNow.AddMinutes(15), [TimeZoneInfo]::Utc) + $RetryEpoch = ([DateTimeOffset]$RetryAtUtc).ToUnixTimeSeconds() + $RetryDelayMinutes = ($RetryAtUtc - [DateTime]::UtcNow).TotalMinutes + $MinutesSinceActivationDisplay = ('{0:N1}' -f $MinutesSinceActivation) + $RetryDelayMinutesDisplay = ('{0:N1}' -f $RetryDelayMinutes) + $RetryParams = [PSCustomObject]@{ + Item = [PSCustomObject]@{ + id = $Item.id + Roles = $Item.Roles + AutoMapRoles = $Item.AutoMapRoles + IgnoreMissingRoles = $Item.IgnoreMissingRoles + AddMissingGroups = $Item.AddMissingGroups + StandardsExcludeAllTenants = $Item.StandardsExcludeAllTenants } - $null = Add-CIPPScheduledTask -Task $RetryTask -DesiredStartTime ([string]$RetryEpoch) + } + $RetryTask = [PSCustomObject]@{ + Name = "GDAP Onboarding retry: $($Relationship.customer.displayName)" + Command = [PSCustomObject]@{ value = 'Push-ExecOnboardTenantQueue' } + Parameters = $RetryParams + TenantFilter = $env:TenantID + Recurrence = '' + ScheduledTime = $RetryEpoch + } + try { + $ScheduleResult = Add-CIPPScheduledTask -Task $RetryTask -DesiredStartTime ([string]$RetryEpoch) + } catch { + $ScheduleResult = "Error - $($_.Exception.Message)" + } + Write-Information "Onboarding: Add-CIPPScheduledTask result=$ScheduleResult" + if ($ScheduleResult -match '^Error') { + $FailMessage = "Failed to schedule onboarding retry for $($Relationship.customer.displayName): $ScheduleResult" + $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = $FailMessage }) + Write-LogMessage -API 'Onboarding' -message $FailMessage -Sev 'Error' + } else { + $RetryLogMessage = "GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Rescheduling onboarding in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle." + $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = $RetryLogMessage }) $RetryMessage = "Rescheduled: GDAP relationship was activated $MinutesSinceActivationDisplay minutes ago. Retrying in $RetryDelayMinutesDisplay minutes to allow Microsoft propagation to settle." $OnboardingSteps.Step4.Status = 'pending' $OnboardingSteps.Step4.Message = $RetryMessage @@ -375,8 +392,6 @@ function Push-ExecOnboardTenantQueue { Write-LogMessage -API 'Onboarding' -message $RetryMessage -Sev 'Info' return } - } catch { - Write-Warning "Failed to check activatedDateTime for relationship ${Id}: $($_.Exception.Message)" } } @@ -445,6 +460,7 @@ function Push-ExecOnboardTenantQueue { } } while ($Refreshing -and (Get-Date) -lt $Start.AddMinutes(8)) + Write-Information "Onboarding: CPV refresh loop completed - success=$CPVSuccess lastError=$LastCPVError" if ($CPVSuccess) { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'CPV permissions refreshed' }) $OnboardingSteps.Step4.Status = 'succeeded' @@ -558,6 +574,7 @@ function Push-ExecOnboardTenantQueue { $ApiException = $_ } + Write-Information "Onboarding: Step5 API test completed - userCount=$UserCount apiError=$ApiError" if ($UserCount -gt 0) { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'API test successful' }) $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Onboarding complete' }) From 942ff39a7a8067b7b70a3a25529862f9a5638fb1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 17 Jun 2026 23:29:44 -0400 Subject: [PATCH 12/14] chore: consolidate graph log message --- Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index 689285e9019e..c69c34636766 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -38,7 +38,6 @@ function New-GraphPOSTRequest { if (!$headers['User-Agent']) { $headers['User-Agent'] = Get-CippUserAgent - Write-Information "User-Agent: $($headers['User-Agent'])" } if (!$contentType) { @@ -50,7 +49,7 @@ function New-GraphPOSTRequest { $RawErrorBody = $null do { try { - Write-Information "$($type.ToUpper()) [ $uri ] | tenant: $tenantid | attempt: $($RetryCount + 1) of $maxRetries" + Write-Information "$($type.ToUpper()) [ $uri ] | tenant: $tenantid | user-agent: $($headers['User-Agent']) | attempt: $($RetryCount + 1) of $maxRetries" $ReturnedData = (Invoke-CIPPRestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType $contentType -SkipHttpErrorCheck:$IgnoreErrors -ResponseHeadersVariable responseHeaders) $RequestSuccessful = $true } catch { From 920e3a1c367ee69d740911b0a0525f2822c87bb7 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:34:29 +0800 Subject: [PATCH 13/14] user auth and sync logic --- .../Authentication/Initialize-CIPPAuth.ps1 | 60 +++++++ .../Timer Functions/Start-UserSyncTimer.ps1 | 158 +++++++++++------- .../CIPP/Settings/Invoke-ExecCIPPUsers.ps1 | 95 ++++++++--- 3 files changed, 228 insertions(+), 85 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 index ec5c613b4692..127af216dcc5 100644 --- a/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Initialize-CIPPAuth.ps1 @@ -159,6 +159,66 @@ function Initialize-CIPPAuth { Write-Information "[Auth-Init] EasyAuth policy reconcile failed (non-fatal): $_" } } + + # 3d. Reconcile API clients — ensure the EasyAuth config matches what the + # "Save to Azure" action (Set-CippApiAuth) would produce for the currently + # enabled API clients. That means BOTH lists must be checked, not just apps: + # allowedApplications = SSO app + every enabled client + # allowedAudiences = api:// for each of the above, plus the MCP host + # URIs and bare client IDs for MCP-enabled clients + # Config drifts when a client is enabled but "Save to Azure" was never run (or a + # prior save partially applied — e.g. apps set but audiences missing), which + # silently breaks API authentication for that client. + if ($AuthState.HasSAMCredentials -and -not $env:CIPP_SSO_MIGRATION_APPID -and $env:WEBSITE_AUTH_V2_CONFIG_JSON) { + try { + $ApiClientsTable = Get-CippTable -tablename 'ApiClients' + $EnabledClients = @(Get-CIPPAzDataTableEntity @ApiClientsTable -Filter 'Enabled eq true' | Where-Object { ![string]::IsNullOrEmpty($_.RowKey) }) + + if ($EnabledClients.Count -gt 0) { + $EnabledClientIds = @($EnabledClients.RowKey) + # MCPAllowed can round-trip as a bool or string; compare on string form (matches SaveToAzure) + $McpClientIds = @($EnabledClients | Where-Object { "$($_.MCPAllowed)" -eq 'True' } | ForEach-Object { $_.RowKey }) + + $ApiAuthConfig = $env:WEBSITE_AUTH_V2_CONFIG_JSON | ConvertFrom-Json -ErrorAction Stop + $AADConfig = $ApiAuthConfig.identityProviders.azureActiveDirectory + + # Desired state — keep in sync with Set-CippApiAuth's CIPPNG branch. + $DesiredApps = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + if ($AADConfig.registration.clientId) { [void]$DesiredApps.Add($AADConfig.registration.clientId) } + foreach ($Id in $EnabledClientIds) { if (-not [string]::IsNullOrEmpty($Id)) { [void]$DesiredApps.Add($Id) } } + + $DesiredAudiences = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($Id in $DesiredApps) { [void]$DesiredAudiences.Add("api://$Id") } + if ($McpClientIds.Count -gt 0 -and $env:WEBSITE_HOSTNAME) { + [void]$DesiredAudiences.Add("https://$($env:WEBSITE_HOSTNAME)") + [void]$DesiredAudiences.Add("https://$($env:WEBSITE_HOSTNAME)/api/ExecMcp") + foreach ($McpId in $McpClientIds) { if (-not [string]::IsNullOrEmpty($McpId)) { [void]$DesiredAudiences.Add($McpId) } } + } + + # Current state from the platform-injected config + $CurrentApps = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($App in @($AADConfig.validation.defaultAuthorizationPolicy.allowedApplications)) { if ($App) { [void]$CurrentApps.Add($App) } } + $CurrentAudiences = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($Aud in @($AADConfig.validation.allowedAudiences)) { if ($Aud) { [void]$CurrentAudiences.Add($Aud) } } + + # Drift when anything the endpoint would set is missing from the live config + $AppsOk = $DesiredApps.IsSubsetOf($CurrentApps) + $AudiencesOk = $DesiredAudiences.IsSubsetOf($CurrentAudiences) + + if (-not $AppsOk -or -not $AudiencesOk) { + $MissingApps = @($DesiredApps | Where-Object { -not $CurrentApps.Contains($_) }) + $MissingAudiences = @($DesiredAudiences | Where-Object { -not $CurrentAudiences.Contains($_) }) + Write-Information "[Auth-Init] API client drift detected — missing apps: [$($MissingApps -join ', ')]; missing audiences: [$($MissingAudiences -join ', ')] — reconciling EasyAuth" + Set-CippApiAuth -TenantId $env:TenantID -ClientIds $EnabledClientIds -McpClientIds $McpClientIds + Write-Information '[Auth-Init] EasyAuth allowedApplications + allowedAudiences reconciled with enabled API clients' + } else { + Write-Information "[Auth-Init] EasyAuth already matches $($EnabledClients.Count) enabled API client(s) — no update needed" + } + } + } catch { + Write-Information "[Auth-Init] API client reconcile failed (non-fatal): $_" + } + } } elseif ($AuthState.HasSAMCredentials) { # EasyAuth NOT configured but we DO have SAM credentials — try to auto-configure Write-Information '[Auth-Init] EasyAuth not configured but SAM credentials available — attempting auto-configuration' diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UserSyncTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UserSyncTimer.ps1 index f4578bccecdf..435d3a3d4a01 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UserSyncTimer.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UserSyncTimer.ps1 @@ -57,7 +57,7 @@ function Start-UserSyncTimer { $Upn = $Upn.Trim().ToLower() if (-not $UserRoleMap.ContainsKey($Upn)) { - $UserRoleMap[$Upn] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $UserRoleMap[$Upn] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) } foreach ($Role in $RolesForGroup) { [void]$UserRoleMap[$Upn].Add($Role) @@ -79,54 +79,56 @@ function Start-UserSyncTimer { $UsersTable = Get-CippTable -tablename 'allowedUsers' $ExistingUsers = @(Get-CIPPAzDataTableEntity @UsersTable | Where-Object { -not $_.RowKey.StartsWith('_') }) - # Build lookup of existing users + # Group existing rows by lowercased UPN so case-variant duplicate rows + # are reconciled into one canonical row. $ExistingLookup = @{} foreach ($Existing in $ExistingUsers) { - $ExistingLookup[$Existing.RowKey.ToLower()] = $Existing + $Key = $Existing.RowKey.ToLower() + if (-not $ExistingLookup.ContainsKey($Key)) { + $ExistingLookup[$Key] = [System.Collections.Generic.List[object]]::new() + } + $ExistingLookup[$Key].Add($Existing) } $Now = (Get-Date).ToUniversalTime().ToString('o') $UpsertCount = 0 $RemoveCount = 0 $EntitiesToUpsert = [System.Collections.Generic.List[object]]::new() + $EntitiesToRemove = [System.Collections.Generic.List[object]]::new() - # Upsert users from Graph + # Upsert users that are members of a mapped role group foreach ($Upn in $UserRoleMap.Keys) { $AutoRoles = @($UserRoleMap[$Upn] | Sort-Object) - $ManualRoles = @() - $Source = 'Auto' - + # Merge manual roles from every case-variant of this user (case-sensitive dedupe) + $ManualRoles = [System.Collections.Generic.List[string]]::new() if ($ExistingLookup.ContainsKey($Upn)) { - $Existing = $ExistingLookup[$Upn] - - # Preserve manual roles if they exist - if ($Existing.ManualRoles) { - try { - $ManualRoles = @($Existing.ManualRoles | ConvertFrom-Json -ErrorAction Stop) - } catch { - $ManualRoles = @() + foreach ($Existing in $ExistingLookup[$Upn]) { + if ($Existing.ManualRoles) { + try { + foreach ($R in @($Existing.ManualRoles | ConvertFrom-Json -ErrorAction Stop)) { + if (-not $ManualRoles.Contains($R)) { $ManualRoles.Add($R) } + } + } catch {} } - } - - # If user was previously manual-only and now also auto, mark as Both - if ($ManualRoles.Count -gt 0) { - $Source = 'Both' + # Any row that isn't the canonical lowercase key is a duplicate to remove + if ($Existing.RowKey -cne $Upn) { $EntitiesToRemove.Add($Existing) } } } + $Source = if ($ManualRoles.Count -gt 0) { 'Both' } else { 'Auto' } - # Compute effective roles = union of auto + manual - $EffectiveRoles = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) - foreach ($Role in $AutoRoles) { [void]$EffectiveRoles.Add($Role) } - foreach ($Role in $ManualRoles) { [void]$EffectiveRoles.Add($Role) } + # Compute effective roles = auto ∪ manual (case-sensitive dedupe) + $EffectiveRoles = [System.Collections.Generic.List[string]]::new() + foreach ($Role in $AutoRoles) { if (-not $EffectiveRoles.Contains($Role)) { $EffectiveRoles.Add($Role) } } + foreach ($Role in $ManualRoles) { if (-not $EffectiveRoles.Contains($Role)) { $EffectiveRoles.Add($Role) } } $EffectiveRolesArray = @($EffectiveRoles | Sort-Object) $Entity = @{ PartitionKey = 'User' RowKey = $Upn Roles = [string]($EffectiveRolesArray | ConvertTo-Json -Compress -AsArray) - AutoRoles = [string]($AutoRoles | ConvertTo-Json -Compress -AsArray) - ManualRoles = [string](($ManualRoles.Count -gt 0 ? $ManualRoles : @()) | ConvertTo-Json -Compress -AsArray) + AutoRoles = [string](@($AutoRoles) | ConvertTo-Json -Compress -AsArray) + ManualRoles = [string]((($ManualRoles.Count -gt 0) ? @($ManualRoles) : @()) | ConvertTo-Json -Compress -AsArray) Source = $Source LastSync = $Now } @@ -135,57 +137,87 @@ function Start-UserSyncTimer { $UpsertCount++ } - # Handle users that were auto-provisioned but are no longer in any role group - foreach ($Existing in $ExistingUsers) { - $ExistingUpn = $Existing.RowKey.ToLower() - if ($UserRoleMap.ContainsKey($ExistingUpn)) { continue } # Still in a group, already handled - - if ($Existing.Source -eq 'Auto') { - # Purely auto-provisioned user no longer in any group — remove - Remove-AzDataTableEntity -Force @UsersTable -Entity $Existing - $RemoveCount++ - } elseif ($Existing.Source -eq 'Both') { - # Was both auto + manual — clear auto roles, keep manual only - $ManualRoles = @() + # Reconcile existing users that are NOT in any mapped role group + foreach ($Key in $ExistingLookup.Keys) { + if ($UserRoleMap.ContainsKey($Key)) { continue } # Still in a group, already handled + + $Variants = $ExistingLookup[$Key] + $NeedsNormalize = ($Variants.Count -gt 1) -or ($Variants[0].RowKey -cne $Key) + + # Merge manual roles across all case-variants (case-sensitive dedupe) + $ManualRoles = [System.Collections.Generic.List[string]]::new() + foreach ($Existing in $Variants) { if ($Existing.ManualRoles) { try { - $ManualRoles = @($Existing.ManualRoles | ConvertFrom-Json -ErrorAction Stop) - } catch { - $ManualRoles = @() - } + foreach ($R in @($Existing.ManualRoles | ConvertFrom-Json -ErrorAction Stop)) { + if (-not $ManualRoles.Contains($R)) { $ManualRoles.Add($R) } + } + } catch {} } + } - if ($ManualRoles.Count -gt 0) { - $Entity = @{ - PartitionKey = 'User' - RowKey = $Existing.RowKey - Roles = [string]($ManualRoles | ConvertTo-Json -Compress -AsArray) - AutoRoles = '[]' - ManualRoles = [string]($ManualRoles | ConvertTo-Json -Compress -AsArray) - Source = 'Manual' - LastSync = $Now + if (-not $NeedsNormalize) { + # Single clean lowercase row — apply the original cleanup rules + $Existing = $Variants[0] + if ($Existing.Source -eq 'Auto') { + # Purely auto-provisioned user no longer in any group — remove + $EntitiesToRemove.Add($Existing) + } elseif ($Existing.Source -eq 'Both') { + if ($ManualRoles.Count -gt 0) { + # Was both auto + manual — clear auto roles, keep manual only + $ManualArray = @($ManualRoles | Sort-Object) + $EntitiesToUpsert.Add(@{ + PartitionKey = 'User' + RowKey = $Key + Roles = [string]($ManualArray | ConvertTo-Json -Compress -AsArray) + AutoRoles = '[]' + ManualRoles = [string]($ManualArray | ConvertTo-Json -Compress -AsArray) + Source = 'Manual' + LastSync = $Now + }) + } else { + $EntitiesToRemove.Add($Existing) } - $EntitiesToUpsert.Add($Entity) - } else { - # No manual roles either — remove - Remove-AzDataTableEntity -Force @UsersTable -Entity $Existing - $RemoveCount++ } + # Source = 'Manual' (or unset) — leave untouched, these are purely manual entries + continue } - # Source = 'Manual' (or unset) — leave untouched, these are purely manual entries - } - # Batch upsert - if ($EntitiesToUpsert.Count -gt 0) { - foreach ($Entity in $EntitiesToUpsert) { - Add-CIPPAzDataTableEntity @UsersTable -Entity $Entity -Force + # Duplicates or non-lowercase casing present — collapse to one canonical lowercase row + if ($ManualRoles.Count -gt 0) { + $ManualArray = @($ManualRoles | Sort-Object) + $EntitiesToUpsert.Add(@{ + PartitionKey = 'User' + RowKey = $Key + Roles = [string]($ManualArray | ConvertTo-Json -Compress -AsArray) + AutoRoles = '[]' + ManualRoles = [string]($ManualArray | ConvertTo-Json -Compress -AsArray) + Source = 'Manual' + LastSync = $Now + }) + # Remove every case-variant except the canonical one (overwritten by the upsert) + foreach ($Existing in $Variants) { + if ($Existing.RowKey -cne $Key) { $EntitiesToRemove.Add($Existing) } + } + } else { + # No manual roles anywhere — purely auto-provisioned; remove all variants + foreach ($Existing in $Variants) { $EntitiesToRemove.Add($Existing) } } } + # Apply upserts first (write canonical rows), then removals (drop duplicates/stale rows) + foreach ($Entity in $EntitiesToUpsert) { + Add-CIPPAzDataTableEntity @UsersTable -Entity $Entity -Force + } + foreach ($Entity in $EntitiesToRemove) { + Remove-AzDataTableEntity -Force @UsersTable -Entity $Entity + $RemoveCount++ + } + # Invalidate CRAFT's in-memory user cache so changes apply try { [Craft.Services.AuthBridge]::InvalidateUsers() } catch {} - Write-LogMessage -API $ApiName -tenant 'none' -message "User sync completed: $UpsertCount users synced, $RemoveCount auto-only users removed." -sev Info + Write-LogMessage -API $ApiName -tenant 'none' -message "User sync completed: $UpsertCount users synced, $RemoveCount duplicate/stale rows removed." -sev Info } catch { $ErrorData = Get-CippException -Exception $_ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 index dfc78cd1a542..2e1ca1931df5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCIPPUsers.ps1 @@ -13,6 +13,18 @@ function Invoke-ExecCIPPUsers { $Action = $Request.Query.Action ?? $Request.Body.Action $Table = Get-CippTable -tablename 'allowedUsers' + # Returns $true if a row carries a manually-assigned 'superadmin' role. + # Superadmin granted via Entra group sync (AutoRoles) does NOT count — group + # membership can change, so it must never be the sole source of superadmin. + # Match is case-sensitive: the built-in role is exactly 'superadmin'; a custom + # role like 'SuperAdmin' is a different role and must not trip this protection. + $HasManualSuperAdmin = { + param($Entity) + if (-not $Entity.ManualRoles) { return $false } + try { return (@($Entity.ManualRoles | ConvertFrom-Json -ErrorAction Stop) -ccontains 'superadmin') } + catch { return $false } + } + switch ($Action) { 'AddUpdate' { try { @@ -20,7 +32,8 @@ function Invoke-ExecCIPPUsers { if ([string]::IsNullOrWhiteSpace($UPN)) { throw 'UPN (email) is required' } - $UPN = $UPN.Trim() + # Squash casing so the RowKey is canonical and case-variant duplicates can't form + $UPN = $UPN.Trim().ToLower() $Roles = @($Request.Body.Roles) if ($Roles.Count -eq 0) { @@ -45,26 +58,41 @@ function Invoke-ExecCIPPUsers { } } - # Check if user already exists to preserve auto-synced roles - $ExistingEntity = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$UPN'" - $AutoRoles = @() - $Source = 'Manual' - - if ($ExistingEntity -and $ExistingEntity.AutoRoles) { - try { - $AutoRoles = @($ExistingEntity.AutoRoles | ConvertFrom-Json -ErrorAction Stop) - } catch { - $AutoRoles = @() + # Find every existing row for this user (case-insensitive) so auto-synced + # roles are preserved and any case-variant duplicates collapse into one + # canonical lowercase row. + $AllUsers = @(Get-CIPPAzDataTableEntity @Table | Where-Object { -not $_.RowKey.StartsWith('_') }) + $MatchingEntities = @($AllUsers | Where-Object { $_.RowKey -and $_.RowKey.ToLower() -eq $UPN }) + + # Invariant: at least one user must always keep a manually-assigned superadmin. + # Block an update that would strip the last manual superadmin. + if (@($Roles) -cnotcontains 'superadmin') { + $TargetHadManualSuperAdmin = @($MatchingEntities | Where-Object { & $HasManualSuperAdmin $_ }).Count -gt 0 + if ($TargetHadManualSuperAdmin) { + $OtherManualSuperAdmins = @($AllUsers | Where-Object { $_.RowKey.ToLower() -ne $UPN -and (& $HasManualSuperAdmin $_) }) + if ($OtherManualSuperAdmins.Count -eq 0) { + throw 'Cannot remove the superadmin role from the last user that has it manually assigned. Grant superadmin manually to another user first (superadmin from Entra group sync does not count).' + } } - if ($AutoRoles.Count -gt 0) { - $Source = 'Both' + } + + # Preserve + merge auto roles across all case-variants (case-sensitive dedupe) + $AutoRoles = [System.Collections.Generic.List[string]]::new() + foreach ($Existing in $MatchingEntities) { + if ($Existing.AutoRoles) { + try { + foreach ($R in @($Existing.AutoRoles | ConvertFrom-Json -ErrorAction Stop)) { + if (-not $AutoRoles.Contains($R)) { $AutoRoles.Add($R) } + } + } catch {} } } + $Source = if ($AutoRoles.Count -gt 0) { 'Both' } else { 'Manual' } - # Compute effective roles = union of manual + auto - $EffectiveRoles = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) - foreach ($R in $Roles) { [void]$EffectiveRoles.Add($R) } - foreach ($R in $AutoRoles) { [void]$EffectiveRoles.Add($R) } + # Compute effective roles = manual ∪ auto (case-sensitive dedupe) + $EffectiveRoles = [System.Collections.Generic.List[string]]::new() + foreach ($R in $Roles) { if (-not $EffectiveRoles.Contains($R)) { $EffectiveRoles.Add($R) } } + foreach ($R in $AutoRoles) { if (-not $EffectiveRoles.Contains($R)) { $EffectiveRoles.Add($R) } } $EffectiveRolesArray = @($EffectiveRoles | Sort-Object) $Entity = @{ @@ -72,11 +100,18 @@ function Invoke-ExecCIPPUsers { RowKey = $UPN Roles = [string]($EffectiveRolesArray | ConvertTo-Json -Compress -AsArray) ManualRoles = [string](@($Roles) | ConvertTo-Json -Compress -AsArray) - AutoRoles = [string]($AutoRoles | ConvertTo-Json -Compress -AsArray) + AutoRoles = [string](@($AutoRoles) | ConvertTo-Json -Compress -AsArray) Source = $Source } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + # Remove any case-variant duplicate rows now merged into the canonical row + foreach ($Existing in $MatchingEntities) { + if ($Existing.RowKey -cne $UPN) { + Remove-AzDataTableEntity -Force @Table -Entity $Existing + } + } + # Trigger a user sync to reconcile auto + manual roles try { Start-UserSyncTimer } catch {} @@ -100,7 +135,7 @@ function Invoke-ExecCIPPUsers { if ([string]::IsNullOrWhiteSpace($UPN)) { throw 'UPN (email) is required' } - $UPN = $UPN.Trim() + $UPN = $UPN.Trim().ToLower() # Self-lockout protection: prevent removing yourself $CurrentUser = $Request.Headers.'x-ms-client-principal-name' @@ -108,12 +143,28 @@ function Invoke-ExecCIPPUsers { throw 'Cannot remove your own user account. This would lock you out.' } - $ExistingEntity = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$UPN'" - if (-not $ExistingEntity) { + # Fetch all users once so we can locate the target (case-insensitively) + # and enforce the "at least one manual superadmin" invariant. + $AllUsers = @(Get-CIPPAzDataTableEntity @Table | Where-Object { -not $_.RowKey.StartsWith('_') }) + $MatchingEntities = @($AllUsers | Where-Object { $_.RowKey -and $_.RowKey.ToLower() -eq $UPN }) + if ($MatchingEntities.Count -eq 0) { throw "User $UPN not found in the allowed users table" } - Remove-AzDataTableEntity -Force @Table -Entity $ExistingEntity + # Invariant: don't remove the last user holding a manually-assigned superadmin. + # (Superadmin granted via Entra group sync does not count — it can disappear + # when group membership changes.) + $TargetHasManualSuperAdmin = @($MatchingEntities | Where-Object { & $HasManualSuperAdmin $_ }).Count -gt 0 + if ($TargetHasManualSuperAdmin) { + $OtherManualSuperAdmins = @($AllUsers | Where-Object { $_.RowKey.ToLower() -ne $UPN -and (& $HasManualSuperAdmin $_) }) + if ($OtherManualSuperAdmins.Count -eq 0) { + throw 'Cannot remove the last user with a manually assigned superadmin role. Grant superadmin manually to another user first (superadmin from Entra group sync does not count).' + } + } + + foreach ($Existing in $MatchingEntities) { + Remove-AzDataTableEntity -Force @Table -Entity $Existing + } try { [Craft.Services.AuthBridge]::InvalidateUsers() } catch {} $Result = "Successfully removed user $UPN" From 5e635412ce47af1969ed8e181bed387e04c7fe99 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:02:14 +0800 Subject: [PATCH 14/14] Update standards.json --- Config/standards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config/standards.json b/Config/standards.json index a698010ce141..fda66153955e 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -4930,7 +4930,7 @@ "name": "standards.SPFileRequests", "cat": "SharePoint Standards", "tag": [], - "helpText": "Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.", + "helpText": "*Requires 'Sharing Level for OneDrive and SharePoint' to be set to Anyone* Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.", "docsDescription": "File Requests allow users to create secure upload-only share links where uploads are hidden from other people using the link. This creates a secure and private way for people to upload files to a folder. This feature is not enabled by default on new tenants and requires PowerShell configuration. This standard enables or disables this feature and optionally configures link expiration settings for both SharePoint and OneDrive.", "executiveText": "Enables secure file upload functionality that allows external users to submit files directly to company folders without seeing other submissions or folder contents. This provides a professional and secure way to collect documents from clients, vendors, and partners while maintaining data privacy and security.", "addedComponent": [