tests/Test-Assessment.21816.ps1
|
<# .SYNOPSIS All Microsoft Entra privileged role assignments are managed with PIM #> function Test-Assessment-21816 { [ZtTest( Category = 'Identity', ImplementationCost = 'Medium', Pillar = 'Identity', RiskLevel = 'High', SfiPillar = 'Protect identities and secrets', TenantType = ('Workforce'), TestId = 21816, Title = 'All Microsoft Entra privileged role assignments are managed with PIM', UserImpact = 'Low' )] [CmdletBinding()] param() Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose if( -not (Get-ZtLicense EntraIDP2) ) { Add-ZtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 return } #region Data Collection $activity = 'Checking Microsoft Entra privileged role assignments are managed with PIM' Write-ZtProgress -Activity $activity $globalAdminRoleId = '62e90394-69f5-4237-9190-012177145e10' $permanentGAUserList = @() $permanentGAGroupList = @() $nonPIMPrivilegedUsers = @() $nonPIMPrivilegedGroups = @() # Query 1: Find all privileged directory roles Write-ZtProgress -Activity $activity -Status 'Getting privileged directory roles' $privilegedRoles = Invoke-ZtGraphRequest -RelativeUri 'roleManagement/directory/roleDefinitions' -Filter 'isPrivileged eq true' -ApiVersion beta Write-PSFMessage "Found $($privilegedRoles.Count) privileged roles" -Level Verbose # Query 2: Check for eligible Global Administrators (PIM usage confirmation) Write-ZtProgress -Activity $activity -Status 'Checking eligible Global Administrators' $eligibleGAs = Invoke-ZtGraphRequest -RelativeUri 'roleManagement/directory/roleEligibilitySchedules' -Filter "roleDefinitionId eq '$globalAdminRoleId'" -ApiVersion beta Write-PSFMessage "Found $($eligibleGAs.Count) eligible GA assignments" -Level Verbose $eligibleGAUsers = 0 foreach ($eligibleGA in $eligibleGAs) { # Get principal information separately $principal = Invoke-ZtGraphRequest -RelativeUri "directoryObjects/$($eligibleGA.principalId)" -ApiVersion beta if ($principal.'@odata.type' -eq '#microsoft.graph.user') { $eligibleGAUsers++ } elseif ($principal.'@odata.type' -eq '#microsoft.graph.group') { # Get group members for eligible GA groups $groupMembers = Invoke-ZtGraphRequest -RelativeUri "groups/$($principal.id)/members" -Select 'userPrincipalName,displayName,id' -ApiVersion beta $eligibleGAUsers += $groupMembers.Count } } # Process each privileged role (excluding Global Administrator for now) Write-ZtProgress -Activity $activity -Status 'Checking privileged role assignments' foreach ($role in $privilegedRoles) { if ($role.templateId -eq $globalAdminRoleId) { continue } # Skip GA, handle separately Write-PSFMessage "Processing role: $($role.displayName)" -Level Verbose # Find directory role instance $directoryRole = Invoke-ZtGraphRequest -RelativeUri 'directoryRoles' -Filter "roleTemplateId eq '$($role.templateId)'" -ApiVersion beta if ($directoryRole) { Write-PSFMessage "Found directory role instance for $($role.displayName)" -Level Verbose # Get members of this role $roleMembers = Invoke-ZtGraphRequest -RelativeUri "directoryRoles/$($directoryRole.id)/members" -Select 'userPrincipalName,displayName,id' -ApiVersion beta Write-PSFMessage "Found $($roleMembers.Count) members in role $($role.displayName)" -Level Verbose foreach ($member in $roleMembers) { # Check if assignment is managed by PIM $pimAssignment = Invoke-ZtGraphRequest -RelativeUri 'roleManagement/directory/roleAssignmentScheduleInstances' -Filter "principalId eq '$($member.id)' and roleDefinitionId eq '$($role.templateId)'" -ApiVersion beta Write-PSFMessage "PIM assignment check for $($member.displayName): Found=$($pimAssignment.Count) results" -Level Verbose if (-not $pimAssignment -or ($pimAssignment.assignmentType -eq 'Assigned' -and $null -eq $pimAssignment.endDateTime)) { # Not managed by PIM or permanent assignment $memberInfo = [PSCustomObject]@{ displayName = $member.displayName userPrincipalName = $member.userPrincipalName id = $member.id roleTemplateId = $role.templateId roleDefinitionId = $role.id roleName = $role.displayName isPrivileged = $true assignmentType = if ($pimAssignment) { $pimAssignment.assignmentType } else { 'Not in PIM' } } if ($member.'@odata.type' -eq '#microsoft.graph.user') { $nonPIMPrivilegedUsers += $memberInfo } else { $nonPIMPrivilegedGroups += $memberInfo } } } } } # Query 3: Handle Global Administrator role separately Write-ZtProgress -Activity $activity -Status 'Checking Global Administrator assignments' $gaDirectoryRole = Invoke-ZtGraphRequest -RelativeUri 'directoryRoles' -Filter "roleTemplateId eq '$globalAdminRoleId'" -ApiVersion beta if ($gaDirectoryRole) { $gaMembers = Invoke-ZtGraphRequest -RelativeUri "directoryRoles/$($gaDirectoryRole.id)/members" -Select 'userPrincipalName,displayName,id' -ApiVersion beta foreach ($member in $gaMembers) { # Check if GA assignment is managed by PIM $pimAssignment = Invoke-ZtGraphRequest -RelativeUri 'roleManagement/directory/roleAssignmentScheduleInstances' -Filter "principalId eq '$($member.id)' and roleDefinitionId eq '$globalAdminRoleId'" -ApiVersion beta if (-not $pimAssignment -or ($pimAssignment.assignmentType -eq 'Assigned' -and $null -eq $pimAssignment.endDateTime)) { # Permanent GA assignment found $memberInfo = [PSCustomObject]@{ displayName = $member.displayName userPrincipalName = $member.userPrincipalName id = $member.id roleTemplateId = $globalAdminRoleId roleDefinitionId = $gaDirectoryRole.id roleName = 'Global Administrator' isPrivileged = $true assignmentType = if ($pimAssignment) { $pimAssignment.assignmentType } else { 'Not in PIM' } } if ($member.'@odata.type' -eq '#microsoft.graph.user') { $permanentGAUserList += $memberInfo } elseif ($member.'@odata.type' -eq '#microsoft.graph.group') { $permanentGAGroupList += $memberInfo # Get group members - only users $groupMembers = Invoke-ZtGraphRequest -RelativeUri "groups/$($member.id)/members" -Select 'userPrincipalName,displayName,id,onPremisesSyncEnabled' -ApiVersion beta foreach ($groupMember in $groupMembers) { # Only process users, skip service principals if ($groupMember.'@odata.type' -eq '#microsoft.graph.user') { $groupMemberInfo = [PSCustomObject]@{ displayName = $groupMember.displayName userPrincipalName = $groupMember.userPrincipalName id = $groupMember.id roleTemplateId = $globalAdminRoleId roleDefinitionId = $gaDirectoryRole.id roleName = 'Global Administrator (via group)' isPrivileged = $true assignmentType = 'Via Group' onPremisesSyncEnabled = $groupMember.onPremisesSyncEnabled } $permanentGAUserList += $groupMemberInfo } } } } } } #endregion Data Collection #region Assessment Logic Write-PSFMessage "Assessment data: EligibleGAUsers=$eligibleGAUsers, NonPIMPrivileged=$($nonPIMPrivilegedUsers.Count + $nonPIMPrivilegedGroups.Count), PermanentGA=$($permanentGAUserList.Count)" -Level Verbose $hasPIMUsage = $eligibleGAUsers -gt 0 $hasNonPIMPrivileged = ($nonPIMPrivilegedUsers.Count + $nonPIMPrivilegedGroups.Count) -gt 0 $permanentGACount = $permanentGAUserList.Count if (-not $hasPIMUsage) { $passed = $false $testResultMarkdown = 'No eligible Global Administrator assignments found. PIM usage cannot be confirmed.' } elseif ($hasNonPIMPrivileged) { $passed = $false $testResultMarkdown = 'Found Microsoft Entra privileged role assignments that are not managed with PIM.' } elseif ($permanentGACount -gt 2) { $passed = $false $customStatus = 'Investigate' $testResultMarkdown = 'Three or more accounts are permanently assigned the Global Administrator role. Review to determine whether these are emergency access accounts.' } else { $passed = $true $testResultMarkdown = 'All Microsoft Entra privileged role assignments are managed with PIM with the exception of up to two standing Global Administrator accounts.' } $testResultMarkdown += "`n`n%TestResult%" #endregion Assessment Logic #region Report Generation $mdInfo = '' # Always show summary information $mdInfo += "`n## Assessment summary`n`n" $mdInfo += "| Metric | Count |`n" $mdInfo += "| :----- | :---- |`n" $mdInfo += "| Privileged roles found | $($privilegedRoles.Count) |`n" $mdInfo += "| Eligible Global Administrators | $($eligibleGAUsers) |`n" $mdInfo += "| Non-PIM privileged users | $($nonPIMPrivilegedUsers.Count) |`n" $mdInfo += "| Non-PIM privileged groups | $($nonPIMPrivilegedGroups.Count) |`n" $mdInfo += "| Permanent Global Administrator users | $($permanentGAUserList.Count) |`n" if ($nonPIMPrivilegedUsers.Count -gt 0 -or $nonPIMPrivilegedGroups.Count -gt 0) { $mdInfo += "`n## Non-PIM managed privileged role assignments`n`n" $mdInfo += "| Display name | User principal name | Role name | Assignment type |`n" $mdInfo += "| :----------- | :------------------ | :-------- | :-------------- |`n" foreach ($user in $nonPIMPrivilegedUsers) { $userLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($user.id)/hidePreviewBanner~/true" $safeDisplayName = Get-SafeMarkdown -Text $user.displayName $displayNameLink = "[$safeDisplayName]($userLink)" $mdInfo += "| $displayNameLink | $($user.userPrincipalName) | $($user.roleName) | $($user.assignmentType) |`n" } foreach ($group in $nonPIMPrivilegedGroups) { $groupLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/RolesAndAdministrators/groupId/$($group.id)/menuId/" $safeDisplayName = Get-SafeMarkdown -Text $group.displayName $displayNameLink = "[$safeDisplayName]($groupLink)" $mdInfo += "| $displayNameLink | N/A (Group) | $($group.roleName) | $($group.assignmentType) |`n" } } if ($permanentGAUserList.Count -gt 0) { $mdInfo += "`n## Permanent Global Administrator assignments`n`n" $mdInfo += "| Display name | User principal name | Assignment type | On-Premises synced |`n" $mdInfo += "| :----------- | :------------------ | :-------------- | :----------------- |`n" foreach ($user in $permanentGAUserList) { $syncStatus = if ($null -ne $user.onPremisesSyncEnabled) { $user.onPremisesSyncEnabled } else { 'N/A' } $userLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($user.id)/hidePreviewBanner~/true" $safeDisplayName = Get-SafeMarkdown -Text $user.displayName $displayNameLink = "[$safeDisplayName]($userLink)" $mdInfo += "| $displayNameLink | $($user.userPrincipalName) | $($user.assignmentType) | $syncStatus |`n" } } $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '21816' Status = $passed Result = $testResultMarkdown } # Only add CustomStatus when it's "Investigate" (more than 2 permanent GAs) if ($permanentGACount -gt 2) { $params.CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params } |