tests/Test-Assessment.21964.ps1
| <# .SYNOPSIS Checks Enable protected actions to secure Conditional Access policy creation and changes #> #region Helper Functions function Get-AuthContextDetails { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [array]$AuthContextId ) # Return null if $AuthContextId is null or empty if ($null -eq $AuthContextId -or $AuthContextId.Count -eq 0) { return $null } # Format auth context IDs for filter query $formattedIds = $AuthContextId | ForEach-Object { "'$_'" } $filterQuery = $formattedIds -join ',' # Get authentication context details $authContextDetails = Invoke-ZtGraphRequest -RelativeUri "identity/conditionalAccess/authenticationContextClassReferences?`$filter=id in ($filterQuery)" -ApiVersion beta return $authContextDetails } function Test-ProtectedActionPolicyCompliance { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyCollection()] [array]$Policies ) $results = @{ HasEnabledPolicies = $false AllHaveAuthStrength = $false AllHaveSessionControls = $false AllHaveDeviceFilters = $false HasPhishingResistantMethods = $false TotalPolicies = 0 EnabledPolicies = 0 } if ($null -eq $Policies -or $Policies.Count -eq 0) { return $results } # Get unique policies based on policy ID $uniquePolicies = $Policies | Where-Object { $null -ne $_.OriginalPolicy } | Group-Object -Property { $_.OriginalPolicy.id } | ForEach-Object { $_.Group[0] } if ($null -eq $uniquePolicies -or $uniquePolicies.Count -eq 0) { return $results } $results.TotalPolicies = $uniquePolicies.Count $enabledPolicies = @($uniquePolicies | Where-Object { $null -ne $_.OriginalPolicy -and $_.OriginalPolicy.state -eq 'enabled' }) $results.EnabledPolicies = $enabledPolicies.Count $results.HasEnabledPolicies = $enabledPolicies.Count -gt 0 # If no enabled policies, return with all false if ($enabledPolicies.Count -eq 0) { return $results } # Check if all enabled policies have required controls (no null values allowed) # Count how many policies are missing each control $missingAuthStrength = @($Policies | Where-Object { $null -ne $_.OriginalPolicy -and $null -eq $_.OriginalPolicy.grantControls.'authenticationStrength@odata.context' }).Count $missingSessionControls = @($Policies | Where-Object { $null -ne $_.OriginalPolicy -and $null -eq $_.OriginalPolicy.sessionControls.signInFrequency }).Count $missingDeviceFilters = @($Policies | Where-Object { $null -ne $_.OriginalPolicy -and $null -eq $_.OriginalPolicy.conditions.devices }).Count # All policies must have these controls (zero missing = all have it) $results.AllHaveAuthStrength = $missingAuthStrength -eq 0 $results.AllHaveSessionControls = $missingSessionControls -eq 0 $results.AllHaveDeviceFilters = $missingDeviceFilters -eq 0 # Check if authentication strength uses phishing-resistant methods (only if all have auth strength) if ($results.AllHaveAuthStrength) { $requiredMethods = @('windowsHelloForBusiness', 'fido2', 'x509CertificateMultiFactor', 'certificateBasedAuthenticationPki', 'deviceBasedPush') $authStrengthPolicies = Invoke-ZtGraphRequest -RelativeUri 'identity/conditionalAccess/authenticationStrength/policies' -ApiVersion beta -Filter "policyType eq 'builtIn'" foreach ($authPolicy in $authStrengthPolicies) { if ($authPolicy.allowedCombinations) { foreach ($combination in $authPolicy.allowedCombinations) { if ($requiredMethods -contains $combination) { $results.HasPhishingResistantMethods = $true break } } if ($results.HasPhishingResistantMethods) { break } } } } return $results } function Get-ProtectedActionCAPolicy { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$AuthenticationContextId, [Parameter(Mandatory = $true)] [object]$Action ) # Get protected action CA Policy details $filter = "conditions/applications/includeAuthenticationContextClassReferences/any(c:c eq '$AuthenticationContextId')" $policyResult = Invoke-ZtGraphRequest -RelativeUri "identity/conditionalAccess/policies" -ApiVersion Beta -Filter $filter $results = @() foreach ($policy in $policyResult) { # Create a custom object to avoid modifying the original policy object $policyWithProtectedAction = [PSCustomObject]@{ ProtectedActionName = $Action.name ProtectedActionDescription = $Action.description OriginalPolicy = $policy } $results += $policyWithProtectedAction } return $results } function Test-Assessment-21964 { [ZtTest( Category = 'Access control', ImplementationCost = 'Low', Pillar = 'Identity', RiskLevel = 'Low', SfiPillar = 'Protect identities and secrets', TenantType = ('Workforce', 'External'), TestId = 21964, Title = 'Enable protected actions to secure Conditional Access policy creation and changes', UserImpact = 'Low' )] [CmdletBinding()] param() Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking Enable protected actions to secure Conditional Access policy creation and changes' $testResultMarkdown = "" Write-ZtProgress -Activity $activity -Status "Getting policy" # Protected actions for Conditional Access policy operations $protectedActions = @( 'microsoft.directory-conditionalAccessPolicies-basic-update-patch', 'microsoft.directory-conditionalAccessPolicies-create-post', 'microsoft.directory-conditionalAccessPolicies-delete-delete', 'microsoft.directory-resourceNamespaces-resourceActions-authenticationContext-update-post' ) # Get protected action settings for each action $protectedActionResults = @() foreach ($action in $protectedActions) { #Write-ZtProgress -Activity $activity -Status "Checking protected action: $action" $actionResult = Invoke-ZtGraphRequest -RelativeUri "roleManagement/directory/resourceNamespaces/microsoft.directory/resourceActions/$action" -ApiVersion beta -Select "authenticationContextId,isAuthenticationContextSettable,name,description" $protectedActionResults += $actionResult } # Check if all protected actions have authentication context configured $unprotectedActions = $protectedActionResults | Where-Object { [string]::IsNullOrEmpty($_.authenticationContextId) } $unprotectedActionsResult = $unprotectedActions.Count -eq 0 # For each protected action, check if there is a Conditional Access policy targeting its authenticationContextId and if any are enabled $policiesPerAction = @() foreach ($action in $protectedActionResults) { # Skip if authenticationContextId is null or empty if (-not [string]::IsNullOrEmpty($action.authenticationContextId)) { $policiesPerAction += Get-ProtectedActionCAPolicy -AuthenticationContextId $action.authenticationContextId -Action $action } } if( $policiesPerAction.Count -eq 0) { $testResultMarkdown = "## Conditional Access Policies by Protected Action`n`n" $testResultMarkdown += "*No Conditional Access policies found for any protected actions.*`n`n" } # Check compliance for ALL policies together (not per action) $protectedActionCAPolicyComplaince = Test-ProtectedActionPolicyCompliance -Policies $policiesPerAction $caPolicyResult = $protectedActionCAPolicyComplaince.HasEnabledPolicies $caPolicyRequireAuthStrength = $protectedActionCAPolicyComplaince.AllHaveAuthStrength $caPolicyHasSessionControls = $protectedActionCAPolicyComplaince.AllHaveSessionControls $caAllowedCombinationResult = $protectedActionCAPolicyComplaince.HasPhishingResistantMethods $testResultMarkdown += "`n### Conditional Access Policies by Protected Action`n`n" # Group policies by protected action $groupedPolicies = $policiesPerAction | Group-Object -Property ProtectedActionName foreach ($action in $protectedActionResults) { # Check if authentication context is configured $hasAuthContext = -not [string]::IsNullOrEmpty($action.authenticationContextId) if (-not $hasAuthContext) { $testResultMarkdown += "#### $($action.description) - ❌ Fail`n`n" $testResultMarkdown += "*Authentication context not configured for this protected action.*`n`n" continue } # Get auth context details $authContext = Get-AuthContextDetails -AuthContextId $action.authenticationContextId $authContextInfo = if ($authContext) { "**Auth Context:** $($authContext.displayName) (ID: $($authContext.id))" } else { "**Auth Context ID:** $($action.authenticationContextId)" } $actionPolicies = $groupedPolicies | Where-Object { ($_.Name -eq $action.name) } # Use overall compliance result (not per-action check) $status = if ($authContext -and ($actionPolicies.Group.OriginalPolicy.state -eq 'enabled')) { '✅ Pass' } else { '❌ Fail' } $testResultMarkdown += "#### $($action.description) - $status`n`n" $testResultMarkdown += "$authContextInfo`n`n" if ($actionPolicies -and $actionPolicies.Group.Count -gt 0) { $testResultMarkdown += "| Display Name | State | Authentication Context | Authentication Strength | Device Filters | SignIn Frequency |`n" $testResultMarkdown += "| :--- | :--- | :--- | :--- | :--- | :--- |`n" foreach ($policy in @($actionPolicies.Group)) { $authStrength = '✅' $devices = '✅' $signInFrequency = '✅' $caPolicyAuthContext = Get-AuthContextDetails -AuthContextId $policy.OriginalPolicy.conditions.applications.includeAuthenticationContextClassReferences if ($null -eq $policy.OriginalPolicy.grantControls.'authenticationStrength@odata.context') { $authStrength = '❌' } if ($null -eq $policy.OriginalPolicy.conditions.devices) { $devices = '❌' } if ($null -eq $policy.OriginalPolicy.sessionControls) { $signInFrequency = '❌' } if ($null -eq $policy.OriginalPolicy.conditions.applications.includeAuthenticationContextClassReferences) { $caPolicyAuthContext = '❌' } $portalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($policy.id)" $testResultMarkdown += "| [$(Get-SafeMarkdown $policy.OriginalPolicy.displayName)]($portalLink) | $(Get-FormattedPolicyState -PolicyState $policy.OriginalPolicy.state) | $($caPolicyAuthContext.displayName -join ',') | $authStrength | $devices | $signInFrequency |`n" } $testResultMarkdown += "`n" } else { $testResultMarkdown += "*No Conditional Access policies found for this protected action.*`n`n" } } $passed = $unprotectedActionsResult -and $caPolicyResult -and (($caPolicyRequireAuthStrength -and $caAllowedCombinationResult) -or $caPolicyHasSessionControls) $params = @{ Status = $passed Result = $testResultMarkdown } if (!$passed) { $params.CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params } |