Public/Get-ConditionalAccessExclusions.ps1
|
<# .SYNOPSIS Audits all exclusions in Conditional Access policies. .DESCRIPTION This function retrieves all Conditional Access policies and identifies excluded users, groups, roles, and applications. It resolves GUIDs to display names and provides a comprehensive report of all exclusions that should be reviewed. Exclusions are often the weakest point in Conditional Access policies and should be regularly audited to ensure they are still justified. .PARAMETER PolicyState Filter policies by state. Valid values: 'All', 'Enabled', 'Disabled', 'ReportOnly'. Default is 'All'. .PARAMETER IncludeApplicationExclusions Include excluded applications in the output. Default is $true. .PARAMETER ExportPath Optional path to export results to CSV. .EXAMPLE Get-ConditionalAccessExclusions Returns all exclusions from all Conditional Access policies. .EXAMPLE Get-ConditionalAccessExclusions -PolicyState Enabled | Format-Table Returns exclusions only from enabled policies. .EXAMPLE Get-ConditionalAccessExclusions -ExportPath "C:\Reports\CA-Exclusions.csv" Exports all exclusions to a CSV file. .EXAMPLE Get-ConditionalAccessExclusions | Where-Object { $_.ExclusionType -eq 'User' } Returns only user exclusions. .NOTES Author: Kent Agent (kentagent-ai) Created: 2026-03-11 Requires: Microsoft.Graph PowerShell module Permissions: Policy.Read.All, Directory.Read.All .LINK https://github.com/kentagent-ai/EntraID-Security-Scripts #> function Get-ConditionalAccessExclusions { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [ValidateSet('All', 'Enabled', 'Disabled', 'ReportOnly')] [string]$PolicyState = 'All', [Parameter(Mandatory = $false)] [bool]$IncludeApplicationExclusions = $true, [Parameter(Mandatory = $false)] [string]$ExportPath ) begin { # Verify Graph connection $context = Get-MgContext if (-not $context) { throw "Not connected to Microsoft Graph. Run: Connect-MgGraph -Scopes 'Policy.Read.All', 'Directory.Read.All'" } # Check required scopes $requiredScopes = @('Policy.Read.All', 'Directory.Read.All') $missingScopes = $requiredScopes | Where-Object { $_ -notin $context.Scopes } if ($missingScopes) { Write-Warning "Missing scopes: $($missingScopes -join ', '). Some lookups may fail." } # Cache for resolved names (avoid repeated API calls) $script:nameCache = @{} function Resolve-ObjectName { param([string]$ObjectId, [string]$ObjectType) if ([string]::IsNullOrEmpty($ObjectId)) { return $null } if ($ObjectId -eq 'All') { return 'All' } if ($ObjectId -eq 'None') { return 'None' } if ($ObjectId -eq 'GuestsOrExternalUsers') { return 'Guests or External Users' } $cacheKey = "$ObjectType-$ObjectId" if ($script:nameCache.ContainsKey($cacheKey)) { return $script:nameCache[$cacheKey] } $name = $ObjectId try { switch ($ObjectType) { 'User' { $obj = Get-MgUser -UserId $ObjectId -Property DisplayName -ErrorAction SilentlyContinue if ($obj) { $name = $obj.DisplayName } } 'Group' { $obj = Get-MgGroup -GroupId $ObjectId -Property DisplayName -ErrorAction SilentlyContinue if ($obj) { $name = $obj.DisplayName } } 'Role' { $obj = Get-MgDirectoryRole -DirectoryRoleId $ObjectId -ErrorAction SilentlyContinue if (-not $obj) { # Try role template $template = Get-MgDirectoryRoleTemplate -DirectoryRoleTemplateId $ObjectId -ErrorAction SilentlyContinue if ($template) { $name = $template.DisplayName } } else { $name = $obj.DisplayName } } 'Application' { $obj = Get-MgServicePrincipal -Filter "appId eq '$ObjectId'" -ErrorAction SilentlyContinue if ($obj) { $name = $obj.DisplayName } } } } catch { Write-Verbose "Could not resolve $ObjectType $ObjectId : $_" } $script:nameCache[$cacheKey] = $name return $name } $results = [System.Collections.Generic.List[PSCustomObject]]::new() } process { Write-Verbose "Retrieving Conditional Access policies..." try { $policies = Get-MgIdentityConditionalAccessPolicy -All -ErrorAction Stop } catch { throw "Failed to retrieve Conditional Access policies: $_" } # Filter by state if specified if ($PolicyState -ne 'All') { $stateMap = @{ 'Enabled' = 'enabled' 'Disabled' = 'disabled' 'ReportOnly' = 'enabledForReportingButNotEnforced' } $policies = $policies | Where-Object { $_.State -eq $stateMap[$PolicyState] } } Write-Verbose "Processing $($policies.Count) policies..." foreach ($policy in $policies) { $policyName = $policy.DisplayName $policyState = $policy.State $policyId = $policy.Id # Process excluded users if ($policy.Conditions.Users.ExcludeUsers) { foreach ($userId in $policy.Conditions.Users.ExcludeUsers) { $displayName = Resolve-ObjectName -ObjectId $userId -ObjectType 'User' $results.Add([PSCustomObject]@{ PolicyName = $policyName PolicyId = $policyId PolicyState = $policyState ExclusionType = 'User' ExcludedId = $userId ExcludedName = $displayName Recommendation = 'Review if user exclusion is still justified' }) } } # Process excluded groups if ($policy.Conditions.Users.ExcludeGroups) { foreach ($groupId in $policy.Conditions.Users.ExcludeGroups) { $displayName = Resolve-ObjectName -ObjectId $groupId -ObjectType 'Group' # Get group member count for risk assessment $memberCount = $null try { $members = Get-MgGroupMember -GroupId $groupId -CountVariable count -Top 1 -ErrorAction SilentlyContinue $memberCount = $count } catch {} $recommendation = if ($memberCount -gt 50) { "HIGH RISK: Large group ($memberCount members) - Review membership" } elseif ($memberCount -gt 10) { "MEDIUM RISK: Group has $memberCount members - Verify all are justified" } else { "Review if group exclusion is still justified" } $results.Add([PSCustomObject]@{ PolicyName = $policyName PolicyId = $policyId PolicyState = $policyState ExclusionType = 'Group' ExcludedId = $groupId ExcludedName = $displayName MemberCount = $memberCount Recommendation = $recommendation }) } } # Process excluded roles if ($policy.Conditions.Users.ExcludeRoles) { foreach ($roleId in $policy.Conditions.Users.ExcludeRoles) { $displayName = Resolve-ObjectName -ObjectId $roleId -ObjectType 'Role' $results.Add([PSCustomObject]@{ PolicyName = $policyName PolicyId = $policyId PolicyState = $policyState ExclusionType = 'Role' ExcludedId = $roleId ExcludedName = $displayName Recommendation = 'CRITICAL: Excluding roles weakens security posture' }) } } # Process excluded guests/external users if ($policy.Conditions.Users.ExcludeGuestsOrExternalUsers) { $guestConfig = $policy.Conditions.Users.ExcludeGuestsOrExternalUsers $results.Add([PSCustomObject]@{ PolicyName = $policyName PolicyId = $policyId PolicyState = $policyState ExclusionType = 'GuestsOrExternalUsers' ExcludedId = 'GuestsOrExternalUsers' ExcludedName = "Guest types: $($guestConfig.GuestOrExternalUserTypes -join ', ')" Recommendation = 'Review if guest exclusion aligns with Zero Trust principles' }) } # Process excluded applications if ($IncludeApplicationExclusions -and $policy.Conditions.Applications.ExcludeApplications) { foreach ($appId in $policy.Conditions.Applications.ExcludeApplications) { $displayName = Resolve-ObjectName -ObjectId $appId -ObjectType 'Application' $results.Add([PSCustomObject]@{ PolicyName = $policyName PolicyId = $policyId PolicyState = $policyState ExclusionType = 'Application' ExcludedId = $appId ExcludedName = $displayName Recommendation = 'Verify application exclusion is documented and necessary' }) } } } } end { Write-Verbose "Found $($results.Count) exclusions across $($policies.Count) policies" # Export if path specified if ($ExportPath) { try { $results | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 Write-Host "Results exported to: $ExportPath" -ForegroundColor Green } catch { Write-Error "Failed to export results: $_" } } # Return results return $results } } # Export function if loaded as module Export-ModuleMember -Function Get-ConditionalAccessExclusions -ErrorAction SilentlyContinue |