Public/Get-PSUAzAccountAccessInSubscriptions.ps1

function Get-PSUAzAccountAccessInSubscriptions {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]  [string]   $UserPrincipalName,
        [Parameter(Mandatory = $false)] [string]   $OutputCsv           = "C:\Temp\account-access-by-subscription.csv",
        [Parameter(Mandatory = $false)] [switch]   $OutputJson,
        [Parameter(Mandatory = $false)] [int]      $JsonDepth           = 6,
        [Parameter(Mandatory = $false)] [string[]] $SubscriptionFilter  = @('*Non-Prod*')
    )

    # 1) Graph lookups
    $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
    $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 @()
    }

    # 3) Collect assignments & normalize principal IDs
    $assignmentsBySub    = @{}
    $unknownPrincipalIds = [System.Collections.Generic.HashSet[string]]::new()

    foreach ($s in $filteredSubs) {
        $subScope = "/subscriptions/$($s.Id)"
        try {
            $assigns = Get-AzRoleAssignment -Scope $subScope -ErrorAction Stop
        } catch {
            Write-Host "Get-AzRoleAssignment failed for $($s.Id): $_"
            $assigns = @()
        }

        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 to $a
            $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
        }

        $matched = $assigns | Where-Object { $_.PSU_PrincipalId -and $principalIdsToCheck -contains $_.PSU_PrincipalId }
        $assignmentsBySub[$s.Id] = $matched

        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)
            }
        }
    }

    # 4) Resolve principals
    $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 output
    $rows = foreach ($s in $filteredSubs) {
        $matchedAssignments = $assignmentsBySub[$s.Id] | ForEach-Object {
            $roleDef = Get-AzRoleDefinition -Id $_.RoleDefinitionId -ErrorAction SilentlyContinue
            [PSCustomObject]@{
                PrincipalId          = $_.PSU_PrincipalId
                PrincipalDisplayName = $resolvedCache[$_.PSU_PrincipalId].DisplayName
                PrincipalType        = $resolvedCache[$_.PSU_PrincipalId].Type
                RoleDefinitionName   = $_.RoleDefinitionName
                RoleDefinitionId     = $_.RoleDefinitionId
                RoleDefinition       = $roleDef
                Scope                = $_.Scope
                ScopeLevel           = if ($_.Scope -match '^/subscriptions/[^/]+$') { 'Subscription' }
                                        elseif ($_.Scope -match '^/subscriptions/[^/]+/resourceGroups/[^/]+$') { 'ResourceGroup' }
                                        else { 'Resource' }
                ResourceGroup        = if ($_.Scope -match '/resourceGroups/([^/]+)') { $matches[1] } else { '' }
                ResourceType         = if ($_.Scope -match 'providers/([^/]+/[^/]+)$') { $matches[1] } else { '' }
                ResourceName         = if ($_.Scope -match 'providers/[^/]+/[^/]+/(.+)$') { $matches[1] } else { '' }
                AssignmentObjectId   = if ($_.PSU_AssignmentId) { $_.PSU_AssignmentId } 
                                        elseif ($_.RoleAssignmentId) { $_.RoleAssignmentId } 
                                        elseif ($_.Id) { $_.Id } 
                                        else { $null }

                Condition            = $_.Condition
                Description          = $_.Description
            }
        }

        [PSCustomObject]@{
            SubscriptionId            = $s.Id
            SubscriptionName          = $s.Name
            MatchedAssignmentsCount   = $matchedAssignments.Count
            MatchedAssignmentsJson    = ($matchedAssignments | ConvertTo-Json -Depth $JsonDepth -Compress)
        }
    }

    # 6) Export
    try {
        $rows | Select-Object SubscriptionId, SubscriptionName, MatchedAssignmentsCount, MatchedAssignmentsJson |
            Export-Csv -Path $OutputCsv -NoTypeInformation -Force -Encoding UTF8
    } catch {
        Write-Warning "Export CSV failed: $_"
    }

    if ($OutputJson) {
        $full = @{ GeneratedAt = (Get-Date).ToString("o"); User = $UserPrincipalName; Results = $rows }
        $jsonPath = [System.IO.Path]::ChangeExtension($OutputCsv, '.full.json')
        try {
            $full | ConvertTo-Json -Depth ($JsonDepth + 2) | Out-File -FilePath $jsonPath -Encoding UTF8
            Write-Output "JSON: $(Resolve-Path $jsonPath)"
        } catch {
            Write-Warning "Failed to write JSON: $_"
        }
    }

    return $rows
}