public/maester/entra/Test-MtCaEmergencyAccessExists.ps1
|
<# .Synopsis Checks if the tenant has at least one emergency/break glass account or account group excluded from all conditional access policies .Description It is recommended to have at least one emergency/break glass account or account group excluded from all conditional access policies. This allows for emergency access to the tenant in case of a misconfiguration or other issues. Learn more: https://learn.microsoft.com/entra/identity/role-based-access-control/security-emergency-access .Example Test-MtCaEmergencyAccessExists .LINK https://maester.dev/docs/commands/Test-MtCaEmergencyAccessExists #> function Test-MtCaEmergencyAccessExists { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Exists is not a plural.')] [CmdletBinding()] [OutputType([bool])] param () if ( ( Get-MtLicenseInformation EntraID ) -eq 'Free' ) { Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP1 return $null } $EmergencyAccessAccounts = Get-MtMaesterConfigGlobalSetting -SettingName 'EmergencyAccessAccounts' try { # Only check policies that are not related to authentication context (the state of policy does not have to be enabled) $policies = Get-MtConditionalAccessPolicy | Where-Object { -not $_.conditions.applications.includeAuthenticationContextClassReferences } # Remove policies that are scoped to service principals $policies = $policies | Where-Object { -not $_.conditions.clientApplications.includeServicePrincipals } $result = $false $PolicyCount = $policies | Measure-Object | Select-Object -ExpandProperty Count if (-not $EmergencyAccessAccounts -or $EmergencyAccessAccounts.Count -eq 0) { Write-Verbose "No emergency access accounts or groups defined in the Maester config. Use the default logic to detect emergency access accounts or groups." $ExcludedUserObjectGUID = $policies.conditions.users.excludeUsers | Group-Object -NoElement | Sort-Object -Property Count -Descending | Select-Object -First 1 -ExpandProperty Name $ExcludedUsers = $policies.conditions.users.excludeUsers | Group-Object -NoElement | Sort-Object -Property Count -Descending | Select-Object -First 1 | Select-Object -ExpandProperty Count $ExcludedGroupObjectGUID = $policies.conditions.users.excludeGroups | Group-Object -NoElement | Sort-Object -Property Count -Descending | Select-Object -First 1 -ExpandProperty Name $ExcludedGroups = $policies.conditions.users.excludeGroups | Group-Object -NoElement | Sort-Object -Property Count -Descending | Select-Object -First 1 | Select-Object -ExpandProperty Count # If the number of enabled policies is not the same as the number of excluded users or groups, there is no emergency access if ($PolicyCount -eq $ExcludedUsers -or $PolicyCount -eq $ExcludedGroups) { $result = $true } else { # If the number of excluded users is higher than the number of excluded groups, check the user object GUID $CheckId = $ExcludedGroupObjectGUID $EmergencyAccessUUIDType = 'group' if ($ExcludedUsers -gt $ExcludedGroups) { $EmergencyAccessUUIDType = 'user' $CheckId = $ExcludedUserObjectGUID } # Get displayName of the emergency access account or group if ($CheckId) { if ($EmergencyAccessUUIDType -eq 'user') { $DisplayName = Invoke-MtGraphRequest -RelativeUri "users/$CheckId" -Select displayName | Select-Object -ExpandProperty displayName } else { $DisplayName = Invoke-MtGraphRequest -RelativeUri "groups/$CheckId" -Select displayName | Select-Object -ExpandProperty displayName } Write-Verbose "Emergency access account or group: $CheckId" $testResult = "Automatically detected emergency access`n`n* $($EmergencyAccessUUIDType): $DisplayName ($CheckId)`n`n" } $policiesWithoutEmergency = $policies | Where-Object { $CheckId -notin $_.conditions.users.excludeUsers -and $CheckId -notin $_.conditions.users.excludeGroups } $policiesWithoutEmergency | Select-Object -ExpandProperty displayName | Sort-Object | ForEach-Object { Write-Verbose "Conditional Access policy $_ does not exclude emergency access $EmergencyAccessUUIDType" } } $testResult += "These conditional access policies don't have the emergency access $EmergencyAccessUUIDType excluded:`n`n%TestResult%" Add-MtTestResultDetail -GraphObjects $policiesWithoutEmergency -GraphObjectType ConditionalAccess -Result $testResult return $result } else { # Resolve emergency access accounts/groups to object IDs and get display names $ResolvedEmergencyAccessAccounts = @() foreach ($account in $EmergencyAccessAccounts) { # Use either Id or UserPrincipalName to identify the account / group and UserPrincipalName as fallback $identifier = if ($account.Id) { $account.Id } else { $account.UserPrincipalName } # Determine the type (user or group) while defaulting to 'user' $type = if ($account.Type) { $account.Type.ToLower() } else { 'user' } if ($identifier -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { # It's an object ID try { $endpoint = if ($type -eq 'group') { "groups/$identifier" } else { "users/$identifier" } $object = Invoke-MtGraphRequest -RelativeUri $endpoint -Select id, displayName -ErrorAction Stop if ($object) { Write-Verbose "Emergency access $type`: $($object.displayName) ($identifier)" $ResolvedEmergencyAccessAccounts += @{ObjectId = $object.id; displayName = $object.displayName; type = $type } } else { Write-Warning "Could not resolve emergency access $type ID: $identifier" } } catch { Write-Warning "Could not resolve emergency access $type ID: $identifier. Error: $_" } } elseif ($identifier -match '^[^@]+@[^@]+\.[^@]+$') { # It's a UPN - could be user or group try { $endpoint = if ($type -eq 'group') { "groups" } else { "users/$identifier" } if ($type -eq 'group') { # For groups, we need to filter by mail or mailNickname $object = Invoke-MtGraphRequest -RelativeUri $endpoint -Filter "mail eq '$identifier' or mailNickname eq '$identifier'" -ErrorAction Stop | Select-Object -First 1 } else { $object = Invoke-MtGraphRequest -RelativeUri $endpoint -Select id, displayName -ErrorAction Stop } if ($object) { Write-Verbose "Emergency access $type`: $($object.displayName) ($($object.id))" $ResolvedEmergencyAccessAccounts += @{ObjectId = $object.id; displayName = $object.displayName; type = $type } } else { Write-Warning "Could not resolve emergency access $type`: $identifier" } } catch { Write-Warning "Could not resolve emergency access $type`: $identifier. Error: $_" } } else { Write-Warning "Invalid identifier format for emergency access account: $identifier" } } Write-Verbose "Emergency access accounts or groups defined in the Maester config: $($EmergencyAccessAccounts.Count) entries" $ResolvedEmergencyAccessUsers = $ResolvedEmergencyAccessAccounts | Where-Object { $_.type -eq 'user' } $ResolvedEmergencyAccessGroups = $ResolvedEmergencyAccessAccounts | Where-Object { $_.type -eq 'group' } $EmergencyAccessAccountsUserCount = @($ResolvedEmergencyAccessUsers).Count $EmergencyAccessAccountsGroupCount = @($ResolvedEmergencyAccessGroups).Count # Find policies that are missing ANY of the configured emergency access accounts or groups $policiesWithoutEmergency = $policies | Where-Object { $CurrentPolicy = $_ $missingEmergency = $false # Check if all configured emergency users are excluded if ($EmergencyAccessAccountsUserCount -gt 0) { $ExcludedKnownUsers = @($CurrentPolicy.conditions.users.excludeUsers | Where-Object { $_ -in $ResolvedEmergencyAccessUsers.ObjectId }).Count if ($ExcludedKnownUsers -lt $EmergencyAccessAccountsUserCount) { $missingEmergency = $true } } # Check if all configured emergency groups are excluded if ($EmergencyAccessAccountsGroupCount -gt 0) { $ExcludedKnownGroups = @($CurrentPolicy.conditions.users.excludeGroups | Where-Object { $_ -in $ResolvedEmergencyAccessGroups.ObjectId }).Count if ($ExcludedKnownGroups -lt $EmergencyAccessAccountsGroupCount) { $missingEmergency = $true } } $missingEmergency } if ($policiesWithoutEmergency.Count -eq 0) { $result = $true $testResult = "All conditional access policies exclude the configured emergency access accounts or groups:`n`n" $ResolvedEmergencyAccessAccounts | ForEach-Object { $typeLabel = if ($_.type -eq 'group') { 'Group' } else { 'User' } if ($_.displayName) { $testResult += "* $typeLabel`: $($_.displayName) ($($_.ObjectId))`n" } else { $testResult += "* $typeLabel`: $($_.ObjectId)`n" } } Add-MtTestResultDetail -Result $testResult return $result } else { $testResult = "Configured emergency access accounts or groups:`n`n" $ResolvedEmergencyAccessAccounts | ForEach-Object { $typeLabel = if ($_.type -eq 'group') { 'Group' } else { 'User' } if ($_.displayName) { $testResult += "* $typeLabel`: $($_.displayName) ($($_.ObjectId))`n" } else { $testResult += "* $typeLabel`: $($_.ObjectId)`n" } } $testResult += "`n`nThese conditional access policies don't have the configured emergency access accounts and groups excluded:`n`n%TestResult%" Add-MtTestResultDetail -GraphObjects $policiesWithoutEmergency -GraphObjectType ConditionalAccess -Result $testResult return $result } } } catch { Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ return $null } } |