Public/PrivilegedAccess/Get-EntraOpsPrivilegedDefenderRoles.ps1

<#
.SYNOPSIS
    Get a list of directory role member assignments in Microsoft Defender.

.DESCRIPTION
    Get a list of RBAC member assignments in Microsoft Defender.

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

.PARAMETER PrincipalTypeFilter
    Filter for principal type. Default is User, Group, ServicePrincipal. Possible values are User, Group, ServicePrincipal.

.PARAMETER ExpandGroupMembers
    Expand group members for transitive role assignments. Default is $true.

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

.EXAMPLE
    Get a list of assignment of Microsoft Defender roles.
    Get-EntraOpsPrivilegedDefenderRoles
#>


function Get-EntraOpsPrivilegedDefenderRoles {
    param (
        [Parameter(Mandatory = $False)]
        [System.String]$TenantId = (Get-EntraOpsAzContextValue -Property TenantId)
        ,
        [Parameter(Mandatory = $False)]
        [ValidateSet("User", "Group", "ServicePrincipal")]
        [Array]$PrincipalTypeFilter = ("User", "Group", "ServicePrincipal")
        ,
        [Parameter(Mandatory = $False)]
        [System.Boolean]$ExpandGroupMembers = $true
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$SampleMode = $False
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$EnableParallelProcessing = $true
        ,
        [Parameter(Mandatory = $false)]
        [System.Int32]$ParallelThrottleLimit = 10
        ,
        [Parameter(Mandatory = $false)]
        [System.Collections.Generic.List[psobject]]$WarningMessages
    )

    # Set Error Action
    $ErrorActionPreference = "Stop"

    #region Get Role Definitions and Role Assignments
    Write-Host "Get Defender Role Management Assignments and Role Definition..."
    if ($SampleMode -eq $True) {
        Write-Warning "Not supported yet!"
    } else {
        $DefenderRoleDefinitions = Invoke-EntraOpsMsGraphQuery -Uri "/beta/roleManagement/defender/roleDefinitions?`$select=id,displayName,description,rolePermissions" -OutputType PSObject
        $DefenderRoleAssignments = Invoke-EntraOpsMsGraphQuery -Uri "/beta/roleManagement/defender/roleAssignments?`$select=id,principalIds,roleDefinitionId,appScopeIds,directoryScopeIds" -OutputType PSObject
    }
    
    # Optimization: Build Role Definition Lookup Hashtable for O(1) access
    Write-Verbose "Building Role Definition Lookup Table..."
    $RoleDefLookup = @{}
    foreach ($RoleDef in $DefenderRoleDefinitions) {
        $RoleDefLookup[$RoleDef.id] = $RoleDef
    }
    #endregion


    #region Get role assignments for all permanent role member
    Write-Host "Get details of Defender Role Assignments foreach individual principal..."
    if (![string]::IsNullOrWhiteSpace($DefenderRoleAssignments.id)) {
        # Fetch Defender custom app scopes for resolving appScopeIds to display names. Defender Unified RBAC
        # supports direct scoping on cloud scope sets (Defender for Cloud, type "CloudSet") and identity scopes
        # (MDI, type "UserGroupId"). The customAppScope.id is the value used as appScopeId in Defender Unified
        # RBAC role assignments.
        # NOTE: MDE device groups are NOT part of URBAC assignment scoping. Device group restrictions are
        # configured separately on the Device Groups page (per-group "User access" with Entra groups) and
        # govern per-device visibility alongside Unified RBAC. They are not exposed as appScopeIds.
        $DefenderAppScopes = @()
        try {
            $DefenderAppScopes = Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/beta/roleManagement/defender/customAppScopes" -OutputType PSObject -DisableCache
        } catch {
            Write-Warning "Failed to fetch Defender custom app scopes for scope name resolution. $($_.Exception.Message)"
        }

        function Get-DefenderAppScopeLookupKey {
            param([string]$ScopeId, [string]$ScopeType)

            if ([string]::IsNullOrWhiteSpace($ScopeId)) { return $null }
            $ScopeId = $ScopeId.Trim().Trim('/')
            $ScopeParts = @($ScopeId -split '/')
            $ScopeLeafId = $ScopeParts[-1]
            $EffectiveScopeType = if (-not [string]::IsNullOrWhiteSpace($ScopeType)) { $ScopeType } elseif ($ScopeParts.Count -gt 1) { $ScopeParts[-2] } else { '' }
            return "$EffectiveScopeType/$ScopeLeafId".ToLowerInvariant()
        }

        $AppScopeLookup = @{}
        foreach ($AppScope in $DefenderAppScopes) {
            $LookupKey = Get-DefenderAppScopeLookupKey -ScopeId "$($AppScope.id)" -ScopeType "$($AppScope.type)"
            if (-not [string]::IsNullOrEmpty($LookupKey)) { $AppScopeLookup[$LookupKey] = $AppScope }
        }

        # CloudSet custom app scopes for Defender for Cloud RBAC are backed 1:1 by a Microsoft Security
        # Exposure Management "zone" (the CloudSet id's GUID is the zone id). Some zones (e.g. created
        # directly via Security Exposure Management rather than the classic Defender for Cloud RBAC UI)
        # are not returned by /beta/roleManagement/defender/customAppScopes at all, which is why their
        # appScopeId previously fell through to "Unresolved app scope" with 0 subscriptions. Resolving
        # directly against the zones API (using the guid embedded in the appScopeId/CloudSet id) works
        # even when the customAppScopes lookup misses the CloudSet entirely.
        function Get-CloudSetZoneInfo {
            param([string]$CloudSetAppScopeId)

            $Result = [pscustomobject]@{ DisplayName = $null; SubscriptionScopes = @() }
            $ZoneId = "$CloudSetAppScopeId".Trim().Trim('/') -replace '(?i)^cloudset/', ''
            if ([string]::IsNullOrWhiteSpace($ZoneId)) { return $Result }

            try {
                # -ThrowOnFailure so a real failure (e.g. missing Zone.Read.All) lands in the catch
                # below - without it, Invoke-EntraOpsMsGraphQuery just warns and returns $null, and
                # this function's own more specific warning message below never fires.
                # -SuppressForbiddenWarning avoids reporting the same 403 twice, since the catch
                # already emits a message naming the CloudSet and the missing permission.
                $Zone = Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/beta/security/zones/$($ZoneId)?`$expand=environments" -OutputType PSObject -ThrowOnFailure -SuppressForbiddenWarning
                $Result.DisplayName = $Zone.displayName
                $ZoneSubscriptionScopes = [System.Collections.Generic.List[string]]::new()
                foreach ($Environment in @($Zone.environments)) {
                    if ("$($Environment.kind)" -eq 'azureSubscription' -and "$($Environment.id)" -match '(?i)^/subscriptions/([0-9a-f-]{36})/?$') {
                        $ZoneSubscriptionScopes.Add("/subscriptions/$($Matches[1].ToLowerInvariant())") | Out-Null
                    }
                }
                $Result.SubscriptionScopes = @($ZoneSubscriptionScopes | Sort-Object -Unique)
            } catch {
                $ZoneWarningMessage = "Failed to resolve Security Exposure Management zone '$ZoneId' for CloudSet '$CloudSetAppScopeId': $($_.Exception.Message)"
                if ($_.Exception.Data['StatusCode'] -eq 403 -or $_.Exception.Message -match '(?i)forbidden') {
                    $ZoneWarningMessage += " This usually means the 'Zone.Read.All' Microsoft Graph permission is missing - re-run New-EntraOpsWorkloadIdentity and grant admin consent, then repeat the collection."
                }
                if ($null -ne $WarningMessages) {
                    $WarningMessages.Add([PSCustomObject]@{
                            Timestamp = (Get-Date)
                            Type      = 'CloudSetZoneResolutionError'
                            ObjectId  = "$CloudSetAppScopeId"
                            Message   = $ZoneWarningMessage
                        })
                }
                Write-Verbose $ZoneWarningMessage
            }
            return $Result
        }

        function Get-CloudSetSubscriptionScopes {
            param([psobject]$CloudSet)

            $ZoneInfo = Get-CloudSetZoneInfo -CloudSetAppScopeId "$($CloudSet.id)"
            if ($ZoneInfo.SubscriptionScopes.Count -gt 0) { return $ZoneInfo.SubscriptionScopes }

            # Fallback for CloudSets that are not zone-backed (or where the zone lookup failed/returned no
            # subscriptions): recover subscription ids embedded in customAttributes if present.
            $subscriptionScopes = [System.Collections.Generic.List[string]]::new()
            if ($null -ne $CloudSet.customAttributes) {
                function Add-CloudSetSubscriptionScope {
                    param([object]$Value, [string]$PropertyName)

                    if ($null -eq $Value) { return }
                    if ($Value -is [string]) {
                        if ($Value.TrimStart().StartsWith('{') -or $Value.TrimStart().StartsWith('[')) {
                            try {
                                Add-CloudSetSubscriptionScope -Value ($Value | ConvertFrom-Json) -PropertyName $PropertyName
                                return
                            } catch {
                                Write-Verbose "CloudSet custom attribute '$PropertyName' is not valid JSON."
                            }
                        }
                        if ($Value -match '(?i)^/subscriptions/([0-9a-f-]{36})/?$') {
                            $subscriptionScopes.Add("/subscriptions/$($Matches[1].ToLowerInvariant())") | Out-Null
                        } elseif ($PropertyName -match '(?i)subscription' -and $Value -match '(?i)^[0-9a-f-]{36}$') {
                            $subscriptionScopes.Add("/subscriptions/$($Value.ToLowerInvariant())") | Out-Null
                        }
                        return
                    }
                    if ($Value -is [System.Collections.IEnumerable]) {
                        foreach ($Item in $Value) { Add-CloudSetSubscriptionScope -Value $Item -PropertyName $PropertyName }
                        return
                    }
                    foreach ($Property in $Value.PSObject.Properties) {
                        Add-CloudSetSubscriptionScope -Value $Property.Value -PropertyName $Property.Name
                    }
                }

                Add-CloudSetSubscriptionScope -Value $CloudSet.customAttributes -PropertyName 'customAttributes'
            }

            return @($subscriptionScopes | Sort-Object -Unique)
        }

        $DefenderRoleAssignmentPrincipals = ($DefenderRoleAssignments | select-object -ExpandProperty principalIds -Unique)
        Write-Host "Processing $($DefenderRoleAssignmentPrincipals.Count) Defender role principals..."
        $PrincipalCounter = 0
        $DefenderPermanentRbacAssignments = foreach ($Principal in $DefenderRoleAssignmentPrincipals) {
            $PrincipalCounter++
            if (($PrincipalCounter % 10) -eq 0 -or $PrincipalCounter -eq $DefenderRoleAssignmentPrincipals.Count) {
                $PercentComplete = [math]::Round(($PrincipalCounter / $DefenderRoleAssignmentPrincipals.Count) * 100, 0)
                Write-Progress -Activity "Processing Defender Role Principals" -Status "Processing principal $PrincipalCounter of $($DefenderRoleAssignmentPrincipals.Count)" -PercentComplete $PercentComplete
            }

            $RawPrincipalId = "$Principal"
            $PrincipalGuid = [guid]::Empty
            if (-not [guid]::TryParse($RawPrincipalId, [ref]$PrincipalGuid)) {
                $WarningMessage = "Skipping Defender role assignment principal with invalid GUID: $RawPrincipalId"
                if ($null -ne $WarningMessages) {
                    $WarningMessages.Add([PSCustomObject]@{
                            Timestamp = (Get-Date)
                            Type      = "InvalidPrincipalId"
                            ObjectId  = $RawPrincipalId
                            Message   = $WarningMessage
                        })
                }
                Write-Warning $WarningMessage
                continue
            }
            $Principal = $PrincipalGuid.ToString()

            Write-Verbose "Get identity information from permanent member $Principal"
            # Reset to avoid carrying over the ObjectType from a previous iteration when resolution fails
            $ObjectType = $null
            try {
                $PrincipalProfile = Invoke-EntraOpsMsGraphQuery -Method Get -Uri "https://graph.microsoft.com/beta/directoryObjects/$($Principal)" -OutputType PSObject
                $ObjectType = $PrincipalProfile.'@odata.type'.Replace('#microsoft.graph.', '')
            } catch {
                $WarningMessage = "Issue to resolve directory object $Principal! $($_.Exception.Message)"
                if ($null -ne $WarningMessages) {
                    $WarningMessages.Add([PSCustomObject]@{
                            Timestamp = (Get-Date)
                            Type      = "ObjectResolutionError"
                            ObjectId  = $Principal
                            Message   = $WarningMessage
                        })
                }
                Write-Warning $WarningMessage
            }

            $AllPrinicpalDefenderRoleAssignments = Invoke-EntraOpsMsGraphQuery -Uri "/beta/roleManagement/defender/RoleAssignments?`$count=true&`$filter=principalIds/any(a:a+eq+'$Principal')" -ConsistencyLevel "eventual" -OutputType PSObject

            foreach ($DefenderPrincipalRoleAssignment in $AllPrinicpalDefenderRoleAssignments) {

                # Optimization: Use hashtable lookup instead of Where-Object for O(1) access
                $Role = $RoleDefLookup[$DefenderPrincipalRoleAssignment.roleDefinitionId]

                if ($null -eq $Role) {
                    $WarningMessage = "Role definition is empty or does not exist for Role Assignment $($DefenderPrincipalRoleAssignment.id)"
                    if ($null -ne $WarningMessages) {
                        $WarningMessages.Add([PSCustomObject]@{
                                Timestamp = (Get-Date)
                                Type      = "RoleDefinitionError"
                                ObjectId  = $DefenderPrincipalRoleAssignment.id
                                Message   = $WarningMessage
                            })
                    }
                    Write-Warning $WarningMessage
                }

                # Check for scope restriction in appScopeIds:
                # Defender Unified RBAC uses appScopeIds for two different things (verified against live tenant data):
                # - Workload/data source tokens (e.g. "Mde", "Mdo", "Mdi", "Mdc", "SecureScoreExternal") when the
                # assignment applies to ALL scopes of the selected data sources. These do NOT resolve in
                # customAppScopes and are effectively tenant-wide for the workload. MDE device group
                # restrictions are managed on the Device Groups page (per-group user access), NOT here.
                # - Ids of directly scoped assignments: customAppScope objects (type "CloudSet" for Defender for
                # Cloud, "UserGroupId" for Defender for Identity). Only these are real scope restrictions and
                # must be used as role scope. An unresolved CloudSet remains a bounded scope; it must never be
                # folded into the tenant-wide workload token list.
                $ResolvedAppScopes = @()
                $WorkloadWideTokens = @()
                $UnresolvedAppScopes = @()
                $WorkloadScopeTokens = @('Mde', 'Mdo', 'Mdi', 'Mdc', 'SecureScoreExternal')
                foreach ($AppScopeId in @($DefenderPrincipalRoleAssignment.appScopeIds | Where-Object { -not [string]::IsNullOrEmpty($_) -and $_ -ne "/" })) {
                    $AppScopeLookupKey = Get-DefenderAppScopeLookupKey -ScopeId "$AppScopeId"
                    $AppScope = $AppScopeLookup[$AppScopeLookupKey]
                    if ($null -ne $AppScope) {
                        $ResolvedAppScopes += [pscustomobject]@{ AppScopeId = "$AppScopeId"; Scope = $AppScope }
                    } elseif ($WorkloadScopeTokens -contains $AppScopeId) {
                        $WorkloadWideTokens += $AppScopeId
                    } elseif ($AppScopeLookupKey -match '(?i)^cloudset/') {
                        # Not returned by /beta/roleManagement/defender/customAppScopes (e.g. a zone created
                        # directly via Security Exposure Management) -- still resolvable via the zones API
                        # using the guid embedded in the appScopeId itself.
                        $ZoneInfo = Get-CloudSetZoneInfo -CloudSetAppScopeId "$AppScopeId"
                        if ($ZoneInfo.SubscriptionScopes.Count -gt 0 -or -not [string]::IsNullOrWhiteSpace($ZoneInfo.DisplayName)) {
                            $SyntheticCloudSet = [pscustomobject]@{ id = "$AppScopeId"; type = 'CloudSet'; displayName = $ZoneInfo.DisplayName; customAttributes = $null }
                            $ResolvedAppScopes += [pscustomobject]@{ AppScopeId = "$AppScopeId"; Scope = $SyntheticCloudSet }
                        } else {
                            $UnresolvedAppScopes += $AppScopeId
                            $WarningMessage = "Defender app scope '$AppScopeId' could not be resolved (not in customAppScopes, and its Security Exposure Management zone lookup returned no data). It will be retained as a bounded scope rather than treated as tenant-wide."
                            if ($null -ne $WarningMessages) {
                                $WarningMessages.Add([PSCustomObject]@{
                                        Timestamp = (Get-Date)
                                        Type      = 'UnresolvedDefenderAppScope'
                                        ObjectId  = $DefenderPrincipalRoleAssignment.id
                                        Message   = $WarningMessage
                                    })
                            }
                            Write-Warning $WarningMessage
                        }
                    } else {
                        $UnresolvedAppScopes += $AppScopeId
                        $WarningMessage = "Defender app scope '$AppScopeId' could not be resolved. It will be retained as a bounded scope rather than treated as tenant-wide."
                        if ($null -ne $WarningMessages) {
                            $WarningMessages.Add([PSCustomObject]@{
                                    Timestamp = (Get-Date)
                                    Type      = 'UnresolvedDefenderAppScope'
                                    ObjectId  = $DefenderPrincipalRoleAssignment.id
                                    Message   = $WarningMessage
                                })
                        }
                        Write-Warning $WarningMessage
                    }
                }

                $RoleAssignmentScopes = @(
                    # Assignment restricted to specific scopes (cloud scope sets, identity/user group scopes)
                    foreach ($ResolvedAppScope in $ResolvedAppScopes) {
                        $AppScope = $ResolvedAppScope.Scope
                        $ScopeName = if (-not [string]::IsNullOrEmpty($AppScope.displayName)) { $AppScope.displayName } else { "$($AppScope.type)-$($ResolvedAppScope.AppScopeId)" }
                        $CloudSetSubscriptionScopes = if ($AppScope.type -eq 'CloudSet') { @(Get-CloudSetSubscriptionScopes -CloudSet $AppScope) } else { @() }
                        [pscustomobject]@{ ScopeId = $ResolvedAppScope.AppScopeId; ScopeName = $ScopeName; AppScopeName = @($WorkloadWideTokens); CloudSetSubscriptionScopes = $CloudSetSubscriptionScopes; ScopeResolutionStatus = 'Resolved' }
                    }
                    foreach ($UnresolvedAppScope in $UnresolvedAppScopes) {
                        [pscustomobject]@{ ScopeId = "$UnresolvedAppScope"; ScopeName = "Unresolved app scope ($UnresolvedAppScope)"; AppScopeName = @($WorkloadWideTokens); CloudSetSubscriptionScopes = @(); ScopeResolutionStatus = 'Unresolved' }
                    }
                    if ($ResolvedAppScopes.Count -eq 0 -and $UnresolvedAppScopes.Count -eq 0 -and $WorkloadWideTokens.Count -gt 0) {
                        # Workload-wide data source selection: tenant-wide scope, data sources kept visible in the
                        # name. Kept as a single combined row (not one row per workload token) since this is one
                        # physical role assignment - splitting it into multiple rows would duplicate it across
                        # reports/exports that count or graph by RoleAssignmentId. AppScopeName instead exposes the
                        # individual workload tokens (e.g. "Mde", "Mdo", "Mdi") for per-workload filtering/reporting
                        # without touching the row count or RoleAssignmentScopeName's classification-relevant value.
                        [pscustomobject]@{ ScopeId = "/"; ScopeName = "Tenant-wide ($($WorkloadWideTokens -join ', '))"; AppScopeName = @($WorkloadWideTokens); CloudSetSubscriptionScopes = @(); ScopeResolutionStatus = 'WorkloadWide' }
                    } elseif ($ResolvedAppScopes.Count -eq 0 -and $UnresolvedAppScopes.Count -eq 0) {
                        # No app scopes: directory scope applies (empty value means tenant-wide,
                        # the role is assigned to all devices or all users without specific directoryScopeId)
                        $DirectoryScopeIds = @($DefenderPrincipalRoleAssignment.directoryScopeIds | Where-Object { -not [string]::IsNullOrEmpty($_) })
                        if ($DirectoryScopeIds.Count -eq 0) { $DirectoryScopeIds = @("/") }
                        foreach ($DirectoryScopeId in $DirectoryScopeIds) {
                            $ScopeName = if ($DirectoryScopeId -eq "/") { "Tenant-wide" } else { $DirectoryScopeId }
                            [pscustomobject]@{ ScopeId = "$DirectoryScopeId"; ScopeName = $ScopeName; AppScopeName = $null; CloudSetSubscriptionScopes = @(); ScopeResolutionStatus = 'DirectoryScope' }
                        }
                    }
                )

                foreach ($RoleAssignmentScope in $RoleAssignmentScopes) {
                    [pscustomobject]@{
                        RoleAssignmentId                      = $DefenderPrincipalRoleAssignment.Id
                        RoleAssignmentScopeId                 = $RoleAssignmentScope.ScopeId
                        RoleAssignmentScopeName               = $RoleAssignmentScope.ScopeName
                        AppScopeName                          = $RoleAssignmentScope.AppScopeName
                        CloudSetSubscriptionScopes            = $RoleAssignmentScope.CloudSetSubscriptionScopes
                        ScopeResolutionStatus                 = $RoleAssignmentScope.ScopeResolutionStatus
                        RoleAssignmentType                    = "Direct"
                        RoleAssignmentSubType                 = ""
                        PIMManagedRole                        = $False
                        PIMAssignmentType                     = "Permanent"
                        RoleDefinitionName                    = $Role.displayName
                        RoleDefinitionId                      = $Role.id
                        RoleType                              = if ($Role.isBuiltIn -eq $True) { "Built-In" } else { "Custom" }
                        RoleIsPrivileged                      = $Role.isPrivileged
                        ObjectId                              = $Principal
                        ObjectTenantId                        = $TenantId
                        ObjectType                            = $ObjectType
                        TransitiveByObjectId                  = ""
                        TransitiveByObjectDisplayName         = ""
                        TransitiveByNestingObjectIds          = $null
                        TransitiveByNestingObjectDisplayNames = $null
                    }
                }
            }
        }
        Write-Progress -Activity "Processing Defender Role Principals" -Completed
    } else {
        Write-Warning "No Defender Role Assignments found!"
        $DefenderPermanentRbacAssignments = $null
    }

    #endregion

    # Summarize results with direct permanent (excl. activated roles) and eligible role assignments
    $AllDefenderRbacAssignments = @()
    $AllDefenderRbacAssignments += $DefenderPermanentRbacAssignments

    #region Collect transitive assignments by group members of Role-Assignable Groups or Security Groups
    if ($ExpandGroupMembers -eq $True) {    
        Write-Verbose "Expanding groups for direct or transitive Entra Microsoft Defender XDR role assignments"
        $GroupsWithRbacAssignment = $AllDefenderRbacAssignments | where-object { $_.ObjectType -eq "group" } | Select-Object -Unique ObjectId, ObjectDisplayName
        $AllTransitiveMembers = @()

        foreach ($GroupWithRbacAssignment in $GroupsWithRbacAssignment) {
            $TransitiveMembers = Get-EntraOpsPrivilegedTransitiveGroupMember -GroupObjectId $($GroupWithRbacAssignment.ObjectId) -WarningMessages $WarningMessages
            $GroupObjectDisplayName = (Invoke-EntraOpsMsGraphQuery -Method Get -Uri "https://graph.microsoft.com/beta/groups/$($GroupWithRbacAssignment.ObjectId)" -OutputType PSObject).displayName
            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 += $Member
            }
        }

        $DefenderTransitiveRbacAssignments = [System.Collections.Generic.List[object]]::new()
        foreach ($RbacAssignmentByGroup in ($AllDefenderRbacAssignments | where-object { $_.ObjectType -eq "group" }) ) {

            $RbacAssignmentByNestedGroupMembers = $AllTransitiveMembers | Where-Object { $_.GroupObjectId -eq $RbacAssignmentByGroup.ObjectId }

            if ($RbacAssignmentByNestedGroupMembers.Count -gt 0) {
                $RbacAssignmentByNestedGroupMembers | foreach-object {
                    $TransitiveAssignment = [pscustomobject]@{
                        RoleAssignmentId                      = $RbacAssignmentByGroup.RoleAssignmentId
                        RoleAssignmentScopeId                 = $RbacAssignmentByGroup.RoleAssignmentScopeId
                        RoleAssignmentScopeName               = $RbacAssignmentByGroup.RoleAssignmentScopeName
                        AppScopeName                          = $RbacAssignmentByGroup.AppScopeName
                        RoleAssignmentType                    = "Transitive"
                        RoleAssignmentSubType                 = $_.RoleAssignmentSubType
                        PIMManagedRole                        = $RbacAssignmentByGroup.PIMManagedRole
                        PIMAssignmentType                     = $RbacAssignmentByGroup.PIMAssignmentType
                        RoleDefinitionName                    = $RbacAssignmentByGroup.RoleDefinitionName
                        RoleDefinitionId                      = $RbacAssignmentByGroup.RoleDefinitionId
                        RoleType                              = $RbacAssignmentByGroup.RoleType
                        RoleIsPrivileged                      = $RbacAssignmentByGroup.RoleIsPrivileged
                        ObjectId                              = $_.Id
                        ObjectType                            = $_.'@odata.type'.Replace("#microsoft.graph.", "").ToLower()
                        TransitiveByObjectId                  = $RbacAssignmentByGroup.ObjectId
                        TransitiveByObjectDisplayName         = $_.GroupObjectDisplayName
                        TransitiveByNestingObjectIds          = $_.NestingObjectIds
                        TransitiveByNestingObjectDisplayNames = $_.NestingObjectDisplayNames
                    }
                    $DefenderTransitiveRbacAssignments.Add($TransitiveAssignment) | Out-Null
                }
            } else {
                if ($null -ne $WarningMessages) {
                    $WarningMessages.Add([PSCustomObject]@{
                            Type    = "Empty Group"
                            Message = "Empty group $($RbacAssignmentByGroup.ObjectId)"
                            Target  = $RbacAssignmentByGroup.ObjectId
                        })
                }
            }
        }
    }
    #endregion

    #region Filtering export if needed
    $AllDefenderRbacAssignments += $DefenderTransitiveRbacAssignments
    $AllDefenderRbacAssignments = $AllDefenderRbacAssignments | where-object { $_.ObjectType -in $PrincipalTypeFilter }
    $AllDefenderRbacAssignments = $AllDefenderRbacAssignments | select-object -Unique *
    $AllDefenderRbacAssignments | Sort-Object RoleAssignmentId, RoleAssignmentScopeName, RoleAssignmentScopeId, RoleAssignmentType, ObjectId
    #endregion
}
# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBlmWq6mf+6UlqQ
# cbnbZVJJ1a9ODt2DeSiMW2ZaI2+IPaCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIIJlV/5h2gfMKfFiWXpE4kKugcrQ0qOFfL/6EnzW
# yWIhMA0GCSqGSIb3DQEBAQUABIIBgD1qWylwrZHLIFCxFQvaenmmvNHqet3axi3O
# XL7/JkF8C8d+FLdmXLXMNbAfnnXI4gTT3oFJ01Ez9eo8Uky1c4kKP4iMrT6vMyy0
# rR2J3F6m1mlTf4MC1WwadQgGSVYQW/LeaLJpn/c48Q/eDvljtpU9R5fbml07ISvJ
# UtLtqBZ6TXpGeFyBH/SUh6STkZd7GM52nR7ZZMDxKJAZOKUfS0o9uSdnEdkpy0av
# hv5L7xeXQoEz41RDfILkfi/GW55y7dwreSVM/dF3Nf7etNxQJwFcLnImQ7E1IJ4W
# uCF97UmPESOKDj27tbzSuSvkv2jEhISMUhoSrausfx+3+zl+sodN+j6gXULySzTy
# CK4snU+sYRMtHgIp3KIRcgjutlv/RxsTeKBdYAbgazBAQBcTMDy2AzBBFdNakimo
# erDs/QYwXcdX7hGFZ8iFDzNeXHltlk8J71Bw/PJL6UkhIisl/5lkXr15NW5aAJwt
# JqzrSOuJLvwxgiQL0sPbtSVABPLZgaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxMTJaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCudTXBoG/bN5KOouMvNRZ8UVQYmG3jQsnM
# QPZTxgv301uIMWoDKaffba2kX6gnZJowgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIATLemQsT8iWt6mNEAUQNabLUe0WF2vUBGb9xZkHXn9ZZgi0Em
# R6jzLGehdL4yNeuKxULdjWtswLy3mwq7RUU3l2+PnlwA7rn2MQJMwHUXHI6b9HXk
# LmGAiPyiyJF/qlzPxmHUXFlf+ZxctajY23wx5HEn6UNvfC9Z3tMFKuZ7Hnbmzzgl
# WkIyg3Em/7H8xfhcADJ0GuatKKY5QzKGlFqweNlZjLjHPq0c2Cg/+K+W8mNpNEZT
# V4WQP8dMg1i7OJligNjO9+xFiuC6/TNA4JalBhj83Umqs/mZfUTtMxywOSCC/kpB
# i1TQ5xk+Ip1L8/yxr9QM44YhMa5hu8IduNYg14c5M9j9bQLdv3sh5fnhKZUpINZF
# pcmbdmWSQOSVLj936VjDfEvyayLwGK6Nh87Xs9ITrXuidY7DB2zg4fkCwNOGgnMa
# W7Ll2FiePtaqf6M0i9tBP9vqW2lHeralzwKW9THblZ8rwX1zomAi8dHGQg6hQYpz
# Sc89mB7ReNMG0GtXMfRFCkhwPfI7oFJe6/iLgB9PQ2n3E76Q0nRQTa4yAVviL6aw
# 8P4uobee596ZCqFyzBjMG/EHGLX1Qc+u7NNm94ascAASs9Y/PYXchlUD/+9BKyG/
# XGbfLiEW8ehH328wcH8XVuh4UL847ItB23aWq5u3ECCAOkmIK55aCzaIkq8=
# SIG # End signature block