private/tests-shared/Get-ZtEmergencyAccessAccounts.ps1
|
<#
.SYNOPSIS Gets emergency access accounts from the configuration. .DESCRIPTION Returns emergency access accounts defined in the ZeroTrustAssessment configuration file. Uses Maester-style configuration format where customers explicitly define their emergency/breakglass accounts. Configuration format (in zt-config.json) - follows Maester format: { "GlobalSettings": { "EmergencyAccessAccounts": [ { "Type": "User", "UserPrincipalName": "breakglass1@contoso.com" }, { "Type": "User", "Id": "00000000-0000-0000-0000-000000000001" }, { "Type": "Group", "Id": "00000000-0000-0000-0000-000000000002" } ] } } Note: Group-based emergency accounts are resolved at runtime via Microsoft Graph API. .PARAMETER Database The DuckDB database connection used to resolve user information. .OUTPUTS Array of PSCustomObject with properties: - Id: User's object ID - UserPrincipalName: User's UPN - DisplayName: User's display name - Type: 'User' or 'GroupMember' (indicates user resolved from a configured group) .EXAMPLE $emergencyAccounts = Get-ZtEmergencyAccessAccounts -Database $Database .NOTES Created to fix Issue #266 - Test 21815 incorrectly flags emergency access accounts as failures for having permanent privileged role assignments. Updated to use config-based approach per PM feedback (FIDO2 requirement too strict). #> function Get-ZtEmergencyAccessAccounts { [CmdletBinding()] param( [Parameter(Mandatory = $true)] $Database ) Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose Write-PSFMessage 'Getting emergency access accounts from configuration' -Level Verbose # Get emergency accounts from PSFConfig (set by Invoke-ZtAssessment) $configuredAccounts = Get-PSFConfigValue -FullName 'ZeroTrustAssessment.EmergencyAccessAccounts' if (-not $configuredAccounts -or $configuredAccounts.Count -eq 0) { Write-PSFMessage 'No emergency access accounts configured' -Level Verbose return @() } Write-PSFMessage "Found $($configuredAccounts.Count) configured emergency access accounts" -Level Verbose $emergencyAccessAccounts = @() foreach ($account in $configuredAccounts) { $type = $account.Type $id = $account.Id $upn = $account.UserPrincipalName if ($type -eq 'User') { # Resolve user by UPN or ID if ($upn) { # Lower-case both sides for case-insensitive UPN match (portable; avoids DB-specific COLLATE syntax) $escapedUpn = ($upn.ToLowerInvariant()) -replace "'", "''" $sql = "SELECT id, userPrincipalName, displayName FROM User WHERE LOWER(userPrincipalName) = '$escapedUpn'" } elseif ($id) { $guidRef = [System.Guid]::Empty if (-not [System.Guid]::TryParse($id, [ref]$guidRef)) { Write-PSFMessage "Skipping invalid user entry: Id '$id' is not a valid GUID" -Level Warning continue } $escapedId = $guidRef.ToString() $sql = "SELECT id, userPrincipalName, displayName FROM User WHERE id = '$escapedId'" } else { Write-PSFMessage "Skipping invalid user entry: no Id or UserPrincipalName provided" -Level Warning continue } $user = Invoke-DatabaseQuery -Database $Database -Sql $sql | Select-Object -First 1 if ($user) { $emergencyAccessAccounts += [PSCustomObject]@{ Id = $user.id UserPrincipalName = $user.userPrincipalName DisplayName = $user.displayName Type = 'User' } Write-PSFMessage "Emergency access user found: $($user.userPrincipalName)" -Level Verbose } else { Write-PSFMessage "Emergency access user not found in tenant: UPN=$upn, Id=$id" -Level Warning } } elseif ($type -eq 'Group') { if (-not $id) { Write-PSFMessage "Skipping invalid group entry: no Id provided" -Level Warning continue } $guidRef = [System.Guid]::Empty if (-not [System.Guid]::TryParse($id, [ref]$guidRef)) { Write-PSFMessage "Skipping invalid group entry: Id '$id' is not a valid GUID" -Level Warning continue } # Resolve group members via Microsoft Graph API (GroupMember table not available in DB) try { Write-PSFMessage "Resolving emergency access group members via Graph API: Id=$id" -Level Verbose $membersResponse = Get-ZtGroupMember -GroupId $id -Recurse -ErrorAction Stop $members = @($membersResponse | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.user' }) if ($members.Count -gt 0) { # Batch all member IDs into a single SQL lookup to avoid N+1 queries; # member IDs come from Graph API responses which are always valid GUIDs. $escapedIds = $members | ForEach-Object { $memberGuid = [System.Guid]::Empty if ([System.Guid]::TryParse($_.id, [ref]$memberGuid)) { "'" + $memberGuid.ToString() + "'" } } | Where-Object { $_ } if (-not $escapedIds) { Write-PSFMessage "Emergency access group members had no valid GUIDs: Id=$id" -Level Warning } else { $idList = $escapedIds -join ',' $memberSql = "SELECT id, userPrincipalName, displayName FROM User WHERE id IN ($idList)" $userDetailsList = @(Invoke-DatabaseQuery -Database $Database -Sql $memberSql) foreach ($userDetails in $userDetailsList) { $emergencyAccessAccounts += [PSCustomObject]@{ Id = $userDetails.id UserPrincipalName = $userDetails.userPrincipalName DisplayName = $userDetails.displayName Type = 'GroupMember' } Write-PSFMessage "Emergency access group member found: $($userDetails.userPrincipalName)" -Level Verbose } } } else { Write-PSFMessage "Emergency access group has no user members: Id=$id" -Level Warning } } catch { Write-PSFMessage "Failed to resolve emergency access group members: Id=$id. Error: $($_.Exception.Message)" -Level Warning } } else { Write-PSFMessage "Skipping unknown account type: $type" -Level Warning } } Write-PSFMessage "Total emergency access accounts resolved: $($emergencyAccessAccounts.Count)" -Level Verbose return $emergencyAccessAccounts } |