diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 85e0cc96e..33e45a60a 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -97,7 +97,8 @@ FunctionsToExport = 'Add-MtTestResultDetail', 'Resolve-SpfRecord', 'Send-MtMail', 'Send-MtTeamsMessage', 'Test-MtAppManagementPolicyEnabled', 'Test-MtAppRegistrationsWithSecrets', 'Test-MtSpExchangeAppAccessPolicy', 'Test-MtServicePrincipalsForAllUsers', 'Test-MtAuthenticationPolicyReferencedObjectsExist', - 'Test-MtCaAllAppsExists', 'Test-MtCaApplicationEnforcedRestriction', 'Test-MtCaBlockLegacyExchangeActiveSyncAuthentication', + 'Test-MtCaAllAppsExists', 'Test-MtCaApplicationEnforcedRestriction', 'Test-MtCaAuthContextProtectedActionsExist', + 'Test-MtCaBlockLegacyExchangeActiveSyncAuthentication', 'Test-MtCaBlockLegacyOtherAuthentication', 'Test-MtCaBlockUnknownOrUnsupportedDevicePlatform', 'Test-MtCaDeviceCodeFlow', 'Test-MtCaDeviceComplianceAdminsExists', 'Test-MtCaDeviceComplianceExists', 'Test-MtCaEmergencyAccessExists', 'Test-MtCaEnforceNonPersistentBrowserSession', 'Test-MtCaEnforceSignInFrequency', diff --git a/powershell/public/maester/entra/Test-MtCaAuthContextProtectedActionsExist.md b/powershell/public/maester/entra/Test-MtCaAuthContextProtectedActionsExist.md new file mode 100644 index 000000000..36081f7c7 --- /dev/null +++ b/powershell/public/maester/entra/Test-MtCaAuthContextProtectedActionsExist.md @@ -0,0 +1,41 @@ +# Protected Actions Authentication Contexts should have Conditional Access policies + +Protected Actions allow organizations to require step-up authentication for sensitive operations by +assigning Authentication Contexts to those actions. However, if an Authentication Context is not +referenced in any Conditional Access policy, the protected action is not effectively protected. + +This test verifies that all Authentication Contexts used by Protected Actions are properly referenced +in at least one Conditional Access policy. + +## How to fix + +If this test fails, you need to create or update Conditional Access policies to reference the Authentication Contexts used by your Protected Actions: + +1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) +2. Go to **Protection** > **Conditional Access** > **Policies** +3. Create a new policy or edit an existing one +4. Under **Target resources** > **Authentication context**, select the Authentication Context(s) that need to be protected +5. Configure the appropriate grant controls (e.g., require authentication context, require compliant device) +6. Enable the policy and save + +Alternatively, if the Protected Action no longer needs step-up authentication, you can remove the Authentication Context assignment from the Protected Action: + +1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) +2. Go to **Identity** > **Roles & admins** > **Protected actions (Preview)** +3. Select the Protected Action +4. Remove or update the Authentication Context assignment + +## Learn more + +- [Protected actions in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/role-based-access-control/protected-actions-overview) +- [Conditional Access: Target resources](https://learn.microsoft.com/entra/identity/conditional-access/concept-conditional-access-cloud-apps) +- [Authentication context in Conditional Access](https://learn.microsoft.com/entra/identity/conditional-access/concept-conditional-access-cloud-apps#authentication-context) + +## Related links + +- [Entra admin center - Conditional Access Policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Overview/fromNav/) +- [Entra admin center - Authentication contexts](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/AuthenticationContext) +- [Entra admin center - Protected actions](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/ProtectedActions) + + +%TestResult% diff --git a/powershell/public/maester/entra/Test-MtCaAuthContextProtectedActionsExist.ps1 b/powershell/public/maester/entra/Test-MtCaAuthContextProtectedActionsExist.ps1 new file mode 100644 index 000000000..791dd2a14 --- /dev/null +++ b/powershell/public/maester/entra/Test-MtCaAuthContextProtectedActionsExist.ps1 @@ -0,0 +1,163 @@ +<# + .Synopsis + Checks if all Protected Actions Authentication Contexts are referenced by a conditional access policy. + + .Description + Protected Actions allow organizations to require step-up authentication for sensitive operations by + assigning Authentication Contexts to those actions. However, if an Authentication Context is not + referenced in any Conditional Access policy, the protected action is not effectively protected. + + This test verifies that all Authentication Contexts used by Protected Actions are properly referenced + in at least one Conditional Access policy. + + Learn more: + https://learn.microsoft.com/entra/identity/role-based-access-control/protected-actions-overview + + .Example + Test-MtCaAuthContextProtectedActionsExist + +.LINK + https://maester.dev/docs/commands/Test-MtCaAuthContextProtectedActionsExist +#> +function Test-MtCaAuthContextProtectedActionsExist { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Exists is not a plural.')] + [CmdletBinding()] + [OutputType([bool])] + param () + + $EntraIDPlan = Get-MtLicenseInformation -Product EntraID + $pim = $EntraIDPlan -eq "P2" -or $EntraIDPlan -eq "Governance" + if (-not $pim) { + Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 + return $null + } + + try { + # Get all authentication contexts + $authContexts = Invoke-MtGraphRequest -RelativeUri 'identity/conditionalAccess/authenticationContextClassReferences' -ApiVersion beta + + if (-not $authContexts -or ($authContexts | Measure-Object).Count -eq 0) { + $testResult = 'No Authentication Contexts are configured in the tenant.' + Add-MtTestResultDetail -Result $testResult + return $true + } + + # Get Protected Actions with authentication contexts + # Protected Actions are accessed through roleManagement/directory/resourceNamespaces + try { + $resourceNamespaces = Invoke-MtGraphRequest -RelativeUri 'roleManagement/directory/resourceNamespaces' -ApiVersion beta -ErrorAction SilentlyContinue + Write-Verbose "Found $($resourceNamespaces.Count) resource namespaces" + } catch { + Write-Verbose "Could not retrieve resource namespaces: $_" + $resourceNamespaces = @() + } + + # Collect all auth context IDs that are used in protected actions + $authContextsInProtectedActions = [System.Collections.Generic.HashSet[string]]::new() + + # Check each resource namespace for protected actions with authentication contexts + if ($resourceNamespaces) { + $namespaceCount = 0 + foreach ($namespace in $resourceNamespaces) { + $namespaceCount++ + try { + # Get resource actions for this namespace + $resourceActions = Invoke-MtGraphRequest -RelativeUri "roleManagement/directory/resourceNamespaces/$($namespace.id)/resourceActions" -ApiVersion beta -ErrorAction SilentlyContinue + if ($resourceActions) { + Write-Verbose "Namespace $namespaceCount/$($resourceNamespaces.Count) ($($namespace.id)): Found $($resourceActions.Count) resource actions" + foreach ($action in $resourceActions) { + # Debug: Log all properties of the first few actions to understand structure + if ($namespaceCount -le 3 -and $resourceActions.IndexOf($action) -le 2) { + Write-Verbose "Sample action properties: $($action | ConvertTo-Json -Depth 2 -Compress)" + } + + # Check if this action has an authentication context requirement + # Try multiple possible property names + $authContextId = $null + if ($action.authenticationContextId) { + $authContextId = $action.authenticationContextId + } elseif ($action.authenticationContext) { + $authContextId = $action.authenticationContext + } elseif ($action.authContext) { + $authContextId = $action.authContext + } elseif ($action.PSObject.Properties['authenticationContextId']) { + $authContextId = $action.PSObject.Properties['authenticationContextId'].Value + } + + if ($authContextId) { + Write-Verbose "Found protected action '$($action.name)' with authentication context: $authContextId" + [void]$authContextsInProtectedActions.Add($authContextId) + } + } + } + } catch { + Write-Verbose "Could not retrieve resource actions for namespace $($namespace.id): $_" + } + } + } + + Write-Verbose "Total authentication contexts found in protected actions: $($authContextsInProtectedActions.Count)" + + # Get all enabled conditional access policies + $caPolicies = Get-MtConditionalAccessPolicy | Where-Object { $_.state -eq 'enabled' } + + # Collect all auth context IDs referenced in CA policies + $authContextsInCAPolicies = [System.Collections.Generic.HashSet[string]]::new() + foreach ($policy in $caPolicies) { + if ($policy.conditions.applications.includeAuthenticationContextClassReferences) { + foreach ($context in $policy.conditions.applications.includeAuthenticationContextClassReferences) { + [void]$authContextsInCAPolicies.Add($context) + } + } + } + + # Check for auth contexts that are used in protected actions but not in CA policies + $unprotectedContexts = [System.Collections.Generic.List[object]]::new() + + foreach ($id in $authContextsInProtectedActions) { + if (-not $authContextsInCAPolicies.Contains($id)) { + $ctx = $authContexts | Where-Object { $_.id -eq $id } | Select-Object -First 1 + $unprotectedContexts.Add(@{ + Id = $id + DisplayName = if ($ctx) { $ctx.displayName } else { '(Deleted or not found)' } + Description = if ($ctx) { $ctx.description } else { '' } + IsAvailable = if ($ctx) { $ctx.isAvailable } else { $null } + }) + } + } + + # Determine result + $result = $unprotectedContexts.Count -eq 0 + + if ($result) { + if ($authContextsInProtectedActions.Count -eq 0) { + $testResult = 'No Authentication Contexts are configured for Protected Actions in this tenant.' + } else { + $testResult = "All Authentication Contexts used in Protected Actions are properly referenced in Conditional Access policies.`n`n" + $testResult += "**Protected Action Auth Contexts with CA policies:**`n`n" + foreach ($authContext in $authContexts) { + if ($authContextsInProtectedActions.Contains($authContext.id)) { + $testResult += "- $($authContext.displayName) ($($authContext.id))`n" + } + } + } + } else { + $testResult = "The following Authentication Contexts are used in Protected Actions but are not referenced by any Conditional Access policy:`n`n" + $testResult += "| Authentication Context | ID | Description |`n" + $testResult += "| --- | --- | --- |`n" + foreach ($context in $unprotectedContexts) { + $displayName = if ($context.DisplayName) { $context.DisplayName } else { "(No name)" } + $description = if ($context.Description) { $context.Description } else { "(No description)" } + $testResult += "| $displayName | $($context.Id) | $description |`n" + } + $testResult += "`n`n⚠️ **Warning**: These Protected Actions are not effectively protected because their Authentication Contexts are not referenced by any Conditional Access policy.`n" + } + + Add-MtTestResultDetail -Result $testResult + return $result + + } catch { + Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ + return $null + } +} diff --git a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 index 6fedf7a6d..e65faa5b9 100644 --- a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 +++ b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 @@ -85,6 +85,10 @@ Test-MtCaApprovedClientApp | Should -Be $true -Because "no policy use the deprecated Approved Client App grant." } + It "MT.1106: All Protected Actions Authentication Contexts should be referenced by a Conditional Access policy. See https://maester.dev/docs/tests/MT.1106" -Tag "MT.1106" { + Test-MtCaAuthContextProtectedActionsExist | Should -Be $true -Because "all Authentication Contexts used in Protected Actions should be referenced by Conditional Access policies." + } + Context "Maester/Entra" -Tag "Entra", "License" { It "MT.1022: All users utilizing a P1 license should be licensed. See https://maester.dev/docs/tests/MT.1022" -Tag "MT.1022" { $LicenseReport = Test-MtCaLicenseUtilization -License "P1" diff --git a/website/docs/tests/maester/MT.1106.md b/website/docs/tests/maester/MT.1106.md new file mode 100644 index 000000000..8bc90ad12 --- /dev/null +++ b/website/docs/tests/maester/MT.1106.md @@ -0,0 +1,50 @@ +--- +title: MT.1106 - All Protected Actions Authentication Contexts should be referenced by a Conditional Access policy +description: This test checks if all Authentication Contexts used in Protected Actions are properly referenced in at least one active Conditional Access policy. +slug: /tests/MT.1106 +sidebar_class_name: hidden +--- + +# Protected Actions Authentication Contexts should have Conditional Access policies + +## Description + +Protected Actions allow organizations to require step-up authentication for sensitive operations by assigning Authentication Contexts to those actions. However, if an Authentication Context is not referenced in any Conditional Access policy, the protected action is not effectively protected. + +This test verifies that all Authentication Contexts used by Protected Actions are properly referenced in at least one active Conditional Access policy. + +When a Protected Action has an Authentication Context assigned but that context is not referenced by any Conditional Access policy: + +- Users will not be prompted for additional authentication when performing the protected action +- The security benefit of the Protected Action is lost +- The tenant may be exposed to unauthorized sensitive operations + +## How to fix + +If this test fails, you need to create or update Conditional Access policies to reference the Authentication Contexts used by your Protected Actions: + +1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) +2. Go to **Protection** > **Conditional Access** > **Policies** +3. Create a new policy or edit an existing one +4. Under **Target resources** > **Authentication context**, select the Authentication Context(s) that need to be protected +5. Configure the appropriate grant controls (e.g., require multifactor authentication, require device to be marked as compliant, require approved client app) +6. Enable the policy and save + +Alternatively, if the Protected Action no longer needs step-up authentication, you can remove the Authentication Context assignment from the Protected Action: + +1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) +2. Go to **Identity** > **Roles & admins** > **Protected actions (Preview)** +3. Select the Protected Action +4. Remove or update the Authentication Context assignment + +## Learn more + +- [Protected actions in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/role-based-access-control/protected-actions-overview) +- [Conditional Access: Target resources](https://learn.microsoft.com/entra/identity/conditional-access/concept-conditional-access-cloud-apps) +- [Authentication context in Conditional Access](https://learn.microsoft.com/entra/identity/conditional-access/concept-conditional-access-cloud-apps#authentication-context) + +## Related links + +- [Entra admin center - Conditional Access Policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Overview/fromNav/) +- [Entra admin center - Authentication contexts](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/AuthenticationContext) +- [Entra admin center - Protected actions](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/ProtectedActions)