Public/PrivilegedAccess/Get-EntraOpsPrivilegedAzureRoles.ps1

<#
.SYNOPSIS
    Get a list of Azure RBAC role assignments (Azure Resource Manager) including Azure PIM.

.DESCRIPTION
    Get a list of Azure RBAC role assignments across the Azure Resource Manager (ARM) hierarchy.
    By default the collection starts at the ARM tenant root scope ("/") so that role assignments
    made directly at the tenant level are included, then expands to all descendant management
    groups and subscriptions to cover the entire tenant.

    The cmdlet uses the optimized ARM query wrapper (Invoke-EntraOpsAzQuery) to batch requests across
    all scopes and supports the same privileged-access concepts as the other EntraOps Roles cmdlets:
    - Azure PIM (eligible, activated and time-bounded active assignments)
    - Custom roles (RoleType BuiltInRole / CustomRole)
    - Constrained delegations (role assignment ABAC conditions returned in RoleAssignmentCondition /
      RoleAssignmentConditionVersion, or delegated managed identity)
    - Nested groups and PIM for Groups (transitive expansion of group assignments)

.PARAMETER TenantId
    Tenant ID of the Microsoft Entra ID tenant. Default is the current tenant ID.

.PARAMETER Scope
    ARM scope to collect role assignments from (e.g. a management group, subscription, resource group
    or resource). Default is "/" (ARM tenant root scope), which captures tenant-level role assignments
    and, when ExpandDescendantScopes is $true, all management groups and subscriptions beneath the
    Tenant Root Group.

.PARAMETER ExpandDescendantScopes
    When the scope is a management group, expand to all descendant management groups and subscriptions
    to cover the entire sub-tree (tenant-wide by default). Default is $true.

.PARAMETER ControlPlaneScopeFilter
    Optional array of ARM scope patterns (e.g. Control Plane RoleAssignmentScopeName hierarchies from
    Classification_Azure.json) used to restrict the collected scopes. After the scope sub-tree is resolved,
    only scopes that equal, are an ancestor of, or are a descendant of any of the provided patterns are
    queried. Wildcard patterns ("/" and "/*") are ignored because they would not constrain the scan.
    Default is an empty array (no filtering; full sub-tree is scanned).

.PARAMETER PrincipalTypeFilter
    Filter for principal type. Default is User, Group, ServicePrincipal, ForeignGroup, AgentUser and
    AgentServicePrincipal. ForeignGroup and agent types are normalized by the downstream object resolver.

.PARAMETER ExpandGroupMembers
    Expand group members for transitive role assignments (covers nested groups and PIM for Groups).
    Default is $true.

.PARAMETER SampleMode
    Use sample data for testing or offline mode. Default is $False.

.EXAMPLE
    Get all Azure RBAC role assignments across the entire tenant (Tenant Root Group and below).
    Get-EntraOpsPrivilegedAzureRoles

.EXAMPLE
    Get Azure RBAC role assignments of a single subscription only.
    Get-EntraOpsPrivilegedAzureRoles -Scope "/subscriptions/00000000-0000-0000-0000-000000000000" -ExpandDescendantScopes $false

.EXAMPLE
    Get Azure RBAC role assignments of a management group and all its descendant scopes.
    Get-EntraOpsPrivilegedAzureRoles -Scope "/providers/Microsoft.Management/managementGroups/Contoso-Platform"
#>


