Public/Get-PSUAzAccountAccessInSubscriptions2--wip.ps1
function Get-PSUAzAccountAccessInSubscriptions2 { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $UserPrincipalName, [Parameter(Mandatory = $false)] [string] $OutputCsv = "C:\Temp\account-access-by-resource.csv", [Parameter(Mandatory = $false)] [switch] $OutputJson, [Parameter(Mandatory = $false)] [int] $JsonDepth = 6, [Parameter(Mandatory = $false)] [string[]] $SubscriptionFilter = @('*Non-Prod*') ) # 1) Graph lookups Write-Host "Looking up user and group memberships..." $me = Get-PSUGraphUser -Upn $UserPrincipalName $groups = Get-PSUTransitiveGroups -UserId $me.Id $principalIdsToCheck = @($me.Id) + ($groups | Select-Object -ExpandProperty Id -ErrorAction SilentlyContinue | Where-Object { $_ }) # 2) Subscriptions filter Write-Host "Filtering subscriptions..." $subs = Get-AzSubscription $patterns = if ($SubscriptionFilter -and $SubscriptionFilter.Count -gt 0) { $SubscriptionFilter } else { @('*') } $filteredSubs = $subs | Where-Object { foreach ($p in $patterns) { if ($null -ne $p -and $p -ne '' -and ($_.Name -like $p -or $_.Id -like $p)) { return $true } } return $false } if (-not $filteredSubs -or $filteredSubs.Count -eq 0) { Write-Warning "No subscriptions matched filter" return @() } Write-Host "Processing $($filteredSubs.Count) subscription(s)..." # 3) Collect all role assignments across all scopes $allAssignments = @() $unknownPrincipalIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($s in $filteredSubs) { Write-Host "Processing subscription: $($s.Name)" # Set subscription context try { Set-AzContext -SubscriptionId $s.Id -ErrorAction Stop | Out-Null } catch { Write-Warning "Failed to set context for subscription $($s.Id): $_" continue } # Get ALL role assignments in the subscription (all scopes) try { $assigns = Get-AzRoleAssignment -ErrorAction Stop } catch { Write-Host "Get-AzRoleAssignment failed for $($s.Id): $_" continue } foreach ($a in $assigns) { # Try helper first $norm = Get-PSUAssignmentPrincipalId -Assignment $a # Fallback if PrincipalId missing but ObjectId exists if (-not $norm.PrincipalId -and $a.ObjectId) { $norm = [PSCustomObject]@{ PrincipalId = $a.ObjectId PrincipalType = $a.ObjectType AssignmentId = if ($a.Id) { $a.Id } else { $a.RoleAssignmentName } } } # Add normalized props and subscription info $a | Add-Member -NotePropertyName PSU_PrincipalId -NotePropertyValue $norm.PrincipalId -Force $a | Add-Member -NotePropertyName PSU_PrincipalType -NotePropertyValue $norm.PrincipalType -Force $a | Add-Member -NotePropertyName PSU_AssignmentId -NotePropertyValue $norm.AssignmentId -Force $a | Add-Member -NotePropertyName PSU_SubscriptionId -NotePropertyValue $s.Id -Force $a | Add-Member -NotePropertyName PSU_SubscriptionName -NotePropertyValue $s.Name -Force } # Filter for assignments that match our user/groups $matched = $assigns | Where-Object { $_.PSU_PrincipalId -and $principalIdsToCheck -contains $_.PSU_PrincipalId } $allAssignments += $matched # Track unknown principals for resolution foreach ($a in $matched) { $PSU_PrincipalId = $a.PSU_PrincipalId if ($PSU_PrincipalId -and $PSU_PrincipalId -ne $me.Id -and -not ($groups | Where-Object { $_.Id -eq $PSU_PrincipalId })) { $null = $unknownPrincipalIds.Add($PSU_PrincipalId) } } } Write-Host "Found $($allAssignments.Count) matching role assignments" # 4) Resolve principals Write-Host "Resolving principal names..." $resolvedCache = @{} $resolvedCache[$me.Id] = @{ DisplayName = $me.DisplayName; Type = 'User' } foreach ($g in $groups) { if ($g.Id) { $resolvedCache[$g.Id] = @{ DisplayName = $g.DisplayName; Type = 'Group' } } } if ($unknownPrincipalIds.Count -gt 0) { $objs = Get-PSUBatchDirectoryObjects -Ids $unknownPrincipalIds foreach ($o in $objs) { $odata = $o.'@odata.type' -or ($o.AdditionalProperties.'@odata.type' -as [string]) $display = $o.displayName -or $o.AdditionalProperties.displayName -or $o.userPrincipalName -or $o.mail $type = if ($odata -like '*group*') { 'Group' } elseif ($odata -like '*user*') { 'User' } else { 'DirectoryObject' } $resolvedCache[$o.id] = @{ DisplayName = $display; Type = $type } } } # 5) Build detailed output with resource-level information Write-Host "Building detailed resource access report..." $rows = foreach ($assignment in $allAssignments) { # Parse scope to extract resource information $scope = $assignment.Scope $subscriptionId = $assignment.PSU_SubscriptionId $subscriptionName = $assignment.PSU_SubscriptionName # Initialize resource details $resourceGroup = "" $resourceType = "" $azResourceName = "" $scopeLevel = "" # Parse the scope to extract resource information if ($scope -match '^/subscriptions/[^/]+$') { $scopeLevel = 'Subscription' $azResourceName = $subscriptionName $resourceType = 'Subscription' } elseif ($scope -match '^/subscriptions/[^/]+/resourceGroups/([^/]+)$') { $scopeLevel = 'ResourceGroup' $resourceGroup = $matches[1] $azResourceName = $resourceGroup $resourceType = 'ResourceGroup' } elseif ($scope -match '^/subscriptions/[^/]+/resourceGroups/([^/]+)/providers/([^/]+/[^/]+)/?(.*)$') { $scopeLevel = 'Resource' $resourceGroup = $matches[1] $resourceType = $matches[2] $resourcePath = $matches[3] # Extract the final resource name from the path if ($resourcePath) { $pathParts = $resourcePath -split '/' $azResourceName = $pathParts[-1] # Take the last part as the resource name } else { $azResourceName = "Unknown Resource" } } else { $scopeLevel = 'Other' $azResourceName = $scope $resourceType = 'Unknown' } # Get role definition for access type mapping $roleDef = Get-AzRoleDefinition -Id $assignment.RoleDefinitionId -ErrorAction SilentlyContinue # Map role to access type (simplified categorization) $typeOfAccess = switch -Wildcard ($assignment.RoleDefinitionName) { "*Owner*" { "Owner" } "*Contributor*" { "Contributor" } "*Reader*" { "Reader" } "*Writer*" { "Write" } "*Admin*" { "Admin" } "*Viewer*" { "Read" } "*Manager*" { "Manage" } "*Operator*" { "Operate" } "*Developer*" { "Develop" } "*User Access*" { "User Access Management" } default { $assignment.RoleDefinitionName } } # Create output row [PSCustomObject]@{ UserPrincipalName = $UserPrincipalName AzResourceName = $azResourceName ResourceGroup = $resourceGroup ResourceType = $resourceType TypeOfAccess = $typeOfAccess RoleDefinitionName = $assignment.RoleDefinitionName ScopeLevel = $scopeLevel Scope = $scope SubscriptionName = $subscriptionName SubscriptionId = $subscriptionId PrincipalDisplayName = $resolvedCache[$assignment.PSU_PrincipalId].DisplayName PrincipalType = $resolvedCache[$assignment.PSU_PrincipalId].Type PrincipalId = $assignment.PSU_PrincipalId AssignmentId = $assignment.PSU_AssignmentId Condition = $assignment.Condition Description = $assignment.Description } } # 6) Export results Write-Host "Exporting results..." try { # Export with the requested columns first, then additional details $rows | Select-Object UserPrincipalName, AzResourceName, ResourceGroup, ResourceType, TypeOfAccess, RoleDefinitionName, ScopeLevel, Scope, SubscriptionName, SubscriptionId, PrincipalDisplayName, PrincipalType, PrincipalId, AssignmentId, Condition, Description | Export-Csv -Path $OutputCsv -NoTypeInformation -Force -Encoding UTF8 Write-Host "CSV exported to: $OutputCsv" } catch { Write-Warning "Export CSV failed: $_" } if ($OutputJson) { $full = @{ GeneratedAt = (Get-Date).ToString("o") User = $UserPrincipalName TotalAssignments = $rows.Count Results = $rows } $jsonPath = [System.IO.Path]::ChangeExtension($OutputCsv, '.full.json') try { $full | ConvertTo-Json -Depth ($JsonDepth + 2) | Out-File -FilePath $jsonPath -Encoding UTF8 Write-Host "JSON exported to: $jsonPath" } catch { Write-Warning "Failed to write JSON: $_" } } Write-Host "Processing complete. Found $($rows.Count) resource access assignments." return $rows } |