1+ # requires -Version 7.0
12<#
23. SYNOPSIS
3- Lists all Conditional Access policies that apply to a given user.
4+ Evaluates the user-assignment scope of Microsoft Entra Conditional Access policies.
5+
6+ . DESCRIPTION
7+ Determines whether a specified user is included in or excluded from each Conditional
8+ Access policy through All users, direct user assignment, transitive group membership,
9+ or supported directory-role assignment. The script reports policy state and matching
10+ reasons, but it does not claim that a policy will trigger for every sign-in because
11+ application, platform, location, device, risk, authentication flow, and other runtime
12+ conditions are evaluated separately by Microsoft Entra.
413
514. PARAMETER UserPrincipalName
6- The UPN (email) of the user to evaluate.
15+ The exact Microsoft Entra user principal name to evaluate.
16+
17+ . PARAMETER IncludeNonTargeted
18+ Includes policies that do not target the user or explicitly exclude the user. By
19+ default, only policies whose user-assignment scope targets the user are returned.
20+
21+ . PARAMETER OutputCsv
22+ Optional path for a CSV export. No CSV is written when this parameter is omitted.
23+
24+ . EXAMPLE
25+ ./Get-ConditionalAccessUserTargeting.ps1 -UserPrincipalName 'alice@contoso.com'
726
827. EXAMPLE
9- .\List-ConditionalAccessPoliciesForUser.ps1 -UserPrincipalName "alice@contoso.com"
28+ ./Get-ConditionalAccessUserTargeting.ps1 'alice@contoso.com' -IncludeNonTargeted -OutputCsv ./ca-user-scope.csv
29+
30+ . NOTES
31+ Required delegated Microsoft Graph scopes:
32+ - Policy.Read.All
33+ - Directory.Read.All
34+
35+ Reading Conditional Access policies also requires an appropriate Microsoft Entra role,
36+ such as Security Reader, Global Reader, Security Administrator, or Conditional Access
37+ Administrator.
1038#>
1139
1240[CmdletBinding ()]
1341param (
14- [Parameter (Mandatory )]
15- [string ]$UserPrincipalName
42+ [Parameter (Mandatory , Position = 0 )]
43+ [ValidatePattern (' ^[^@\s]+@[^@\s]+\.[^@\s]+$' )]
44+ [string ]$UserPrincipalName ,
45+
46+ [Parameter ()]
47+ [switch ]$IncludeNonTargeted ,
48+
49+ [Parameter ()]
50+ [string ]$OutputCsv
1651)
1752
18- # 1. Ensure Graph module is present
19- if (-not (Get-Module Microsoft.Graph)) {
20- Install-Module Microsoft.Graph - Scope CurrentUser - Force
53+ Set-StrictMode - Version Latest
54+ $ErrorActionPreference = ' Stop'
55+
56+ $requiredCommands = @ (
57+ ' Connect-MgGraph' ,
58+ ' Get-MgContext' ,
59+ ' Get-MgUser' ,
60+ ' Get-MgUserTransitiveMemberOf' ,
61+ ' Get-MgIdentityConditionalAccessPolicy'
62+ )
63+
64+ foreach ($commandName in $requiredCommands ) {
65+ if (-not (Get-Command $commandName - ErrorAction SilentlyContinue)) {
66+ throw " Required Microsoft Graph command '$commandName ' was not found. Install Microsoft.Graph, Microsoft.Graph.Users, and Microsoft.Graph.Identity.SignIns."
67+ }
68+ }
69+
70+ $requiredScopes = @ (' Policy.Read.All' , ' Directory.Read.All' )
71+ $context = Get-MgContext
72+ $mustConnect = $null -eq $context
73+
74+ if (-not $mustConnect ) {
75+ $currentScopes = @ ($context.Scopes )
76+ foreach ($scope in $requiredScopes ) {
77+ if ($scope -notin $currentScopes ) {
78+ $mustConnect = $true
79+ break
80+ }
81+ }
82+ }
83+
84+ if ($mustConnect ) {
85+ Write-Host ' Connecting to Microsoft Graph...' - ForegroundColor Cyan
86+ Connect-MgGraph - Scopes $requiredScopes - NoWelcome | Out-Null
87+ }
88+
89+ Write-Host " Resolving user: $UserPrincipalName " - ForegroundColor Cyan
90+ $user = Get-MgUser - UserId $UserPrincipalName - Property Id, DisplayName, UserPrincipalName, UserType - ErrorAction Stop
91+
92+ # Transitive membership is required because Conditional Access group targeting also
93+ # applies through nested groups. The returned collection can include groups and
94+ # directory roles.
95+ $memberships = @ (Get-MgUserTransitiveMemberOf - UserId $user.Id - All - Property Id, DisplayName, RoleTemplateId)
96+
97+ $groupIds = [System.Collections.Generic.HashSet [string ]]::new([System.StringComparer ]::OrdinalIgnoreCase)
98+ $roleTemplateIds = [System.Collections.Generic.HashSet [string ]]::new([System.StringComparer ]::OrdinalIgnoreCase)
99+
100+ foreach ($membership in $memberships ) {
101+ $odataType = [string ]$membership.AdditionalProperties [' @odata.type' ]
102+
103+ if ($odataType -eq ' #microsoft.graph.group' ) {
104+ [void ]$groupIds.Add ([string ]$membership.Id )
105+ continue
106+ }
107+
108+ if ($odataType -eq ' #microsoft.graph.directoryRole' ) {
109+ $roleTemplateId = $null
110+
111+ if ($membership.PSObject.Properties [' RoleTemplateId' ]) {
112+ $roleTemplateId = [string ]$membership.RoleTemplateId
113+ }
114+
115+ if ([string ]::IsNullOrWhiteSpace($roleTemplateId )) {
116+ $roleTemplateId = [string ]$membership.AdditionalProperties [' roleTemplateId' ]
117+ }
118+
119+ if (-not [string ]::IsNullOrWhiteSpace($roleTemplateId )) {
120+ [void ]$roleTemplateIds.Add ($roleTemplateId )
121+ }
122+ }
123+ }
124+
125+ Write-Host " Resolved $ ( $groupIds.Count ) transitive group membership(s) and $ ( $roleTemplateIds.Count ) active directory-role template ID(s)." - ForegroundColor DarkGray
126+
127+ $policies = @ (Get-MgIdentityConditionalAccessPolicy - All)
128+ $results = [System.Collections.Generic.List [object ]]::new()
129+
130+ function Find-FirstMatch {
131+ param (
132+ [AllowNull ()][object []]$PolicyValues ,
133+ [Parameter (Mandatory )]
134+ [AllowEmptyCollection ()]
135+ [System.Collections.Generic.HashSet [string ]]$UserValues
136+ )
137+
138+ foreach ($value in @ ($PolicyValues )) {
139+ if (-not [string ]::IsNullOrWhiteSpace([string ]$value ) -and $UserValues.Contains ([string ]$value )) {
140+ return [string ]$value
141+ }
142+ }
143+
144+ return $null
21145}
22- Import-Module Microsoft.Graph
23-
24- # 2. Connect to Microsoft Graph
25- Write-Verbose " Connecting to Microsoft Graph..."
26- Connect-MgGraph - Scopes " Policy.Read.All" , " Directory.Read.All"
27-
28- # 3. Retrieve user object and memberships
29- $user = Get-MgUser - UserId $UserPrincipalName
30- $userId = $user.Id
31-
32- # Fetch all group and role memberships
33- $memberOf = Get-MgUserMemberOf - UserId $userId - All
34- $groupIds = $memberOf |
35- Where-Object { $_ .' @odata.type' -eq ' #microsoft.graph.group' } |
36- Select-Object - ExpandProperty Id
37- $roleIds = $memberOf |
38- Where-Object { $_ .' @odata.type' -eq ' #microsoft.graph.directoryRole' } |
39- Select-Object - ExpandProperty Id
40-
41- # 4. Pull down every CA policy
42- $policies = Get-MgIdentityConditionalAccessPolicy - All
43-
44- # 5. Evaluate each policy’s user filter
45- $applied = foreach ($pol in $policies ) {
46- $u = $pol.Conditions.Users
47-
48- # Determine inclusion
49- $included = $false
50- if ($u.IncludeUsers -contains ' All' ) { $included = $true }
51- elseif ($u.IncludeUsers -contains $userId ) { $included = $true }
52- elseif ($u.IncludeGroups -contains ' All' ) { $included = $true }
53- elseif ($groupIds | Where-Object { $u.IncludeGroups -contains $_ }) { $included = $true }
54- elseif ($u.IncludeRoles -contains ' All' ) { $included = $true }
55- elseif ($roleIds | Where-Object { $u.IncludeRoles -contains $_ }) { $included = $true }
56-
57- if (-not $included ) { continue }
58-
59- # Determine exclusion
60- $excluded = $false
61- if ($u.ExcludeUsers -contains ' All' ) { $excluded = $true }
62- elseif ($u.ExcludeUsers -contains $userId ) { $excluded = $true }
63- elseif ($u.ExcludeGroups -contains ' All' ) { $excluded = $true }
64- elseif ($groupIds | Where-Object { $u.ExcludeGroups -contains $_ }) { $excluded = $true }
65- elseif ($u.ExcludeRoles -contains ' All' ) { $excluded = $true }
66- elseif ($roleIds | Where-Object { $u.ExcludeRoles -contains $_ }) { $excluded = $true }
67-
68- if (-not $excluded ) {
69- [PSCustomObject ]@ {
70- Name = $pol.DisplayName
71- Id = $pol.Id
72- State = $pol.State
73- }
74- }
146+
147+ foreach ($policy in $policies ) {
148+ $users = $policy.Conditions.Users
149+
150+ if ($null -eq $users ) {
151+ $results.Add ([pscustomobject ]@ {
152+ PolicyName = $policy.DisplayName
153+ PolicyId = $policy.Id
154+ State = $policy.State
155+ UserScopeResult = ' NotTargeted'
156+ IncludedBy = $null
157+ ExcludedBy = $null
158+ MatchedGroupId = $null
159+ MatchedRoleTemplateId = $null
160+ RuntimeConditionsRemain = $true
161+ Notes = ' Policy has no user assignment condition.'
162+ })
163+ continue
164+ }
165+
166+ $includeUsers = @ ($users.IncludeUsers )
167+ $excludeUsers = @ ($users.ExcludeUsers )
168+ $includeGroups = @ ($users.IncludeGroups )
169+ $excludeGroups = @ ($users.ExcludeGroups )
170+ $includeRoles = @ ($users.IncludeRoles )
171+ $excludeRoles = @ ($users.ExcludeRoles )
172+
173+ $matchedIncludeGroup = Find-FirstMatch - PolicyValues $includeGroups - UserValues $groupIds
174+ $matchedExcludeGroup = Find-FirstMatch - PolicyValues $excludeGroups - UserValues $groupIds
175+ $matchedIncludeRole = Find-FirstMatch - PolicyValues $includeRoles - UserValues $roleTemplateIds
176+ $matchedExcludeRole = Find-FirstMatch - PolicyValues $excludeRoles - UserValues $roleTemplateIds
177+
178+ $includedBy = $null
179+ if ($includeUsers -contains ' All' ) {
180+ $includedBy = ' AllUsers'
181+ }
182+ elseif ($includeUsers -contains $user.Id ) {
183+ $includedBy = ' DirectUser'
184+ }
185+ elseif (-not [string ]::IsNullOrWhiteSpace($matchedIncludeGroup )) {
186+ $includedBy = ' TransitiveGroup'
187+ }
188+ elseif (-not [string ]::IsNullOrWhiteSpace($matchedIncludeRole )) {
189+ $includedBy = ' DirectoryRole'
190+ }
191+
192+ # Guest/external-user targeting is more complex than a simple userType check
193+ # because policies can select specific external identity types and tenants.
194+ $guestTargetingPresent = $null -ne $users.IncludeGuestsOrExternalUsers
195+
196+ $excludedBy = $null
197+ if ($excludeUsers -contains ' All' ) {
198+ $excludedBy = ' AllUsers'
199+ }
200+ elseif ($excludeUsers -contains $user.Id ) {
201+ $excludedBy = ' DirectUser'
202+ }
203+ elseif (-not [string ]::IsNullOrWhiteSpace($matchedExcludeGroup )) {
204+ $excludedBy = ' TransitiveGroup'
205+ }
206+ elseif (-not [string ]::IsNullOrWhiteSpace($matchedExcludeRole )) {
207+ $excludedBy = ' DirectoryRole'
208+ }
209+
210+ $userScopeResult = if (-not [string ]::IsNullOrWhiteSpace($excludedBy )) {
211+ ' Excluded'
212+ }
213+ elseif (-not [string ]::IsNullOrWhiteSpace($includedBy )) {
214+ ' Targeted'
215+ }
216+ elseif ($guestTargetingPresent -and [string ]$user.UserType -eq ' Guest' ) {
217+ ' ReviewGuestOrExternalUserTargeting'
218+ }
219+ else {
220+ ' NotTargeted'
221+ }
222+
223+ $notes = switch ($userScopeResult ) {
224+ ' Targeted' { ' User is within the policy user-assignment scope. Other policy conditions still determine whether a sign-in triggers the policy.' }
225+ ' Excluded' { ' User matches an explicit user, group, role, or All users exclusion.' }
226+ ' ReviewGuestOrExternalUserTargeting' { ' Policy contains guest/external-user targeting that requires tenant and external-user-type evaluation.' }
227+ default { ' No matching direct user, transitive group, or active directory-role inclusion was found.' }
228+ }
229+
230+ $results.Add ([pscustomobject ]@ {
231+ PolicyName = $policy.DisplayName
232+ PolicyId = $policy.Id
233+ State = $policy.State
234+ UserScopeResult = $userScopeResult
235+ IncludedBy = $includedBy
236+ ExcludedBy = $excludedBy
237+ MatchedGroupId = if ($includedBy -eq ' TransitiveGroup' ) { $matchedIncludeGroup } elseif ($excludedBy -eq ' TransitiveGroup' ) { $matchedExcludeGroup } else { $null }
238+ MatchedRoleTemplateId = if ($includedBy -eq ' DirectoryRole' ) { $matchedIncludeRole } elseif ($excludedBy -eq ' DirectoryRole' ) { $matchedExcludeRole } else { $null }
239+ RuntimeConditionsRemain = $true
240+ Notes = $notes
241+ })
242+ }
243+
244+ $output = if ($IncludeNonTargeted ) {
245+ @ ($results )
246+ }
247+ else {
248+ @ ($results | Where-Object UserScopeResult -eq ' Targeted' )
249+ }
250+
251+ $output = @ ($output | Sort-Object State, PolicyName)
252+
253+ Write-Host ' '
254+ Write-Host " User: $ ( $user.DisplayName ) <$ ( $user.UserPrincipalName ) >" - ForegroundColor Green
255+ Write-Host " Policies evaluated: $ ( $policies.Count ) "
256+ Write-Host " User-scope targeted: $ ( @ ($results | Where-Object UserScopeResult -eq ' Targeted' ).Count) "
257+ Write-Host " Explicitly excluded: $ ( @ ($results | Where-Object UserScopeResult -eq ' Excluded' ).Count) "
258+ Write-Host ' '
259+
260+ if ($output.Count -gt 0 ) {
261+ $output |
262+ Select-Object PolicyName, State, UserScopeResult, IncludedBy, ExcludedBy, RuntimeConditionsRemain |
263+ Format-Table - AutoSize - Wrap
264+ }
265+ else {
266+ Write-Host ' No Conditional Access policies target this user through the evaluated user-assignment methods.' - ForegroundColor Yellow
267+ }
268+
269+ if (-not [string ]::IsNullOrWhiteSpace($OutputCsv )) {
270+ $parentDirectory = Split-Path - Parent $OutputCsv
271+ if (-not [string ]::IsNullOrWhiteSpace($parentDirectory )) {
272+ New-Item - Path $parentDirectory - ItemType Directory - Force | Out-Null
273+ }
274+
275+ $output | Export-Csv - Path $OutputCsv - NoTypeInformation - Encoding utf8
276+ Write-Host " CSV report saved to: $OutputCsv " - ForegroundColor Cyan
75277}
76278
77- # 6. Output results
78- if ($applied ) {
79- $applied | Sort-Object Name | Format-Table - AutoSize
80- } else {
81- Write-Host " No Conditional Access policies apply to $UserPrincipalName ."
82- }
279+ Write-Host ' '
280+ Write-Host ' Important: This is a user-assignment scope analysis, not a simulation of a specific sign-in. Use the Conditional Access What If tool or sign-in logs to evaluate application, device, location, risk, client type, and other runtime conditions.' - ForegroundColor Yellow
0 commit comments