function Get-EntraOpsPrivilegedAzureRoles {
    param (
        [Parameter(Mandatory = $False)]
        [System.String]$TenantId = (Get-EntraOpsAzContextValue -Property TenantId)
        ,
        [Parameter(Mandatory = $False)]
        [System.String]$Scope
        ,
        [Parameter(Mandatory = $False)]
        [System.Boolean]$ExpandDescendantScopes = $true
        ,
        [Parameter(Mandatory = $False)]
        [Array]$ControlPlaneScopeFilter = @()
        ,
        [Parameter(Mandatory = $False)]
        [ValidateSet("User", "Group", "ServicePrincipal", "ForeignGroup", "AgentUser", "AgentServicePrincipal")]
        [Array]$PrincipalTypeFilter = ("User", "Group", "ServicePrincipal", "ForeignGroup", "AgentUser", "AgentServicePrincipal")
        ,
        [Parameter(Mandatory = $False)]
        [System.Boolean]$ExpandGroupMembers = $true
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$SampleMode = $False
        ,
        [Parameter(Mandatory = $False)]
        [System.Collections.Generic.List[psobject]]$WarningMessages
    )

    # Set Error Action
    $ErrorActionPreference = "Stop"

    # Default to the ARM tenant root scope so tenant-level role assignments are included
    if ([string]::IsNullOrEmpty($Scope)) {
        $Scope = "/"
    }

    # ARM API versions
    $PimApiVersion = "2020-10-01"
    $RbacApiVersion = "2022-04-01"
    $MgmtApiVersion = "2020-05-01"

    $AzureRbacAssignments = @()

    if ($SampleMode -eq $True) {
        Write-Warning "Not supported yet!"
        return
    }

    #region Resolve the list of scopes to query (entire sub-tree for management groups)
    Write-Host "Get Azure RBAC role assignments starting at scope '$Scope'..."
    $Scopes = [System.Collections.Generic.List[string]]::new()
    $Scopes.Add($Scope)

    # When the scope is "/", expand descendants from the Tenant Root Group MG so all management
    # groups and subscriptions are covered in addition to the tenant-level "/" scope itself.
    if ($ExpandDescendantScopes -eq $True -and $Scope -eq "/") {
        $ManagementGroupName = $TenantId
    } elseif ($ExpandDescendantScopes -eq $True -and $Scope -like "*/providers/Microsoft.Management/managementGroups/*") {
        $ManagementGroupName = ($Scope -split '/managementGroups/')[-1]
    } else {
        $ManagementGroupName = $null
    }

    if ($ExpandDescendantScopes -eq $True -and $null -ne $ManagementGroupName) {
        try {
            $Descendants = Invoke-EntraOpsAzQuery -Uri "/providers/Microsoft.Management/managementGroups/$($ManagementGroupName)/descendants" -ApiVersion $MgmtApiVersion
            foreach ($Descendant in $Descendants) {
                if ($Descendant.type -eq "Microsoft.Management/managementGroups") {
                    $Scopes.Add("/providers/Microsoft.Management/managementGroups/$($Descendant.name)")
                } elseif ($Descendant.type -like "*subscriptions") {
                    $Scopes.Add("/subscriptions/$($Descendant.name)")
                }
            }
        } catch {
            $WarningMessage = "Could not expand descendant scopes of management group $($ManagementGroupName): $($_.Exception.Message)"
            if ($null -ne $WarningMessages) {
                $WarningMessages.Add([pscustomobject]@{ Timestamp = (Get-Date); Type = "ScopeExpansion"; ObjectId = $ManagementGroupName; Message = $WarningMessage })
            }
            Write-Warning $WarningMessage
        }
        # Ensure the Tenant Root MG itself is also in the list when starting from "/"
        if ($Scope -eq "/") {
            $Scopes.Add("/providers/Microsoft.Management/managementGroups/$ManagementGroupName")
        }
    }
    # Case-insensitive dedup: ARM returns management group and subscription ids with inconsistent
    # casing, and Select-Object -Unique compares case-sensitively - the same scope could otherwise be
    # queried twice, duplicating every role definition and assignment request made against it.
    $Scopes = @(
        $Scopes |
            Group-Object -Property { "$_".ToLowerInvariant() } |
            ForEach-Object { $_.Group[0] }
    )

    # Restrict scan to Control Plane hierarchies when a scope filter is provided.
    # A scope is kept when it equals, is an ancestor of, or is a descendant of any
    # Control Plane RoleAssignmentScopeName pattern (matching the ARM hierarchy).
    $ScopeFilterPatterns = @($ControlPlaneScopeFilter | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -ne "/" -and $_ -ne "/*" } | Select-Object -Unique)
    if ($ScopeFilterPatterns.Count -gt 0) {
        $UnfilteredScopeCount = @($Scopes).Count
        $Scopes = @($Scopes | Where-Object {
                # Management groups are always retained. Ancestor matching below is string-prefix based, and a
                # management group scope (/providers/Microsoft.Management/managementGroups/<name>) is never a
                # string prefix of /subscriptions/<id>. Filtering them out means the ancestor MG is never
                # queried, so an assignment that is "Direct" only at that MG survives solely as an "Inherited"
                # instance at the subscription - which is skipped below - and disappears entirely. Management
                # groups number in the tens, so retaining all of them is cheap.
                if ($_ -like "/providers/Microsoft.Management/managementGroups/*") { return $true }
                $ScopeItem = $_.TrimEnd('/')
                $Keep = $false
                foreach ($Pattern in $ScopeFilterPatterns) {
                    $PatternTrimmed = $Pattern.TrimEnd('/')
                    if ($ScopeItem -ieq $PatternTrimmed -or $ScopeItem -like $Pattern -or $PatternTrimmed -like "$ScopeItem/*" -or $ScopeItem -like "$PatternTrimmed/*") {
                        $Keep = $true
                        break
                    }
                }
                $Keep
            })
        Write-Host "Control Plane scope filter applied: $(@($Scopes).Count) of $UnfilteredScopeCount scope(s) retained."
        if (@($Scopes).Count -eq 0) {
            $WarningMessage = "Control Plane scope filter matched no scopes; no Azure RBAC assignments will be collected."
            if ($null -ne $WarningMessages) {
                $WarningMessages.Add([pscustomobject]@{ Timestamp = (Get-Date); Type = "ScopeFilter"; ObjectId = ""; Message = $WarningMessage })
            }
            Write-Warning $WarningMessage
        }
    }

    Write-Host "Collecting Azure RBAC assignments across $($Scopes.Count) scope(s)..."
    #endregion

    #region Get role definitions to determine custom roles and privileged classification
    # Fetch built-ins once at the starting scope, then only custom roles at every collected scope.
    # Custom-role visibility depends on assignable scopes, so those queries cannot be collapsed safely.
    $RoleDefinitionRequests = [System.Collections.Generic.List[string]]::new()
    if ($Scopes.Count -gt 0) {
        $RoleDefinitionRequests.Add("$($Scopes[0].TrimEnd('/'))/providers/Microsoft.Authorization/roleDefinitions?api-version=$($RbacApiVersion)")
    }
    foreach ($ScopeItem in $Scopes) {
        $RoleDefinitionRequests.Add("$($ScopeItem.TrimEnd('/'))/providers/Microsoft.Authorization/roleDefinitions?api-version=$($RbacApiVersion)&`$filter=type%20eq%20%27CustomRole%27")
    }
    $RoleDefinitions = Invoke-EntraOpsAzQuery -BatchRequest $RoleDefinitionRequests

    # Build a lookup keyed by role definition GUID (stable across scope prefixes) with name, type and privileged flag
    $PrivilegedActions = @('*', 'Microsoft.Authorization/*', 'Microsoft.Authorization/roleAssignments/write', 'Microsoft.Authorization/roleDefinitions/write', 'Microsoft.Authorization/elevateAccess/action')
    $RoleDefinitionLookup = @{}
    foreach ($RoleDefinition in $RoleDefinitions) {
        $RoleDefinitionGuid = ($RoleDefinition.name)
        if ([string]::IsNullOrEmpty($RoleDefinitionGuid) -or $RoleDefinitionLookup.ContainsKey($RoleDefinitionGuid)) { continue }

        $IsPrivileged = $false
        foreach ($Permission in @($RoleDefinition.properties.permissions)) {
            foreach ($Action in (@($Permission.actions) + @($Permission.dataActions))) {
                if ($Action -in $PrivilegedActions -or $Action -like 'Microsoft.Authorization/role*/write') {
                    $IsPrivileged = $true
                    break
                }
            }
            if ($IsPrivileged) { break }
        }

        $RoleDefinitionLookup[$RoleDefinitionGuid] = [pscustomobject]@{
            RoleDefinitionName = $RoleDefinition.properties.roleName
            RoleType           = if ($RoleDefinition.properties.type -eq "CustomRole") { "CustomRole" } else { "BuiltInRole" }
            IsPrivileged       = $IsPrivileged
        }
    }
    #endregion

    #region Get active/permanent (roleAssignmentScheduleInstances) and eligible (roleEligibilityScheduleInstances) assignments
    # Both endpoints return expandedProperties (principal, roleDefinition, scope) which avoids extra lookups.
    # The ARM tenant root ("/") does not support PIM schedule instance endpoints; exclude it here and
    # handle it separately below via the plain roleAssignments endpoint.
    $PimScopes = @($Scopes | Where-Object { $_ -ne "/" })
    $AssignmentRequests = foreach ($ScopeItem in $PimScopes) {
        "$($ScopeItem.TrimEnd('/'))/providers/Microsoft.Authorization/roleAssignmentScheduleInstances?api-version=$($PimApiVersion)"
        "$($ScopeItem.TrimEnd('/'))/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=$($PimApiVersion)"
    }
    $AllInstances = Invoke-EntraOpsAzQuery -BatchRequest $AssignmentRequests

    # Collect plain (always-permanent) role assignments at the tenant root "/" scope.
    $RootScopeRoleAssignments = @()
    if ($Scopes -contains "/") {
        try {
            $RootScopeRoleAssignments = @(Invoke-EntraOpsAzQuery -Uri "/providers/Microsoft.Authorization/roleAssignments?api-version=$($RbacApiVersion)&`$filter=atScope()")
        } catch {
            $WarningMessage = "Could not retrieve role assignments at tenant root scope '/': $($_.Exception.Message)"
            if ($null -ne $WarningMessages) {
                $WarningMessages.Add([pscustomobject]@{ Timestamp = (Get-Date); Type = "ScopeQuery"; ObjectId = "/"; Message = $WarningMessage })
            }
            Write-Warning $WarningMessage
        }
    }

    # Keep only directly assigned instances (memberType "Direct"); PIM-expanded group members (memberType
    # "Group") are resolved by our own transitive expansion below. Dedupe by instance id (the same
    # inherited assignment can be returned from multiple child scope queries).
    $SeenAssignmentIds = @{}
    $AzureRbacAssignments = foreach ($Instance in $AllInstances) {
        if ($null -eq $Instance.properties) { continue }
        if ($Instance.properties.memberType -eq "Group" -or $Instance.properties.memberType -eq "Inherited") { continue }
        if ($SeenAssignmentIds.ContainsKey($Instance.id)) { continue }
        $SeenAssignmentIds[$Instance.id] = $true

        $InstanceProperties = $Instance.properties
        $IsEligible = $Instance.type -like "*RoleEligibilityScheduleInstances"

        # Determine PIM assignment type and whether the role is PIM managed
        if ($IsEligible) {
            $PIMAssignmentType = "Eligible"
            $PIMManagedRole = $True
        } elseif ($InstanceProperties.assignmentType -eq "Activated") {
            $PIMAssignmentType = "Activated"
            $PIMManagedRole = $True
        } elseif ($null -ne $InstanceProperties.endDateTime -or $null -ne $InstanceProperties.linkedRoleEligibilityScheduleInstanceId) {
            $PIMAssignmentType = "TimeBounded"
            $PIMManagedRole = $True
        } else {
            $PIMAssignmentType = "Permanent"
            $PIMManagedRole = $False
        }

        # Resolve role definition details (lookup by GUID, fallback to expandedProperties)
        $RoleDefinitionGuid = ($InstanceProperties.roleDefinitionId -split '/')[-1]
        $RoleDefinitionInfo = $RoleDefinitionLookup[$RoleDefinitionGuid]
        if ($null -ne $RoleDefinitionInfo) {
            $RoleDefinitionName = $RoleDefinitionInfo.RoleDefinitionName
            $RoleType = $RoleDefinitionInfo.RoleType
            $RoleIsPrivileged = $RoleDefinitionInfo.IsPrivileged
        } else {
            $RoleDefinitionName = $InstanceProperties.expandedProperties.roleDefinition.displayName
            $RoleType = if ($InstanceProperties.expandedProperties.roleDefinition.type -eq "CustomRole") { "CustomRole" } else { "BuiltInRole" }
            $RoleIsPrivileged = $false
        }

        # Constrained delegation: role assignment conditions (ABAC) or delegated managed identity
        $RoleAssignmentSubType = ""
        $RoleAssignmentCondition = $InstanceProperties.condition
        $RoleAssignmentConditionVersion = $InstanceProperties.conditionVersion
        if (-not [string]::IsNullOrEmpty($RoleAssignmentCondition)) {
            $RoleAssignmentSubType = "Constrained delegation"
        } elseif (-not [string]::IsNullOrEmpty($InstanceProperties.delegatedManagedIdentityResourceId)) {
            $RoleAssignmentSubType = "Delegated Managed Identity"
        }

        $ScopeId = $InstanceProperties.scope
        $ScopeName = if (-not [string]::IsNullOrEmpty($InstanceProperties.expandedProperties.scope.displayName)) {
            $InstanceProperties.expandedProperties.scope.displayName
        } else {
            $ScopeId
        }

        [pscustomobject]@{
            RoleAssignmentId                      = $Instance.name
            RoleAssignmentScopeId                 = $ScopeId
            RoleAssignmentScopeName               = $ScopeName
            RoleAssignmentType                    = "Direct"
            RoleAssignmentSubType                 = $RoleAssignmentSubType
            RoleAssignmentCondition               = $RoleAssignmentCondition
            RoleAssignmentConditionVersion        = $RoleAssignmentConditionVersion
            PIMManagedRole                        = $PIMManagedRole
            PIMAssignmentType                     = $PIMAssignmentType
            RoleDefinitionName                    = $RoleDefinitionName
            RoleDefinitionId                      = $RoleDefinitionGuid
            RoleType                              = $RoleType
            RoleIsPrivileged                      = $RoleIsPrivileged
            ObjectId                              = $InstanceProperties.principalId
            ObjectTenantId                        = $TenantId
            ObjectType                            = ($InstanceProperties.principalType ?? "unknown").toLower()
            TransitiveByObjectId                  = ""
            TransitiveByObjectDisplayName         = ""
            TransitiveByNestingObjectIds          = $null
            TransitiveByNestingObjectDisplayNames = $null
        }
    }

    # Append permanent role assignments from the ARM tenant root "/" scope.
    # The plain roleAssignments API returns a different shape (no expandedProperties / schedule fields).
    foreach ($RootAssignment in $RootScopeRoleAssignments) {
        if ($null -eq $RootAssignment.properties) { continue }
        if ($SeenAssignmentIds.ContainsKey($RootAssignment.id)) { continue }
        $SeenAssignmentIds[$RootAssignment.id] = $true

        $RootProps = $RootAssignment.properties
        $RoleDefinitionGuid = ($RootProps.roleDefinitionId -split '/')[-1]
        $RoleDefinitionInfo = $RoleDefinitionLookup[$RoleDefinitionGuid]
        if ($null -ne $RoleDefinitionInfo) {
            $RoleDefinitionName = $RoleDefinitionInfo.RoleDefinitionName
            $RoleType = $RoleDefinitionInfo.RoleType
            $RoleIsPrivileged = $RoleDefinitionInfo.IsPrivileged
        } else {
            $RoleDefinitionName = $RoleDefinitionGuid
            $RoleType = "BuiltInRole"
            $RoleIsPrivileged = $false
        }

        $RoleAssignmentSubType = ""
        $RoleAssignmentCondition = $RootProps.condition
        $RoleAssignmentConditionVersion = $RootProps.conditionVersion
        if (-not [string]::IsNullOrEmpty($RoleAssignmentCondition)) {
            $RoleAssignmentSubType = "Constrained delegation"
        } elseif (-not [string]::IsNullOrEmpty($RootProps.delegatedManagedIdentityResourceId)) {
            $RoleAssignmentSubType = "Delegated Managed Identity"
        }

        $AzureRbacAssignments += [pscustomobject]@{
            RoleAssignmentId                      = $RootAssignment.name
            RoleAssignmentScopeId                 = "/"
            RoleAssignmentScopeName               = "/"
            RoleAssignmentType                    = "Direct"
            RoleAssignmentSubType                 = $RoleAssignmentSubType
            RoleAssignmentCondition               = $RoleAssignmentCondition
            RoleAssignmentConditionVersion        = $RoleAssignmentConditionVersion
            PIMManagedRole                        = $False
            PIMAssignmentType                     = "Permanent"
            RoleDefinitionName                    = $RoleDefinitionName
            RoleDefinitionId                      = $RoleDefinitionGuid
            RoleType                              = $RoleType
            RoleIsPrivileged                      = $RoleIsPrivileged
            ObjectId                              = $RootProps.principalId
            ObjectTenantId                        = $TenantId
            ObjectType                            = ($RootProps.principalType ?? "unknown").toLower()
            TransitiveByObjectId                  = ""
            TransitiveByObjectDisplayName         = ""
            TransitiveByNestingObjectIds          = $null
            TransitiveByNestingObjectDisplayNames = $null
        }
    }
    #endregion

    #region Collect transitive assignments by group members (nested groups and PIM for Groups)
    $AzureRbacTransitiveAssignments = [System.Collections.Generic.List[object]]::new()
    if ($ExpandGroupMembers -eq $True) {
        Write-Verbose "Expanding groups for direct or transitive Azure RBAC role assignments"
        $GroupsWithRbacAssignment = $AzureRbacAssignments | Where-Object { $_.ObjectType -eq "group" }
        $AllTransitiveMembers = [System.Collections.Generic.List[object]]::new()

        foreach ($GroupWithRbacAssignment in ($GroupsWithRbacAssignment | Sort-Object ObjectId -Unique)) {
            try {
                $GroupObjectDisplayName = (Invoke-EntraOpsMsGraphQuery -Method Get -Uri "https://graph.microsoft.com/beta/groups/$($GroupWithRbacAssignment.ObjectId)" -OutputType PSObject).displayName
                $TransitiveMembers = Get-EntraOpsPrivilegedTransitiveGroupMember -GroupObjectId $($GroupWithRbacAssignment.ObjectId) -TenantId $TenantId -WarningMessages $WarningMessages
            } catch {
                $WarningMessage = "Could not expand group $($GroupWithRbacAssignment.ObjectId): $($_.Exception.Message)"
                if ($null -ne $WarningMessages) {
                    $WarningMessages.Add([pscustomobject]@{ Timestamp = (Get-Date); Type = "GroupExpansion"; ObjectId = $GroupWithRbacAssignment.ObjectId; Message = $WarningMessage })
                }
                Write-Warning $WarningMessage
                continue
            }

            foreach ($TransitiveMember in $TransitiveMembers) {
                $Member = [pscustomobject]@{
                    displayName               = $TransitiveMember.displayName
                    id                        = $TransitiveMember.id
                    '@odata.type'             = $TransitiveMember.'@odata.type'
                    RoleAssignmentSubType     = $TransitiveMember.RoleAssignmentSubType
                    GroupObjectDisplayName    = $GroupObjectDisplayName
                    GroupObjectId             = $GroupWithRbacAssignment.ObjectId
                    NestingObjectIds          = $TransitiveMember.NestingObjectIds
                    NestingObjectDisplayNames = $TransitiveMember.NestingObjectDisplayNames
                }
                $AllTransitiveMembers.Add($Member)
            }
        }

        # Index transitive members by their assigning group for O(1) lookups instead of
        # re-scanning the full member list for every group assignment.
        $TransitiveMembersByGroup = @{}
        foreach ($TransitiveMemberEntry in $AllTransitiveMembers) {
            if (-not $TransitiveMembersByGroup.ContainsKey($TransitiveMemberEntry.GroupObjectId)) {
                $TransitiveMembersByGroup[$TransitiveMemberEntry.GroupObjectId] = [System.Collections.Generic.List[object]]::new()
            }
            $TransitiveMembersByGroup[$TransitiveMemberEntry.GroupObjectId].Add($TransitiveMemberEntry)
        }

        foreach ($RbacAssignmentByGroup in $GroupsWithRbacAssignment) {
            $RbacAssignmentByNestedGroupMembers = $TransitiveMembersByGroup[$RbacAssignmentByGroup.ObjectId]

            if ($null -ne $RbacAssignmentByNestedGroupMembers -and $RbacAssignmentByNestedGroupMembers.Count -gt 0) {
                $RbacAssignmentByNestedGroupMembers | ForEach-Object {
                    $TransitiveAssignment = [pscustomobject]@{
                        RoleAssignmentId                      = $RbacAssignmentByGroup.RoleAssignmentId
                        RoleAssignmentScopeId                 = $RbacAssignmentByGroup.RoleAssignmentScopeId
                        RoleAssignmentScopeName               = $RbacAssignmentByGroup.RoleAssignmentScopeName
                        RoleAssignmentType                    = "Transitive"
                        RoleAssignmentSubType                 = $_.RoleAssignmentSubType
                        RoleAssignmentCondition               = $RbacAssignmentByGroup.RoleAssignmentCondition
                        RoleAssignmentConditionVersion        = $RbacAssignmentByGroup.RoleAssignmentConditionVersion
                        PIMManagedRole                        = $RbacAssignmentByGroup.PIMManagedRole
                        PIMAssignmentType                     = $RbacAssignmentByGroup.PIMAssignmentType
                        RoleDefinitionName                    = $RbacAssignmentByGroup.RoleDefinitionName
                        RoleDefinitionId                      = $RbacAssignmentByGroup.RoleDefinitionId
                        RoleType                              = $RbacAssignmentByGroup.RoleType
                        RoleIsPrivileged                      = $RbacAssignmentByGroup.RoleIsPrivileged
                        ObjectId                              = $_.id
                        ObjectTenantId                        = $TenantId
                        ObjectType                            = $_.'@odata.type'.Replace('#microsoft.graph.', '').toLower()
                        TransitiveByObjectId                  = $RbacAssignmentByGroup.ObjectId
                        TransitiveByObjectDisplayName         = $_.GroupObjectDisplayName
                        TransitiveByNestingObjectIds          = $_.NestingObjectIds
                        TransitiveByNestingObjectDisplayNames = $_.NestingObjectDisplayNames
                    }
                    $AzureRbacTransitiveAssignments.Add($TransitiveAssignment) | Out-Null
                }
            } else {
                if ($null -ne $WarningMessages) {
                    $WarningMessages.Add([pscustomobject]@{ Timestamp = (Get-Date); Type = "EmptyGroup"; ObjectId = $RbacAssignmentByGroup.ObjectId; Message = "Group has no members or members could not be resolved." })
                }
            }
        }
    }
    #endregion

    $AllAzureRbacAssignments = @()
    $AllAzureRbacAssignments += $AzureRbacAssignments
    $AllAzureRbacAssignments += $AzureRbacTransitiveAssignments
    $AllAzureRbacAssignments = $AllAzureRbacAssignments | Where-Object { $_.ObjectType -in $PrincipalTypeFilter }

    # Efficient deduplication using composite key (assignment, principal, assignment type)
    $DeduplicationHash = @{}
    $UniqueAssignments = foreach ($Assignment in $AllAzureRbacAssignments) {
        $Key = "$($Assignment.RoleAssignmentId)|$($Assignment.ObjectId)|$($Assignment.RoleAssignmentType)"
        if (-not $DeduplicationHash.ContainsKey($Key)) {
            $DeduplicationHash[$Key] = $true
            $Assignment
        }
    }

    $UniqueAssignments | Sort-Object RoleAssignmentId, RoleAssignmentType, ObjectId
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBRqcXMgaFxvPvB
# mR2JF1eNlztlYnimNgCLxZAXaCqhD6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
# ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
# dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l
# dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG
# A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl
# BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb
# Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC
# 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va
# qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ
# 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet
# /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe
# GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV
# WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl
# s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb
# MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc
# noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl
# jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA
# AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8
# +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud
# DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w
# bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v
# c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np
# dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG
# CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM
# BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1
# BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR
# mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl
# lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs
# NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv
# n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggZrMIIEU6ADAgECAhBX5gfP8LabvnxC
# XoItEBa/MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB
# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgMjAyMSBDQTAeFw0yNjEwMDYwODI4MjNaFw0yNzEwMDYwODI4MjJaMIGQ
# MQswCQYDVQQGEwJERTEdMBsGA1UECAwUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQ
# BgNVBAcMCUxhaG5zdGVpbjEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVy
# MS4wLAYDVQQDDCVPcGVuIFNvdXJjZSBEZXZlbG9wZXIgVGhvbWFzIE5hdW5oZWlt
# MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAphp2ljSmVsRB6gvkEq7A
# DUMENpKZiDFoAfF1ktD1nCwaVyI+bJ0ROjHwAn4iJgS+vlBDPOUIw2GLHkiqUhOg
# HSwzg6gj45GNmt6zxsKP0zPlhMB/fb0zmz7/F7e3yfKCXLfhbHuk0Es9gJ+8Wmet
# 9StVwwSE/K0MpW06GKA9nPWyE4N/ADwigDIj9rsTKJR4cve9w8G1b3zohpUc/qMr
# onab+HVw5r9b3sOBnnaXC6zvrX4fovRz1ztwfWjRjILLoFG6mW/TuK5F2ICD3Rhl
# 9R2rJWWWhzTDMsQlzvX+/SaM74q3OFpIVNdhvtmWU9pKUBRAyYMijcDqKEFGSzrc
# 8JFmGMpP5TuroWF+N21d3px2eARvW1mV0DCfFAH+Og20SH/gVW6/ltQQEkH/z/Zt
# 49ARyJrgESLOQtlMCIYSoKISdkk4ml4VUS1E/PcbzXEYj3a7m6DOxcd3WuY03UT9
# cnk3p0LMN6LeLYAZgVY+HRloZlH+kW2SkOHNuWgtXvZhAgMBAAGjggF4MIIBdDAM
# BgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3NjYTIwMjEu
# Y3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcwZTAsBggr
# BgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20wNQYIKwYB
# BQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIwMjEuY2Vy
# MB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQWBBTBvGyH
# MXj1dMbVnxuof9oP4PY7cDBLBgNVHSAERDBCMAgGBmeBDAEEATA2BgsqhGgBhvZ3
# AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwvQ1BTMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF
# AAOCAgEAkC165c7/r6y3VeEEpNXas9bQs6ZpKPdJffHkf9xoPi4kWvAIWGc4ocG1
# DJLb18RXiDYShDmT5sb/6I08FeipKi2DNJkuOq6yOTJ3S7+hNFIZCyd6i7WKO8ea
# P5I+FAbyJP4+5coZoMg45bUa9JoocaJCm+5n7WiCfN7/EQpClW5Trm4+ZMbp7EMN
# F1uLJgGCMnN0SsoUarp7PHlqupMxMC4Uhhe+lAP2ytcSIU81L97/qjrADbLWqICk
# 9il5iGM+aZndguq/cmfH9rJj4GRZHWnG6WwnzYD9+KgYlRI6em+/9dq+IRiXe2Bs
# thUuIaFW7U2gWw8tBTEYm1UBLHdJx+L1gjJCLs5KT/wy5wU32wZxNDqPUQejUC1x
# GvU3cKqmLgRq+yNf3gFVfzqIoggyM3U9wQvnk7nBtCkpjXS2hdx4yxzcmrlWsDHr
# z6+6/jeyQySzbYUlFmKnc4n9Mw2soHOajEua5poFARVRS5KgUBuiiQ21YFNYzWBo
# qGuECaaAI/C5rGxapMJ3dAOBnHw6NLqBet+ZXBJEccX5y89MLlOQu+jDmJP8HieR
# qDLlwfhVZnw4RY8Rus43NZBwhNlVuOUXqocHKmJhKNlekda3qsJj93eQBQOxKJcc
# w5O08q9jukpH2+yzFAj+7ll33DuNUgWptogc4Awtbx3glzcEvJQwggaCMIIEaqAD
# AgECAhAo8HfBHDa9/l90MkdwJy4DMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYT
# AlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMT
# G0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQTAeFw0yNjAzMTEwNzM0NTRaFw0z
# NjAyMjcwNzM0NTRaMFAxCzAJBgNVBAYTAlBMMSEwHwYDVQQKDBhBc3NlY28gRGF0
# YSBTeXN0ZW1zIFMuQS4xHjAcBgNVBAMMFUNlcnR1bSBUaW1lc3RhbXAgMjAyNjCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALi9zfBw+xI12hi2F9uNQBNO
# STkTIi13fuh3DiWybI5392bBfJw6tO7zDs7UqP/rejVf4xFTU3XNOPsuOFkP5SPF
# snWwn3VJL+NhNhsuujvYsQtaQnvNo1wk46reTgws1L8OFEdKaxgyqT4zOQx81mLw
# NZSqXNU9Rrb9oAJWJF4Mydz+C1ebu0D0+vgM3tqAQ8TU203fqgKCPqIdHloaFqH9
# eyR+8tnpmy2hnnN59ZOtmsuHeRwf5iTOhlhf0d/qqquLOSIGPgGVL1Hj5XBUox2O
# E7tQRr+l/xxNbL6OuwBO4Aa2EbuxYg37cpXv5i2/lTL4NASB/duf9W4fjA3Ro+GP
# +bC54n2OYiM3Cq8z92ejx8F5HWO2e4/75eC95B9tMw/eq7109GDyW2+2ut5Bn0o8
# MXJTERkyystWbqcnUkomA97qfiDCwvpWuOMdfTOksXe9S+xDxOJaCbiyTivgPulx
# cT+tfQTRXBr5UwtPvc+INnafOEilzVHA11tv+pHcylryxBc08cUihf/ttqkZS0bm
# x3VxQay8uohaTp0//ECA9vYGW51SKdJMJhXvKyIdBLy8WYNchTt8yRITv9Adxg9J
# qGujh+TnP1bW3aQjFtlEl7SzgNeI8OVA/CJAWWUuRT3Au9ToOpHy/flrrknhN4he
# Ew7/6ILMv4HSsj0YZ4otAgMBAAGjggFQMIIBTDB1BggrBgEFBQcBAQRpMGcwOwYI
# KwYBBQUHMAKGL2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdHNj
# YTIwMjEuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMB8GA1UdIwQYMBaAFL5UAi+/QGxzQ86sCSVOnkNEGu7gMAwGA1UdEwEB/wQC
# MAAwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0ucGwv
# Y3RzY2EyMDIxLmNybDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E
# BAMCB4AwIgYDVR0gBBswGTAIBgZngQwBBAIwDQYLKoRoAYb2dwIFAQswHQYDVR0O
# BBYEFCM5aiirmhKnpKfyH/lFmr+N/oIDMA0GCSqGSIb3DQEBDAUAA4ICAQBm/pOb
# rAveTqYFPCSthxNVwBHTW4TfuBQY2KGCdMlH6ALjVPYf2XARNGMTXScR0JQ2/c2L
# xLpoHHhRwxFf3d933DhFS165rYj6iPSQKm41rc9AMpTCjMdmai1aK9o870pOzTzL
# RhWFVIxm+5Qf+t4V5z14tPoUsAWdfRKD1Zzvzfe5AHlE9rxOZcGROiULxs3Edf7Z
# J8RZI0jzD9juj2oEX2XS7BVKuNCK1W0h1mjapv028yqo8UtZh5/1Ib48Yyp88BDY
# Shh5PSGUUuS5tYXVfcduIPBe3PdNTYRYdT5VNxkwLgjMzIcr/6SH2fejFJDjnCxj
# vXLuarErJg/IdI+hXdGIBDVOGv1zrkF4ktoQvFZIp5DIAYh0GwyUhc87qqn5UaYT
# j2+iyiBDSNh7aGCrkuAnqg7Qh7frhX+QhMGeJ9SpLpnG76qN85hLyjvYR6gRYiLT
# j8G4fxjqV400/buPw0rBo9yJZyq/8cGdCOLPOYf9qpefTcrZKeO9pg+UJHx8UABb
# aTqizRxm8sUSEycccCCFhIit3AI/3wGuSOADeiA676NcQ7+dsP04YvzvdN7pTZNy
# g672WpBDJdg0eItHl4yomvNeIOWh1qs4QqOW6TI8Ocsi4uSDTLorxLUDnBTwKQzz
# pxd1YUctEgRh32R0J9FkFkeEruJU94RljFr1uDCCBrkwggShoAMCAQICEQCZo4AK
# JlU7ZavcboSms+o5MA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAG
# A1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVt
# IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3Rl
# ZCBOZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjE4WhcNMzYwNTE4MDUzMjE4WjBW
# MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu
# MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQCdI88EMCM7wUYs5zNzPmNdenW6vlxNur3r
# Lfi+5OZ+U3iZIB+AspO+CC/bj+taJUbMbFP1gQBJUzDUCPx7BNLgid1TyztVLn52
# NKgxxu8gpyTr6EjWyGzKU/gnIu+bHAse1LCitX3CaOE13rbuHbtrxF2tPU8f253Q
# gX6eO8yTbGps1Mg+yda3DcTsOYOhSYNCJiL+5wnjZ9weoGRtvFgMHtJg6i671OPX
# IciiHO4Lwo2p9xh/tnj+JmCQEn5QU0NxzrOiRna4kjFaA9ZcwSaG7WAxeC/xoZSx
# F1oK1UPZtKVt+yrsGKqWONoK6f5EmBOAVEK2y4ATDSkb34UD7JA32f+Rm0wsr5aj
# zftDhA5mBipVZDjHpwzv8bTKzCDUSUuUmPo1govD0RwFcTtMXcfJtm1i+P2UNXad
# PyYVKRxKQATHN3imsfBiNRdN5kiVVeqP55piqgxOkyt+HkwIA4gbmSc3hD8ke66t
# 9MjlcNg73rZZlrLHsAIV/nJ0mmgSjBI/TthoGJDydekOQ2tQD2Dup/+sKQptalDl
# ui59SerVSJg8gAeV7N/ia4mrGoiez+SqV3olVfxyLFt3o/OQOnBmjhKUANoKLYlK
# mUpKEFI0PfoT8Q1W/y6s9LTI6ekbi0igEbFUIBE8KDUGfIwnisEkBw5KcBZ3XwnH
# mfznwlKo8QIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
# 3XRdTADbe5+gdMqxbvc8wDLAcM0wHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61Pgc
# ptE6Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDAGA1Ud
# HwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYI
# KwYBBQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0
# dW0uY29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
# Y3RuY2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0
# cDovL3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQB1iFgP5Y9Q
# KJpTnxDsQ/z0O23JmoZifZdEOEmQvo/79PQg9nLF/GJe6ZiUBEyDBHMtFRK0mXj3
# Qv3gL0sYXe+PPMfwmreJHvgFGWQ7XwnfMh2YIpBrkvJnjwh8gIlNlUl4KENTK5DL
# qsYPEtRQCw7R6p4s2EtWyDDr/M58iY2UBEqfUU/ujR9NuPyKk0bEcEi62JGxauFY
# zZ/yld13fHaZskIoq2XazjaD0pQkcQiIueL0HKiohS6XgZuUtCKA7S6CHttZEsOb
# QJ1j2s0urIDdqF7xaXFVaTHKtAuMfwi0jXtF3JJphrJfc+FFILgCbX/uYBPBlbBI
# P4Ht4xxk2GmfzMn7oxPITpigQFJFWuzTMUUgdRHTxaTSKRJ/6Uh7ki/pFjf9sUAS
# WgxT69QF9Ki4JF5nBIujxZ2sOU9e1HSCJwOfK07t5nnzbs1LbHuAIGJsRJiQ6HX/
# DW1XFOlXY1rc9HufFhWU+7Uk+hFkJsfzqBz3pRO+5aI6u5abI4Qws4YaeJH7H7M8
# X/YNoaArZbV4Ql+jarKsE0+8XvC4DJB+IVcvC9Ydqahi09mjQse4fxfef0L7E3hh
# o2O3bLDM6v60rIRUCi2fJT2/IRU5ohgyTch4GuYWefSBsp5NPJh4QRTP9DC3gc5Q
# EKtbrTY0Ka87Web7/zScvLmvQBm8JDFpDjCCBrkwggShoAMCAQICEQDn/2nHOzXO
# S5Em2HR8aKWHMA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UE
# ChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl
# cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBO
# ZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjA3WhcNMzYwNTE4MDUzMjA3WjBWMQsw
# CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQw
# IgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0EwggIiMA0GCSqGSIb3
# DQEBAQUAA4ICDwAwggIKAoICAQDpEh8ENe25XXrFppVBvoplf0530W0lddNmjtv4
# YSh/f7eDQKFaIqc7tHj7ox+u8vIsJZlroakUeMS3i3T8aJRC+eQs4FF0GqvkM6+W
# ZO8kmzZfxmZaBYmMLs8FktgFYCzywmXeQ1fEExflee2OpbHVk665eXRHjH7MYZIz
# Nnjl2m8Hy8ulB9mR8wL/W0v0pjKNT6G0sfrx1kk+3OGosFUb7yWNnVkWKU4qSxLv
# 16kJ6oVJ4BSbZ4xMak6JLeB8szrK9vwGDpvGDnKCUMYL3NuviwH1x4gZG0JAXU3x
# 2pOAz91JWKJSAmRy/l0s0l5bEYKolg+DMqVhlOANd8Yh5mkQWaMEvBRE/kAGzIqg
# WhwzN2OsKIVtO8mf5sPWSrvyplSABAYa13rMYnzwfg08nljZHghquCJYCa/xHK9a
# cev9UD7Y+usr15d7mrszzxhF1JOr1Mpup2chNSBlyOObhlSO16rwrffVrg/SzaKf
# SndS5swRhr8bnDqNJY9TNyEYvBYpgF95K7p0g4LguR4A++Z1nFIHWVY5v0fNVZmg
# zxD9uVo/gta3onGOQj3JCxgYx0KrCXu4yc9QiVwTFLWbNdHFSjBCt5/8Q9pLuRhV
# ocdCunhcHudMS1CGQ/Rn0+7P+fzMgWdRKfEOh/hjLrnQ8BdJiYrZNxvIOhM2aa3z
# EDHNwwIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvlQC
# L79AbHNDzqwJJU6eQ0Qa7uAwHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61PgcptE6
# Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMDAGA1UdHwQp
# MCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYIKwYB
# BQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ru
# Y2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0cDov
# L3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQC4k1l3yUwV/ZQH
# CKCneqAs8EGTnwEUJLdDpokN/dMhKjK0rR5qX8nIIHzxpQR3TAw2IRw1Uxsr2Pli
# G3bCFqSdQTUbfaTq6V3vBzEebDru9QFjqlKnxCF2h1jhLNFFplbPJiW+JSnJTh1f
# KEqEdKdxgl9rVTvlxfEJ7exOn25MGbd/wGPwuSmMxRJVO0wnqgS7kmoJjNF9zqee
# hFSDDP8ZVkWg4EZ2tIS0M3uZmByRr+1Lkwjjt8AtW83mVnZTyTsOb+FNfwJY7DS4
# FmWhkRbgcHRetreoTirPOr/ozyDKhT8MTSTf6Lttg6s6T/u08mDWw6HK04ZRDfQ9
# sb77QV8mKgO44WGP31vXnVKoWVJpFBjPvjL8/Zck/5wXX2iqjOaLStFOR/IQki+E
# hn4zlcgVm22ZVCBPF+l8nAwUUShCtKuSU7GmZLKCmmxQMkSiWILTm8EtVD6AxnJh
# oq8EnhjEEyUoflkeRF2WhFiVQOmWTwZRr44IxWGkNJC6tTorW5rl2Zl+2e9JLPYf
# 3pStAPMDoPKIjVXd6NW2+fZrNUBeDo2eOa5Fn7Brs/HLQff5Xgris5MeUbdVgDrF
# 8uxO6cLPvZPo63j62SsNg55pTWk9fUIF9iPoRbb4QurjoY/woI1RAOKtYtTic6aA
# Jq3u83RIPpGXBSJKwx4KJAOZnCDCtTGCBqIwggaeAgEBMGowVjELMAkGA1UEBhMC
# UEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMb
# Q2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBX5gfP8LabvnxCXoItEBa/MA0G
# CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI
# hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC
# ARUwLwYJKoZIhvcNAQkEMSIEIG0LfSmImqxaCs0oneedW21eLr1/TiA3v5nPXDDg
# +RPzMA0GCSqGSIb3DQEBAQUABIIBgIoMN7igrxItJBV6Rm2ZW9FhNfe0J/GHd+8N
# ifgle6Gn9maWTETcRWxJwgKwJqJOS3/ayXfL70ZUfGKT+KkRR3nGd4eu50M4o36z
# hDa8SbaVZTl3OsUHUPzZ/tAe8AQNHQCf05ym7gH3BbvpFBW0td/6xuhkFw/gLIXC
# WB1/0ku6fO4OYpGGJn8fUtYu86VmbmfhR+4ZDSH9hmRCRE5GqfX7t17oZPp8agfl
# HuLnupsOxMKO65mu9gDrVp+pKMJwaQrcNZH0RSDe7PoAVzBHUN2L07z6pj+683DL
# 0iHK1typkAA3CIFRLyVsDy1GFNrw3FdU1iV0q/ZsBt8B3nINmZAG33EqqNZ7ad4c
# BeDP/wPBHaeKUCS92AR+fwSmFWZTB6ggeZS+d5GGE3HFG9OfzcSAdxPAUIh8cU/y
# g6uTmJ5IM6jxvQySM9F3k7AR8jCgc9wpLCZYUtrvCJ4gghconeF+NTuRQH3Buq7T
# 5QpUaP5j9TytSqv1/hslJANKkcWx+qGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxMTFaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDB5sQTBTgB13Tqx1BNlDvjWMzmGFKoZeaIH
# 3ZDPiUj6DSOwe8wnVBB3JJHiUZUs/icwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAA1DagfqOTxOsYZGUuzuxnoLuhYQA4sTCAo5YK8/mgZ2M+MuN
# vATTSjZWaZu0L1nZ7X4NaVFmEU0LQ134rs9wut4Hli7b2yQl4dWMyro7xnowBgsV
# vr4ILl9LXGRykiAtp7eOebzr0ShvGVm2Z7QU4Z2uEh9FU/+OszwZE2pCXCwIAfHV
# bcfj7hCS8ZjYQyvzATmgcUr667z3aooEFzzAdy+DalC8G8IdRh55nhxid37gOM5s
# VczZJ+L42AfxwSna758eDvrAjRLg6+UPEQAyHEv+VImr3i1D2yM1bhiw8iGXlBLb
# ks5Nj1YYzJtvhUGlY7g2nO+mtUFcO1tZvsl7UCfj3SlZBu9ZoTTa42H+fAaRYjxi
# LuFKOPOc2oHKideFPFdvQz2rOOFQ56AfbzgSqCAs0fZHAaCzOZuLZdNVeIoScDO1
# 80MvGU6Q5tkDIFOC3DNJtZxk8fmd42jElxhbhJfradBT+4BOn0dVtJm5gvRrl8k/
# Yv9ZT5omoCs2Y0jp0enW1xMKldu87NvzFWZrokukrq1rm+X58X47kt/DOnEfz2xH
# 9GFu6bKEFkzOtc/ByOHB/lbHas9/kQz1R7BUlf7gIGjXQZITv3wsfaiAdjWObmEQ
# p+0CwNV4inKf/D9ymBHUc/jzB9JVXWypcyveOTBzje6SG0lKuBsK7q/oLaM=
# SIG # End signature block