|
| 1 | +function Invoke-CippTestZTNA21835 { |
| 2 | + param($Tenant) |
| 3 | + |
| 4 | + $TestId = 'ZTNA21835' |
| 5 | + |
| 6 | + try { |
| 7 | + # Get Global Administrator role (template ID: 62e90394-69f5-4237-9190-012177145e10) |
| 8 | + $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' |
| 9 | + $GlobalAdminRole = $Roles | Where-Object { $_.roleTemplateId -eq '62e90394-69f5-4237-9190-012177145e10' } |
| 10 | + |
| 11 | + if (-not $GlobalAdminRole) { |
| 12 | + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Global Administrator role not found in database' -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' |
| 13 | + return |
| 14 | + } |
| 15 | + |
| 16 | + # Get permanent Global Administrator members |
| 17 | + $PermanentGAMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleId $GlobalAdminRole.id | Where-Object { |
| 18 | + $_.AssignmentType -eq 'Permanent' -and $_.'@odata.type' -eq '#microsoft.graph.user' |
| 19 | + } |
| 20 | + |
| 21 | + # Get Users data to check sync status |
| 22 | + $Users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' |
| 23 | + |
| 24 | + $EmergencyAccountCandidates = [System.Collections.Generic.List[object]]::new() |
| 25 | + |
| 26 | + foreach ($Member in $PermanentGAMembers) { |
| 27 | + $User = $Users | Where-Object { $_.id -eq $Member.principalId } |
| 28 | + |
| 29 | + # Only process cloud-only accounts |
| 30 | + if ($User -and $User.onPremisesSyncEnabled -ne $true) { |
| 31 | + # Note: Individual user authentication methods require per-user API calls not available in cache |
| 32 | + # Add all cloud-only permanent GAs as candidates (cannot verify auth methods from cache) |
| 33 | + $EmergencyAccountCandidates.Add([PSCustomObject]@{ |
| 34 | + Id = $User.id |
| 35 | + UserPrincipalName = $User.userPrincipalName |
| 36 | + DisplayName = $User.displayName |
| 37 | + OnPremisesSyncEnabled = $User.onPremisesSyncEnabled |
| 38 | + AuthenticationMethods = @('Unknown - requires per-user API call') |
| 39 | + CAPoliciesTargeting = 0 |
| 40 | + ExcludedFromAllCA = $false |
| 41 | + }) |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + # Get CA policies |
| 46 | + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' |
| 47 | + $EnabledCAPolicies = $CAPolicies | Where-Object { $_.state -eq 'enabled' } |
| 48 | + |
| 49 | + $EmergencyAccessAccounts = [System.Collections.Generic.List[object]]::new() |
| 50 | + |
| 51 | + foreach ($Candidate in $EmergencyAccountCandidates) { |
| 52 | + # Note: Transitive group and role memberships require per-user API calls not available in cache |
| 53 | + # Simplified check: only verify direct includes/excludes in CA policies |
| 54 | + $UserGroupIds = @() |
| 55 | + $UserRoles = @() |
| 56 | + $UserRoleIds = @() |
| 57 | + |
| 58 | + $PoliciesTargetingUser = 0 |
| 59 | + $ExcludedFromAll = $true |
| 60 | + |
| 61 | + foreach ($Policy in $EnabledCAPolicies) { |
| 62 | + $IsTargeted = $false |
| 63 | + |
| 64 | + # Check user includes/excludes |
| 65 | + $IncludeUsers = @($Policy.conditions.users.includeUsers) |
| 66 | + $ExcludeUsers = @($Policy.conditions.users.excludeUsers) |
| 67 | + |
| 68 | + if ($IncludeUsers -contains 'All' -or $IncludeUsers -contains $Candidate.Id) { |
| 69 | + $IsTargeted = $true |
| 70 | + } |
| 71 | + |
| 72 | + if ($ExcludeUsers -contains $Candidate.Id) { |
| 73 | + $IsTargeted = $false |
| 74 | + } |
| 75 | + |
| 76 | + # Check group includes/excludes |
| 77 | + if (-not $IsTargeted -and $UserGroupIds.Count -gt 0) { |
| 78 | + $IncludeGroups = @($Policy.conditions.users.includeGroups) |
| 79 | + $ExcludeGroups = @($Policy.conditions.users.excludeGroups) |
| 80 | + |
| 81 | + foreach ($GroupId in $UserGroupIds) { |
| 82 | + if ($IncludeGroups -contains $GroupId) { |
| 83 | + $IsTargeted = $true |
| 84 | + } |
| 85 | + if ($ExcludeGroups -contains $GroupId) { |
| 86 | + $IsTargeted = $false |
| 87 | + break |
| 88 | + } |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + # Check role includes/excludes |
| 93 | + $IncludeRoles = @($Policy.conditions.users.includeRoles) |
| 94 | + $ExcludeRoles = @($Policy.conditions.users.excludeRoles) |
| 95 | + |
| 96 | + foreach ($RoleId in $UserRoleIds) { |
| 97 | + $Role = $UserRoles | Where-Object { $_.id -eq $RoleId } |
| 98 | + if ($Role -and $IncludeRoles -contains $Role.roleTemplateId) { |
| 99 | + $IsTargeted = $true |
| 100 | + } |
| 101 | + if ($Role -and $ExcludeRoles -contains $Role.roleTemplateId) { |
| 102 | + $IsTargeted = $false |
| 103 | + break |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + if ($IsTargeted) { |
| 108 | + $PoliciesTargetingUser++ |
| 109 | + $ExcludedFromAll = $false |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + $Candidate.CAPoliciesTargeting = $PoliciesTargetingUser |
| 114 | + $Candidate.ExcludedFromAllCA = $ExcludedFromAll |
| 115 | + |
| 116 | + if ($ExcludedFromAll) { |
| 117 | + $EmergencyAccessAccounts.Add($Candidate) |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + $AccountCount = $EmergencyAccessAccounts.Count |
| 122 | + $Passed = 'Failed' |
| 123 | + $ResultMarkdown = '' |
| 124 | + |
| 125 | + if ($AccountCount -lt 2) { |
| 126 | + $ResultMarkdown = "Fewer than two emergency access accounts were identified based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n" |
| 127 | + } elseif ($AccountCount -ge 2 -and $AccountCount -le 4) { |
| 128 | + $Passed = 'Passed' |
| 129 | + $ResultMarkdown = "Emergency access accounts appear to be configured as per Microsoft guidance based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n" |
| 130 | + } else { |
| 131 | + $ResultMarkdown = "$AccountCount emergency access accounts appear to be configured based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions. Review these accounts to determine whether this volume is excessive for your organization.`n`n" |
| 132 | + } |
| 133 | + |
| 134 | + $ResultMarkdown += "**Summary:**`n" |
| 135 | + $ResultMarkdown += "- Total permanent Global Administrators: $($PermanentGAMembers.Count)`n" |
| 136 | + $ResultMarkdown += "- Cloud-only GAs with phishing-resistant auth: $($EmergencyAccountCandidates.Count)`n" |
| 137 | + $ResultMarkdown += "- Emergency access accounts (excluded from all CA): $AccountCount`n" |
| 138 | + $ResultMarkdown += "- Enabled Conditional Access policies: $($EnabledCAPolicies.Count)`n`n" |
| 139 | + |
| 140 | + if ($EmergencyAccessAccounts.Count -gt 0) { |
| 141 | + $ResultMarkdown += "## Emergency access accounts`n`n" |
| 142 | + $ResultMarkdown += "| Display name | UPN | Synced from on-premises | Authentication methods |`n" |
| 143 | + $ResultMarkdown += "| :----------- | :-- | :---------------------- | :--------------------- |`n" |
| 144 | + |
| 145 | + foreach ($Account in $EmergencyAccessAccounts) { |
| 146 | + $SyncStatus = if ($Account.OnPremisesSyncEnabled -ne $true) { 'No' } else { 'Yes' } |
| 147 | + $AuthMethodDisplay = ($Account.AuthenticationMethods | ForEach-Object { |
| 148 | + $_ -replace '#microsoft.graph.', '' -replace 'AuthenticationMethod', '' |
| 149 | + }) -join ', ' |
| 150 | + |
| 151 | + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($Account.Id)" |
| 152 | + $ResultMarkdown += "| $($Account.DisplayName) | [$($Account.UserPrincipalName)]($PortalLink) | $SyncStatus | $AuthMethodDisplay |`n" |
| 153 | + } |
| 154 | + $ResultMarkdown += "`n" |
| 155 | + } |
| 156 | + |
| 157 | + if ($PermanentGAMembers.Count -gt 0) { |
| 158 | + $ResultMarkdown += "## All permanent Global Administrators`n`n" |
| 159 | + $ResultMarkdown += "| Display name | UPN | Cloud only | All CA excluded | Phishing resistant auth |`n" |
| 160 | + $ResultMarkdown += "| :----------- | :-- | :--------: | :---------: | :---------------------: |`n" |
| 161 | + |
| 162 | + $UserSummary = [System.Collections.Generic.List[object]]::new() |
| 163 | + foreach ($Member in $PermanentGAMembers) { |
| 164 | + $User = $Users | Where-Object { $_.id -eq $Member.principalId } |
| 165 | + if (-not $User) { continue } |
| 166 | + |
| 167 | + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($User.id)" |
| 168 | + $IsCloudOnly = ($User.onPremisesSyncEnabled -ne $true) |
| 169 | + $CloudOnlyEmoji = if ($IsCloudOnly) { '✅' } else { '❌' } |
| 170 | + |
| 171 | + $EmergencyAccount = $EmergencyAccessAccounts | Where-Object { $_.Id -eq $User.id } |
| 172 | + $CAExcludedEmoji = if ($EmergencyAccount) { '✅' } else { '❌' } |
| 173 | + |
| 174 | + $Candidate = $EmergencyAccountCandidates | Where-Object { $_.Id -eq $User.id } |
| 175 | + $PhishingResistantEmoji = if ($Candidate) { '✅' } else { '❌' } |
| 176 | + |
| 177 | + $UserSummary.Add([PSCustomObject]@{ |
| 178 | + DisplayName = $User.displayName |
| 179 | + UserPrincipalName = $User.userPrincipalName |
| 180 | + PortalLink = $PortalLink |
| 181 | + CloudOnly = $CloudOnlyEmoji |
| 182 | + CAExcluded = $CAExcludedEmoji |
| 183 | + PhishingResistant = $PhishingResistantEmoji |
| 184 | + }) |
| 185 | + } |
| 186 | + |
| 187 | + foreach ($UserSum in $UserSummary) { |
| 188 | + $ResultMarkdown += "| $($UserSum.DisplayName) | [$($UserSum.UserPrincipalName)]($($UserSum.PortalLink)) | $($UserSum.CloudOnly) | $($UserSum.CAExcluded) | $($UserSum.PhishingResistant) |`n" |
| 189 | + } |
| 190 | + |
| 191 | + $ResultMarkdown += "`n" |
| 192 | + } |
| 193 | + |
| 194 | + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' |
| 195 | + |
| 196 | + } catch { |
| 197 | + $ErrorMessage = Get-CippException -Exception $_ |
| 198 | + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage |
| 199 | + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' |
| 200 | + } |
| 201 | +} |
0 commit comments