tests/Test-Assessment.21835.ps1
|
<# .SYNOPSIS Checks if emergency access accounts are configured appropriately .DESCRIPTION This test identifies emergency access accounts based on: - Permanent Global Administrator role assignment (cloud-only) - Phishing-resistant authentication methods (FIDO2 and/or Certificate) - Exclusion from all enabled Conditional Access policies The test evaluates whether 2-4 emergency accounts are configured per Microsoft guidance. #> function Test-Assessment-21835 { [ZtTest( Category = 'Application management', ImplementationCost = 'High', MinimumLicense = ('P1'), Pillar = 'Identity', RiskLevel = 'High', SfiPillar = 'Protect engineering systems', TenantType = ('Workforce'), TestId = 21835, Title = 'Emergency access accounts are configured appropriately', UserImpact = 'Low' )] [CmdletBinding()] param() Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose if ( -not (Get-ZtLicense EntraIDP1) ) { Add-ZtTestResultDetail -SkippedBecause NotLicensedEntraIDP1 return } $activity = 'Checking emergency access accounts configuration' Write-ZtProgress -Activity $activity -Status 'Starting assessment' #region Step 1: Find permanent Global Administrator users Write-ZtProgress -Activity $activity -Status 'Finding Global Administrator role members' # Global Administrator role template ID: 62e90394-69f5-4237-9190-012177145e10 $sql = @" SELECT vr.principalId as id, vr.principalDisplayName as displayName, vr.userPrincipalName, vr.privilegeType, u.onPremisesSyncEnabled, vr."@odata.type" FROM vwRole vr LEFT JOIN "User" u ON vr.principalId = u.id WHERE vr.roleDefinitionId = '62e90394-69f5-4237-9190-012177145e10' AND vr.privilegeType = 'Permanent' AND vr."@odata.type" = '#microsoft.graph.user' "@ $permanentGAUsers = @(Invoke-DatabaseQuery -Database $Database -Sql $sql) Write-PSFMessage "Total permanent GA users: $($permanentGAUsers.Count)" -Level Verbose #endregion #region Step 2: Find cloud-only GAs with phishing-resistant auth methods Write-ZtProgress -Activity $activity -Status 'Analyzing authentication methods' $emergencyAccountCandidates = @() foreach ($user in $permanentGAUsers) { # Only process cloud-only accounts (onPremisesSyncEnabled is null or false) if ($null -eq $user.onPremisesSyncEnabled -or $user.onPremisesSyncEnabled -eq $false) { Write-PSFMessage "Checking auth methods for cloud-only user: $($user.userPrincipalName)" -Level Verbose # Use Get-ZtUserAuthenticationMethod helper to get authentication methods $userAuthInfo = Get-ZtUserAuthenticationMethod -UserId $user.id $authMethods = $userAuthInfo.AuthenticationMethods if ($authMethods) { # Check if user only has FIDO2 and/or Certificate auth methods $hasOnlyPhishingResistant = $true $authMethodTypes = @() foreach ($method in $authMethods) { $methodType = $method.'@odata.type' $authMethodTypes += $methodType # If any method is not FIDO2 or Certificate, mark as not phishing-resistant only if ($methodType -ne '#microsoft.graph.fido2AuthenticationMethod' -and $methodType -ne '#microsoft.graph.x509CertificateAuthenticationMethod') { $hasOnlyPhishingResistant = $false } } if ($hasOnlyPhishingResistant -and $authMethodTypes.Count -gt 0) { # This is a candidate emergency account $emergencyAccountCandidates += [PSCustomObject]@{ Id = $user.id UserPrincipalName = $user.userPrincipalName DisplayName = $user.displayName OnPremisesSyncEnabled = $user.onPremisesSyncEnabled AuthenticationMethods = $authMethodTypes CAPoliciesTargeting = 0 ExcludedFromAllCA = $false } Write-PSFMessage "Candidate emergency account found: $($user.userPrincipalName)" -Level Verbose } } } } Write-PSFMessage "Emergency account candidates (cloud-only with phishing-resistant auth): $($emergencyAccountCandidates.Count)" -Level Verbose #endregion #region Step 3 & 4: Get CA policies and check if candidates are excluded from all Write-ZtProgress -Activity $activity -Status 'Analyzing Conditional Access policies' # Use Get-ZtConditionalAccessPolicy helper function $allCAPolicies = Get-ZtConditionalAccessPolicy $enabledCAPolicies = $allCAPolicies | Where-Object { $_.state -eq 'enabled' } Write-PSFMessage "Found $($enabledCAPolicies.Count) enabled CA policies" -Level Verbose $emergencyAccessAccounts = @() foreach ($candidate in $emergencyAccountCandidates) { Write-PSFMessage "Checking CA policy targeting for: $($candidate.UserPrincipalName)" -Level Verbose # Query 6: Get transitive group memberships $userGroups = Invoke-ZtGraphRequest -RelativeUri "users/$($candidate.Id)/transitiveMemberOf/microsoft.graph.group" ` -Select 'id' -ApiVersion v1.0 $userGroupIds = @($userGroups | Select-Object -ExpandProperty id) # Query 7: Get directory role memberships $userRoles = Invoke-ZtGraphRequest -RelativeUri "users/$($candidate.Id)/memberOf/microsoft.graph.directoryRole" ` -Select 'id,roleTemplateId' -ApiVersion v1.0 $userRoleIds = @($userRoles | Select-Object -ExpandProperty id) $policiesTargetingUser = 0 $excludedFromAll = $true foreach ($policy in $enabledCAPolicies) { $isTargeted = $false # Check user includes/excludes $includeUsers = @($policy.conditions.users.includeUsers) $excludeUsers = @($policy.conditions.users.excludeUsers) # Check if user is explicitly included if ($includeUsers -contains 'All' -or $includeUsers -contains $candidate.Id) { $isTargeted = $true } # Check if user is excluded if ($excludeUsers -contains $candidate.Id) { $isTargeted = $false } # Check group includes/excludes if not already determined if (-not $isTargeted -and $userGroupIds.Count -gt 0) { $includeGroups = @($policy.conditions.users.includeGroups) $excludeGroups = @($policy.conditions.users.excludeGroups) foreach ($groupId in $userGroupIds) { if ($includeGroups -contains $groupId) { $isTargeted = $true } if ($excludeGroups -contains $groupId) { $isTargeted = $false break } } } # Check role includes/excludes $includeRoles = @($policy.conditions.users.includeRoles) $excludeRoles = @($policy.conditions.users.excludeRoles) foreach ($roleId in $userRoleIds) { $role = $userRoles | Where-Object { $_.id -eq $roleId } if ($includeRoles -contains $role.roleTemplateId) { $isTargeted = $true } if ($excludeRoles -contains $role.roleTemplateId) { $isTargeted = $false break } } if ($isTargeted) { $policiesTargetingUser++ $excludedFromAll = $false } } $candidate.CAPoliciesTargeting = $policiesTargetingUser $candidate.ExcludedFromAllCA = $excludedFromAll if ($excludedFromAll) { $emergencyAccessAccounts += $candidate Write-PSFMessage "Emergency access account confirmed: $($candidate.UserPrincipalName)" -Level Verbose } } #endregion #region Step 5: Evaluate results and generate report Write-ZtProgress -Activity $activity -Status 'Generating results' $accountCount = $emergencyAccessAccounts.Count Write-PSFMessage "Total emergency access accounts identified: $accountCount" -Level Verbose # Determine pass/fail status $passed = $false $testResultMarkdown = '' if ($accountCount -lt 2) { $passed = $false $testResultMarkdown = "Fewer than two emergency access accounts were identified based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n" } elseif ($accountCount -ge 2 -and $accountCount -le 4) { $passed = $true $testResultMarkdown = "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" } else { $passed = $false $testResultMarkdown = "$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" } # Add summary information $testResultMarkdown += "**Summary:**`n" $testResultMarkdown += "- Total permanent Global Administrators: $($permanentGAUsers.Count)`n" $testResultMarkdown += "- Cloud-only GAs with phishing-resistant auth: $($emergencyAccountCandidates.Count)`n" $testResultMarkdown += "- Emergency access accounts (excluded from all CA): $accountCount`n" $testResultMarkdown += "- Enabled Conditional Access policies: $($enabledCAPolicies.Count)`n`n" # Add details table if ($emergencyAccessAccounts.Count -gt 0) { $testResultMarkdown += "## Emergency access accounts`n`n" $testResultMarkdown += "| Display name | UPN | Synced from on-premises | Authentication methods | CA policies targeting |`n" $testResultMarkdown += "| :----------- | :-- | :---------------------- | :--------------------- | :-------------------- |`n" foreach ($account in $emergencyAccessAccounts) { $syncStatus = if ($null -eq $account.OnPremisesSyncEnabled) { 'No' } else { if ($account.OnPremisesSyncEnabled) { 'Yes' } else { 'No' } } $authMethodDisplay = ($account.AuthenticationMethods | ForEach-Object { $_ -replace '#microsoft.graph.', '' -replace 'AuthenticationMethod', '' }) -join ', ' $portalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($account.Id)" $testResultMarkdown += "| $(Get-SafeMarkdown -Text $account.DisplayName) | [$(Get-SafeMarkdown -Text $account.UserPrincipalName)]($portalLink) | $syncStatus | $authMethodDisplay | $($account.CAPoliciesTargeting) |`n" } $testResultMarkdown += "`n" } # Add candidates that didn't qualify if ($emergencyAccountCandidates.Count -gt $emergencyAccessAccounts.Count) { $testResultMarkdown += "## Accounts not excluded from all CA policies`n`n" $testResultMarkdown += "These accounts have the correct authentication configuration but are targeted by one or more CA policies:`n`n" $testResultMarkdown += "| Display name | UPN | CA policies targeting |`n" $testResultMarkdown += "| :----------- | :-- | :-------------------- |`n" $targetedCandidates = $emergencyAccountCandidates | Where-Object { -not $_.ExcludedFromAllCA } foreach ($account in $targetedCandidates) { $portalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($account.Id)" $testResultMarkdown += "| $(Get-SafeMarkdown -Text $account.DisplayName) | [$(Get-SafeMarkdown -Text $account.UserPrincipalName)]($portalLink) | $($account.CAPoliciesTargeting) |`n" } $testResultMarkdown += "`n" } #endregion Add-ZtTestResultDetail -TestId '21835' ` -Status $passed ` -Result $testResultMarkdown } |