tests/Test-Assessment.25383.ps1
|
<#
.SYNOPSIS Validates that Global Administrator and Global Secure Access Administrator roles are tightly limited. .DESCRIPTION This test checks if Global Administrator (GA) and Global Secure Access Administrator (GSA) roles are assigned only to vetted Member users, without groups, guests, or service principals, to prevent tenant-wide compromise from a single identity compromise. .NOTES Test ID: 25383 Category: Global Secure Access Required API: Uses database queries (vwRole, User, RoleDefinition tables) #> function Test-Assessment-25383 { [ZtTest( Category = 'Global Secure Access', ImplementationCost = 'Low', MinimumLicense = ('AAD_PREMIUM'), Pillar = 'Network', RiskLevel = 'High', SfiPillar = 'Protect identities and secrets', TenantType = ('Workforce'), TestId = 25383, Title = 'Global and GSA admin privileges are tightly limited to prevent tenant-wide compromise', UserImpact = 'Low' )] [CmdletBinding()] param( $Database ) #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking Global Administrator and GSA role assignments' Write-ZtProgress -Activity $activity -Status 'Getting role definitions' # Query Q1: Get role definitions for GA and GSA from database $sqlRoleDefinitions = @" SELECT id, displayName, templateId FROM main."RoleDefinition" WHERE displayName = 'Global Administrator' OR displayName = 'Global Secure Access Administrator' "@ $roleDefinitions = @(Invoke-DatabaseQuery -Database $Database -Sql $sqlRoleDefinitions -AsCustomObject) $gaRole = $roleDefinitions | Where-Object { $_.displayName -eq 'Global Administrator' } $gsaRole = $roleDefinitions | Where-Object { $_.displayName -eq 'Global Secure Access Administrator' } # Get tenant ID for portal links $tenantId = (Get-MgContext).TenantId # Query Q2/Q3: Get role assignments for GA and GSA roles from vwRole view with user details Write-ZtProgress -Activity $activity -Status 'Getting role assignments' $sqlRoleAssignments = @" SELECT vr.roleDefinitionId, vr.roleDisplayName, vr.principalId, vr.principalDisplayName, vr.userPrincipalName, vr."@odata.type" as principalOdataType, vr.privilegeType, u.userType, u.accountEnabled FROM main.vwRole vr LEFT JOIN main."User" u ON vr.principalId = u.id WHERE vr.roleDisplayName = 'Global Administrator' OR vr.roleDisplayName = 'Global Secure Access Administrator' "@ $allRoleAssignments = @(Invoke-DatabaseQuery -Database $Database -Sql $sqlRoleAssignments -AsCustomObject) # Define roles to check $rolesToCheck = @( @{ Role = $gaRole; Name = 'Global Administrator' } @{ Role = $gsaRole; Name = 'Global Secure Access Administrator' } ) $allResults = @() foreach ($roleInfo in $rolesToCheck) { $role = $roleInfo.Role $roleName = $roleInfo.Name $result = [PSCustomObject]@{ RoleName = $roleName RoleDefinitionId = $null TemplateId = $null Found = $false TotalCount = 0 Issues = @() DisabledAssignments = @() ValidAssignments = @() HasIssues = $false HasDisabledUsers = $false ExceedsThreshold = $false } if (-not $role -or $role.Count -eq 0) { $allResults += $result continue } $result.Found = $true $result.RoleDefinitionId = $role.id $result.TemplateId = $role.templateId # Filter assignments locally for this role $assignments = $allRoleAssignments | Where-Object { $_.roleDefinitionId -eq $role.id } foreach ($assignment in $assignments) { # Determine principal type from @odata.type $principalType = switch -Wildcard ($assignment.principalOdataType) { '*user' { 'user' } '*group' { 'group' } '*servicePrincipal' { 'servicePrincipal' } default { 'Unknown' } } $assignmentInfo = [PSCustomObject]@{ DisplayName = $assignment.principalDisplayName UPN = $assignment.userPrincipalName Type = $principalType UserType = if ($assignment.userType) { $assignment.userType } else { 'N/A' } AccountEnabled = $assignment.accountEnabled Issue = $null } # Check for non-compliant assignments if ($principalType -eq 'group') { $assignmentInfo.Issue = 'Group assignment' $result.Issues += $assignmentInfo } elseif ($principalType -eq 'servicePrincipal') { $assignmentInfo.Issue = 'Service Principal assignment' $result.Issues += $assignmentInfo } elseif ($principalType -eq 'user' -and $assignment.userType -eq 'Guest') { $assignmentInfo.Issue = 'Guest user assignment' $result.Issues += $assignmentInfo } elseif ($principalType -eq 'user' -and $assignment.accountEnabled -eq $false) { # Disabled Member users - latent risk, indicates poor lifecycle management $assignmentInfo.Issue = 'Disabled account with privileged role' $result.DisabledAssignments += $assignmentInfo } elseif ($principalType -eq 'user' -and $assignment.accountEnabled -eq $true) { # Only enabled Member users are considered valid $result.ValidAssignments += $assignmentInfo } } # TotalCount includes all categorized assignments $result.TotalCount = $result.Issues.Count + $result.DisabledAssignments.Count + $result.ValidAssignments.Count $result.HasIssues = $result.Issues.Count -gt 0 $result.HasDisabledUsers = $result.DisabledAssignments.Count -gt 0 $result.ExceedsThreshold = $result.TotalCount -gt 5 $allResults += $result } #endregion Data Collection #region Assessment Logic # Determine result status based on spec evaluation logic $gaResult = $allResults | Where-Object { $_.RoleName -eq 'Global Administrator' } # Per spec: GA not found → Fail (critical role must exist) # Per spec: GSA not found → Skip GSA evaluation (role may not be provisioned) $gaNotFound = -not $gaResult.Found # Only evaluate roles that were found $foundResults = $allResults | Where-Object { $_.Found } $hasAnyIssues = ($foundResults | Where-Object { $_.HasIssues }).Count -gt 0 $hasAnyDisabledUsers = ($foundResults | Where-Object { $_.HasDisabledUsers }).Count -gt 0 $exceedsAnyThreshold = ($foundResults | Where-Object { $_.ExceedsThreshold }).Count -gt 0 $passed = $false $customStatus = $null # Fail: Global Administrator role not found (critical role must exist) if ($gaNotFound) { $testResultMarkdown = "❌ Global Administrator role definition not found. This is a critical role that must exist in all tenants.`n`n%TestResult%" } # Fail: Groups, guests, or service principals assigned elseif ($hasAnyIssues) { $testResultMarkdown = "❌ GA/GSA roles include groups, guests, or service principals requiring immediate review.`n`n%TestResult%" } # Investigate: Disabled users with privileged roles - latent risk elseif ($hasAnyDisabledUsers) { $passed = $true $customStatus = 'Investigate' $testResultMarkdown = "⚠️ GA/GSA roles include disabled Member users or exceed recommended assignment thresholds; review and remediate.`n`n%TestResult%" } # Investigate: Excessive assignments but no other issues elseif ($exceedsAnyThreshold) { $passed = $true $customStatus = 'Investigate' $testResultMarkdown = "⚠️ GA/GSA roles include disabled Member users or exceed recommended assignment thresholds; review and remediate.`n`n%TestResult%" } # Pass: All principals are enabled Member users within limits else { $passed = $true $testResultMarkdown = "✅ GA/GSA roles are limited to enabled, vetted Member users; no groups, guests, service principals, or disabled accounts are assigned, and assignment counts are within approved limits.`n`n%TestResult%" } #endregion Assessment Logic #region Report Generation # Build report $mdInfo = '' foreach ($result in $allResults) { $mdInfo += "`n## [$($result.RoleName) assignments](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles)`n`n" if (-not $result.Found) { if ($result.RoleName -eq 'Global Administrator') { $mdInfo += "❌ $($result.RoleName) role definition not found. This is a critical role that must exist.`n`n" } else { $mdInfo += "ℹ️ $($result.RoleName) role definition not found. Skipping evaluation (role may not be provisioned in tenant).`n`n" } continue } $mdInfo += "**Role Definition ID**: $($result.RoleDefinitionId) `n" $mdInfo += "**Total Assignment Count**: $($result.TotalCount) `n" $mdInfo += "**Valid Assignment Count**: $($result.ValidAssignments.Count) `n" $mdInfo += "**Issue Count**: $($result.Issues.Count + $result.DisabledAssignments.Count) `n`n" if ($result.Issues.Count -gt 0) { $mdInfo += "### ❌ Non-compliant assignments`n`n" $mdInfo += "| Name | Principal name | Type | User type | Account enabled | Status |`n" $mdInfo += "| :----------- | :-- | :--- | :-------- | :-------------- | :----- |`n" $maxDisplay = 5 $displayIssues = $result.Issues if ($result.Issues.Count -gt $maxDisplay) { $displayIssues = $result.Issues[0..($maxDisplay - 1)] } foreach ($issue in $displayIssues) { $mdInfo += "| $(Get-SafeMarkdown $issue.DisplayName) | $(Get-SafeMarkdown $issue.UPN) | $($issue.Type) | $($issue.UserType) | $($issue.AccountEnabled) | Fail |`n" } if ($result.Issues.Count -gt $maxDisplay) { $roleNameEncoded = [System.Uri]::EscapeDataString($result.RoleName) $portalUrl = "https://entra.microsoft.com/#view/Microsoft_Azure_PIMCommon/UserRolesViewModelMenuBlade/~/members/roleObjectId/$($result.TemplateId)/roleId/$($result.TemplateId)/roleTemplateId/$($result.TemplateId)/roleName/$roleNameEncoded/isRoleCustom~/false/resourceScopeId/%2F/resourceId/$tenantId" $mdInfo += "`n*Showing first $maxDisplay of $($result.Issues.Count) records. [View all assignments in Entra Portal]($portalUrl)*`n" } $mdInfo += "`n" } if ($result.DisabledAssignments.Count -gt 0) { $mdInfo += "### ⚠️ Disabled accounts with privileged roles`n`n" $mdInfo += "| Name | Principal name | Type | User type | Account enabled | Status |`n" $mdInfo += "| :----------- | :-- | :--- | :-------- | :-------------- | :----- |`n" $maxDisplay = 5 $displayDisabled = $result.DisabledAssignments if ($result.DisabledAssignments.Count -gt $maxDisplay) { $displayDisabled = $result.DisabledAssignments[0..($maxDisplay - 1)] } foreach ($disabled in $displayDisabled) { $mdInfo += "| $(Get-SafeMarkdown $disabled.DisplayName) | $(Get-SafeMarkdown $disabled.UPN) | $($disabled.Type) | $($disabled.UserType) | $($disabled.AccountEnabled) | Investigate |`n" } if ($result.DisabledAssignments.Count -gt $maxDisplay) { $roleNameEncoded = [System.Uri]::EscapeDataString($result.RoleName) $portalUrl = "https://entra.microsoft.com/#view/Microsoft_Azure_PIMCommon/UserRolesViewModelMenuBlade/~/members/roleObjectId/$($result.TemplateId)/roleId/$($result.TemplateId)/roleTemplateId/$($result.TemplateId)/roleName/$roleNameEncoded/isRoleCustom~/false/resourceScopeId/%2F/resourceId/$tenantId" $mdInfo += "`n*Showing first $maxDisplay of $($result.DisabledAssignments.Count) records. [View all assignments in Entra Portal]($portalUrl)*`n" } $mdInfo += "`n" } if ($result.ValidAssignments.Count -gt 0) { $mdInfo += "### ✅ Valid Member User assignments`n`n" $mdInfo += "| Name | Principal name | Type | User type | Account enabled | Status |`n" $mdInfo += "| :----------- | :-- | :--- | :-------- | :-------------- | :----- |`n" $maxDisplay = 5 $displayValid = $result.ValidAssignments if ($result.ValidAssignments.Count -gt $maxDisplay) { $displayValid = $result.ValidAssignments[0..($maxDisplay - 1)] } foreach ($valid in $displayValid) { $mdInfo += "| $(Get-SafeMarkdown $valid.DisplayName) | $(Get-SafeMarkdown $valid.UPN) | $($valid.Type) | $($valid.UserType) | $($valid.AccountEnabled) | Valid |`n" } if ($result.ValidAssignments.Count -gt $maxDisplay) { $roleNameEncoded = [System.Uri]::EscapeDataString($result.RoleName) $portalUrl = "https://entra.microsoft.com/#view/Microsoft_Azure_PIMCommon/UserRolesViewModelMenuBlade/~/members/roleObjectId/$($result.TemplateId)/roleId/$($result.TemplateId)/roleTemplateId/$($result.TemplateId)/roleName/$roleNameEncoded/isRoleCustom~/false/resourceScopeId/%2F/resourceId/$tenantId" $mdInfo += "`n*Showing first $maxDisplay of $($result.ValidAssignments.Count) records. [View all assignments in Entra Portal]($portalUrl)*`n" } $mdInfo += "`n" } } $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '25383' Title = 'Global and GSA admin privileges are tightly limited to prevent tenant-wide compromise' Status = $passed Result = $testResultMarkdown } if ($customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |