Public/PrivilegedAccess/Get-EntraOpsPrivilegedEAMAzure.ps1

<#
.SYNOPSIS
    Get a list in EntraOps EAM schema of all privileged principals with Azure RBAC role assignments and their classifications.

.DESCRIPTION
    Collects Azure RBAC role assignments via Get-EntraOpsPrivilegedAzureRoles, fetches role definition
    actions from the Azure Resource Graph, then classifies each unique (RoleDefinitionId, RoleAssignmentScopeId)
    pair against Classification_Azure.json.

    Scope matching uses PowerShell -like wildcard patterns (e.g. "/*" matches any ARM path).
    Action matching uses bidirectional wildcard matching so that a role with actions ["*"] correctly
    matches specific classification actions, and classification patterns like
    "Microsoft.Authorization/*/write" correctly match a role action
    "Microsoft.Authorization/roleAssignments/write". notActions exclusion is applied so that
    Contributor (actions=["*"], notActions=["Microsoft.Authorization/*/Write",...]) is not
    classified as having authorization write permissions.

    Run Update-EntraOpsClassificationControlPlaneScope with -ClassificationParameterScope including
    "Azure" before calling this function to get tenant-specific Tier0/Tier1 scope parameterization.

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

.PARAMETER FolderClassification
    Folder path to the classification definition files. Default is $DefaultFolderClassification.

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

.PARAMETER GlobalExclusion
    Apply the global exclusion list. Default is $true.

.PARAMETER EnableParallelProcessing
    Enable parallel processing for object resolution. Default is $true.

.PARAMETER ParallelThrottleLimit
    Maximum number of parallel threads. Default is 10.

.PARAMETER ControlPlaneScopeOnly
    Restrict the Azure RBAC role assignment scan to only the Control Plane ARM hierarchies defined as
    RoleAssignmentScopeName under the ControlPlane (EAMTierLevelTagValue "0") tier in Classification_Azure.json.
    The tenant-specific Classification/<TenantName>/Classification_Azure.json is used when it exists, otherwise
    the template is used. Wildcard entries ("/" and "/*") are ignored so only concrete management group,
    subscription, resource group or resource hierarchies limit the scan. Run
    Update-EntraOpsClassificationControlPlaneScope with -ClassificationParameterScope including "Azure" first
    to populate tenant-specific Tier0 scopes. Default is $false.

.PARAMETER IncludeJustification
    Include the Justification property (documenting a manual classification overwrite) on all Classification
    entries of the returned objects. Default is $false, so the property is not present in the output at all.

.PARAMETER UnresolvedRoleDefinitionFallbackTier
    Tier assigned to role assignments whose role definition could not be resolved from Azure Resource Graph
    (e.g. ARG replication lag for a new custom role, deleted role definition or a transient ARG failure).
    The fallback classification is marked with TaggedBy "FallbackUnresolvedRoleDefinition" and a warning is
    added to the summary for each affected (RoleDefinitionId, Scope) pair. Allowed values: "Unclassified"
    (default, consistent with the Unclassified fallback of the other RBAC systems), "ManagementPlane" or
    "ControlPlane" (stricter fail-closed tiers with Service "Unresolved Role Definition") or "None"
    (skip classification, previous behavior). Can be set via
    AzureRbacClassification.UnresolvedRoleDefinitionFallbackTier in EntraOpsConfig.json.

.PARAMETER DeletedPrincipalAssignmentHandling
    Controls Azure RBAC role assignments whose principal is confirmed deleted. "Filter" (default)
    removes those assignments from EAM output. "Keep" preserves the fail-closed unresolved object
    only after both the Microsoft Graph directory-object and user lookups return HTTP 404. Permission,
    throttling, network and other resolution failures remain visible. Can be set via
    AzureRbacClassification.DeletedPrincipalAssignmentHandling in EntraOpsConfig.json.

.PARAMETER IncludeObjectDetails
    Include descriptive object details in warning output. Defaults to ConsoleOutput.IncludeObjectDetails from
    EntraOpsConfig.json. Object IDs are always shown.

.EXAMPLE
    Get-EntraOpsPrivilegedEAMAzure -TenantId "00000000-0000-0000-0000-000000000000"

.EXAMPLE
    Scan only Control Plane hierarchies defined in Classification_Azure.json instead of the whole tenant.
    Get-EntraOpsPrivilegedEAMAzure -ControlPlaneScopeOnly $true
#>


function Get-EntraOpsPrivilegedEAMAzure {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $false)]
        [System.String]$TenantId = (Get-EntraOpsAzContextValue -Property TenantId)
        ,
        [Parameter(Mandatory = $false)]
        [System.String]$FolderClassification = "$DefaultFolderClassification"
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$SampleMode = $False
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$GlobalExclusion = $true
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$EnableParallelProcessing = $true
        ,
        [Parameter(Mandatory = $false)]
        [System.Int32]$ParallelThrottleLimit = 10
        ,
        [Parameter(Mandatory = $false)]
        [switch]$IncludeJustification
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$ControlPlaneScopeOnly = $false
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$ClassifyConstrainedDelegationAlwaysAsControlPlane = $(if ($null -ne $EntraOpsConfig.AzureRbacClassification.ClassifyConstrainedDelegationAlwaysAsControlPlane) { [System.Convert]::ToBoolean($EntraOpsConfig.AzureRbacClassification.ClassifyConstrainedDelegationAlwaysAsControlPlane) } else { $false })
        ,
        [Parameter(Mandatory = $false)]
        [ValidateSet('Unclassified', 'ManagementPlane', 'ControlPlane', 'None')]
        [System.String]$UnresolvedRoleDefinitionFallbackTier = $(if (-not [string]::IsNullOrEmpty($EntraOpsConfig.AzureRbacClassification.UnresolvedRoleDefinitionFallbackTier)) { $EntraOpsConfig.AzureRbacClassification.UnresolvedRoleDefinitionFallbackTier } else { 'Unclassified' })
        ,
        [Parameter(Mandatory = $false)]
        [ValidateSet('Keep', 'Filter')]
        [System.String]$DeletedPrincipalAssignmentHandling = $(if (-not [string]::IsNullOrEmpty($EntraOpsConfig.AzureRbacClassification.DeletedPrincipalAssignmentHandling)) { $EntraOpsConfig.AzureRbacClassification.DeletedPrincipalAssignmentHandling } else { 'Filter' })
        ,
        [Parameter(Mandatory = $false)]
        [System.Boolean]$IncludeObjectDetails = [bool]$Global:EntraOpsIncludeObjectDetails
    )

    $WarningMessages = New-Object -TypeName "System.Collections.Generic.List[psobject]"
    $Stage1Start = Get-Date

    #region Stage 1: Fetch Azure RBAC Role Assignments
    Write-Host ""
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " Stage 1/4: Fetching Azure RBAC Role Assignments" -ForegroundColor Cyan
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "Retrieving Azure RBAC role assignments across tenant scope..." -ForegroundColor Gray
    Write-Progress -Activity "Stage 1/4: Fetching Azure RBAC Roles" -Status "Loading role assignments and global exclusions..." -PercentComplete 10

    # Optionally restrict the Azure RBAC scan to Control Plane hierarchies defined in the classification file
    $ControlPlaneScopeFilterPatterns = @()
    if ($ControlPlaneScopeOnly -eq $true) {
        $AzureClassificationFilterPath = Resolve-EntraOpsClassificationPath -ClassificationFileName "Classification_Azure.json" -FolderClassification $FolderClassification
        if ($null -eq $AzureClassificationFilterPath) {
            $WarningMessages.Add([PSCustomObject]@{ Type = "Stage1"; Message = "ControlPlaneScopeOnly enabled but Classification_Azure.json could not be resolved; scanning full tenant scope." })
        } else {
            try {
                $AzureClassificationForFilter = Get-Content -Path $AzureClassificationFilterPath -Raw | ConvertFrom-Json -Depth 10
                $ControlPlaneTier = $AzureClassificationForFilter | Where-Object { "$($_.EAMTierLevelTagValue)" -eq "0" }
                $ControlPlaneScopeFilterPatterns = @($ControlPlaneTier.TierLevelDefinition.RoleAssignmentScopeName |
                    Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -ne "/" -and $_ -ne "/*" } |
                    Select-Object -Unique)
                Write-Host "Control Plane scope filter: $($ControlPlaneScopeFilterPatterns.Count) hierarchy scope(s) from $([System.IO.Path]::GetFileName($AzureClassificationFilterPath))." -ForegroundColor Gray
                if ($ControlPlaneScopeFilterPatterns.Count -eq 0) {
                    $WarningMessages.Add([PSCustomObject]@{ Type = "Stage1"; Message = "ControlPlaneScopeOnly enabled but no specific Control Plane RoleAssignmentScopeName hierarchies found (only '/' or '/*'); scanning full tenant scope." })
                }
            } catch {
                $WarningMessages.Add([PSCustomObject]@{ Type = "Stage1"; Message = "Failed to load Control Plane scope filter from classification file: $($_.Exception.Message); scanning full tenant scope." })
            }
        }
    }

    if ($SampleMode -ne $True) {
        $AzureRbacAssignments = Get-EntraOpsPrivilegedAzureRoles -TenantId $TenantId -ControlPlaneScopeFilter $ControlPlaneScopeFilterPatterns -WarningMessages $WarningMessages
    } else {
        $WarningMessages.Add([PSCustomObject]@{ Type = "Stage1"; Message = "SampleMode is not supported for Azure RBAC." })
        $AzureRbacAssignments = @()
    }

    #region Resolve ForeignGroup principals (cross-tenant Azure RBAC) via remoteTenantGroups
    # Azure RBAC assignments to a group from another tenant are returned with principalType 'ForeignGroup'
    # (ObjectType 'foreigngroup'). These have no directory object in the home tenant, so resolve them via
    # remoteTenantGroups to obtain the real remote group id, display name and owning tenant. The matching
    # assignments are re-keyed to the remote group identity so the rest of the pipeline (object resolution,
    # classification grouping) treats them as ordinary groups. When the remote tenant matches the configured
    # managing tenant, full details are resolved later in Stage 4b via Invoke-EntraOpsCrossTenantObjectResolution.
    $ForeignGroupDetailsCache = @{}
    $ForeignGroupAssignments = @($AzureRbacAssignments | Where-Object { "$($_.ObjectType)" -ieq 'foreigngroup' })
    if ($ForeignGroupAssignments.Count -gt 0) {
        Write-Host "Resolving $($ForeignGroupAssignments.Count) ForeignGroup role assignment(s) via remoteTenantGroups..." -ForegroundColor Gray
        $ForeignGroupResolution = @{}
        foreach ($StubObjectId in @($ForeignGroupAssignments.ObjectId | Select-Object -Unique)) {
            $ResolvedForeignGroup = Get-EntraOpsPrivilegedEntraObject -AadObjectId $StubObjectId -TenantId $TenantId
            if ($null -ne $ResolvedForeignGroup -and "$($ResolvedForeignGroup.ObjectType)" -ieq 'group' -and -not [string]::IsNullOrEmpty($ResolvedForeignGroup.ObjectId)) {
                $ForeignGroupResolution[$StubObjectId] = $ResolvedForeignGroup
                $ForeignGroupDetailsCache[$ResolvedForeignGroup.ObjectId] = $ResolvedForeignGroup
            } else {
                $WarningMessages.Add([PSCustomObject]@{ Type = "Stage1-ForeignGroup"; Message = "ForeignGroup $StubObjectId could not be resolved via remoteTenantGroups." })
            }
        }
        foreach ($ForeignGroupAssignment in $ForeignGroupAssignments) {
            $ResolvedForeignGroup = $ForeignGroupResolution[$ForeignGroupAssignment.ObjectId]
            if ($null -ne $ResolvedForeignGroup) {
                $ForeignGroupAssignment.ObjectId = $ResolvedForeignGroup.ObjectId
                $ForeignGroupAssignment.ObjectTenantId = $ResolvedForeignGroup.ObjectTenantId
                $ForeignGroupAssignment.ObjectType = 'group'
            }
        }
    }
    #endregion

    $GlobalExclusionList = Import-EntraOpsGlobalExclusions -Enabled $GlobalExclusion

    $Stage1Duration = ((Get-Date) - $Stage1Start).TotalSeconds
    Write-Host "✓ Stage 1 completed in $([Math]::Round($Stage1Duration, 2)) seconds ($(@($AzureRbacAssignments).Count) role assignments retrieved)" -ForegroundColor Green
    Write-Progress -Activity "Stage 1/4: Fetching Azure RBAC Roles" -Completed
    #endregion

    if ($null -eq $AzureRbacAssignments -or @($AzureRbacAssignments).Count -eq 0) {
        Write-Warning "No Azure RBAC role assignments found. Returning empty result."
        return @()
    }

    #region Stage 2: Load Classification File and Classify by Role Definition and Scope
    $Stage2Start = Get-Date
    Write-Host ""
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " Stage 2/4: Loading Classification Rules and Classifying Roles" -ForegroundColor Cyan
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "Loading Azure RBAC classification file and matching role definitions..." -ForegroundColor Gray
    Write-Progress -Activity "Stage 2/4: Loading Classification Rules" -Status "Reading classification file..." -PercentComplete 25

    $AzureClassificationFilePath = Resolve-EntraOpsClassificationPath -ClassificationFileName "Classification_Azure.json" -FolderClassification $FolderClassification
    if ($null -eq $AzureClassificationFilePath) {
        Write-Error "Could not resolve Classification_Azure.json. Run Update-EntraOpsClassificationFiles first."
        return @()
    }
    $AzureResourcesByClassificationJSON = Expand-EntraOpsPrivilegedEAMJsonFile -FilePath $AzureClassificationFilePath |
    Select-Object EAMTierLevelName, EAMTierLevelTagValue, Category, Service, ActionType, RoleAssignmentScopeName, ExcludedRoleAssignmentScopeName, RoleDefinitionActions, ExcludedRoleDefinitionActions

    # Fetch role definitions from Azure Resource Graph for all referenced role definition GUIDs.
    # Results are deduped by name (GUID) since the same built-in role can appear at multiple scopes.
    $RoleDefinitionCache = @{}
    $UniqueRoleDefIds = @($AzureRbacAssignments | ForEach-Object { $_.RoleDefinitionId } | Where-Object { -not [string]::IsNullOrEmpty($_) } | Select-Object -Unique)

    if ($SampleMode -ne $True -and $UniqueRoleDefIds.Count -gt 0) {
        Write-Host "Fetching $($UniqueRoleDefIds.Count) unique role definition(s) from Azure Resource Graph..." -ForegroundColor Gray

        # Batch ARG queries in groups of 100 to stay within query length limits
        $ArgBatchSize = 100
        for ($BatchStart = 0; $BatchStart -lt $UniqueRoleDefIds.Count; $BatchStart += $ArgBatchSize) {
            $BatchEnd = [Math]::Min($BatchStart + $ArgBatchSize - 1, $UniqueRoleDefIds.Count - 1)
            $BatchIds = @($UniqueRoleDefIds[$BatchStart..$BatchEnd])
            $RoleDefIdFilter = ($BatchIds | ForEach-Object { "'$_'" }) -join ', '
            $RoleDefKql = "authorizationresources | where type =~ 'microsoft.authorization/roledefinitions' | where name in ($RoleDefIdFilter) | project name, properties"
            try {
                # -ThrowOnFailure makes the catch below reachable. Without it the helper reports failure
                # with a non-terminating error and returns whatever it collected, so a throttled batch
                # silently left role definitions unresolved and no warning was recorded.
                $BatchResults = @(Invoke-EntraOpsAzGraphQuery -KqlQuery $RoleDefKql -ThrowOnFailure)
                foreach ($RoleDef in $BatchResults) {
                    if (-not [string]::IsNullOrEmpty($RoleDef.name) -and -not $RoleDefinitionCache.ContainsKey($RoleDef.name)) {
                        $RoleDefinitionCache[$RoleDef.name] = $RoleDef
                    }
                }
            } catch {
                $WarningMessage = "Failed to fetch role definitions batch (start=$BatchStart): $($_.Exception.Message)"
                $WarningMessages.Add([PSCustomObject]@{ Type = "Stage2"; Message = $WarningMessage })
                Write-Warning $WarningMessage
            }
        }
        Write-Host "Resolved $($RoleDefinitionCache.Count) of $($UniqueRoleDefIds.Count) role definition(s) from Azure Resource Graph." -ForegroundColor Gray

        # Guardrail: a high unresolved ratio points to an ARG outage rather than individual stale role
        # definitions, so fallback classifications from this run should not be trusted blindly.
        $UnresolvedRoleDefIds = @($UniqueRoleDefIds | Where-Object { -not $RoleDefinitionCache.ContainsKey($_) })
        if ($UnresolvedRoleDefIds.Count -gt 0 -and ($UnresolvedRoleDefIds.Count / $UniqueRoleDefIds.Count) -gt 0.2) {
            $WarningMessages.Add([PSCustomObject]@{
                    Type    = "Stage2"
                    Message = "$($UnresolvedRoleDefIds.Count) of $($UniqueRoleDefIds.Count) role definitions could not be resolved from Azure Resource Graph. A transient ARG outage is suspected; fallback classifications of this run may be unreliable."
                })
        }
    }

    # Resolve tier tag and service of the configured unresolved-role-definition fallback.
    # "Unclassified" mirrors the assignment-level Unclassified fallback used by the other RBAC systems.
    $FallbackTierTagValue = $null
    $FallbackServiceName = $null
    if ($UnresolvedRoleDefinitionFallbackTier -eq 'Unclassified') {
        $FallbackTierTagValue = 'Unclassified'
        $FallbackServiceName = 'Unclassified'
    } elseif ($UnresolvedRoleDefinitionFallbackTier -ne 'None') {
        $FallbackTierTagValue = @($AzureResourcesByClassificationJSON | Where-Object { $_.EAMTierLevelName -eq $UnresolvedRoleDefinitionFallbackTier })[0].EAMTierLevelTagValue
        if ([string]::IsNullOrEmpty("$FallbackTierTagValue")) {
            $FallbackTierTagValue = if ($UnresolvedRoleDefinitionFallbackTier -eq 'ControlPlane') { '0' } else { '1' }
        }
        $FallbackServiceName = 'Unresolved Role Definition'
    }

    # Classify each unique (RoleDefinitionId, RoleAssignmentScopeId) combination.
    # For each matching scope+action pair the result is one classification entry with all matched actions.
    $AzureRbacClassificationsByJSON = @()
    # Select-Object -Unique compares case-SENSITIVELY, and ARM does not guarantee consistent casing of
    # role definition GUIDs or scope ids across endpoints, so the same logical pair could survive dedup
    # twice and be classified twice. Group on a lowercased key and keep the first member of each group.
    $UniqueAssignmentPairs = @(
        $AzureRbacAssignments |
            Group-Object -Property { "$($_.RoleDefinitionId)".ToLowerInvariant() + '|' + "$($_.RoleAssignmentScopeId)".ToLowerInvariant() } |
            ForEach-Object { $_.Group[0] | Select-Object RoleDefinitionId, RoleAssignmentScopeId }
    )
    $PairsProcessed = 0
    $PairsTotal = $UniqueAssignmentPairs.Count

    $AzureRbacClassificationsByJSON += foreach ($AzureRbacAssignment in $UniqueAssignmentPairs) {
        $PairsProcessed++
        if ($PairsProcessed % 20 -eq 0 -or $PairsProcessed -eq $PairsTotal) {
            Write-Progress -Activity "Stage 2/4: Loading Classification Rules" -Status "Classifying assignment $PairsProcessed of $PairsTotal" -PercentComplete (25 + ($PairsProcessed / [Math]::Max($PairsTotal, 1) * 25))
        }

        $RoleAssignmentScopeId = $AzureRbacAssignment.RoleAssignmentScopeId
        $RoleDefinitionId = $AzureRbacAssignment.RoleDefinitionId

        $RoleDef = $RoleDefinitionCache[$RoleDefinitionId]
        if ($null -eq $RoleDef) {
            if ($UnresolvedRoleDefinitionFallbackTier -eq 'None') {
                $WarningMessages.Add([PSCustomObject]@{
                        Type    = "Stage2"
                        Message = "Role definition '$RoleDefinitionId' was not found in Azure Resource Graph; classification skipped for scope '$RoleAssignmentScopeId'."
                    })
                continue
            }
            # Emit a self-identifying fallback classification so the assignment stays visible instead of
            # silently losing classification when the role definition cannot be resolved.
            $WarningMessages.Add([PSCustomObject]@{
                    Type    = "Stage2-UnresolvedFallback"
                    Message = "Role definition '$RoleDefinitionId' was not found in Azure Resource Graph; assignment at scope '$RoleAssignmentScopeId' classified with $UnresolvedRoleDefinitionFallbackTier fallback."
                })
            [PSCustomObject]@{
                'RoleDefinitionId'      = $RoleDefinitionId
                'RoleAssignmentScopeId' = $RoleAssignmentScopeId
                'Classification'        = @([PSCustomObject]@{
                        'AdminTierLevel'             = "$FallbackTierTagValue"
                        'AdminTierLevelName'         = $UnresolvedRoleDefinitionFallbackTier
                        'Service'                    = $FallbackServiceName
                        'MatchedActions'             = $null
                        'ScopedObjects'              = $null
                        'TaggedBy'                   = "FallbackUnresolvedRoleDefinition"
                        'TaggedByObjectIds'          = $null
                        'TaggedByObjectDisplayNames' = $null
                        'TaggedByRoleSystem'         = "Azure"
                    })
            }
            continue
        }

        # --- Scope matching ---
        # Find classification entries whose RoleAssignmentScopeName pattern matches the assignment scope.
        $MatchedClassificationByScope = @()
        $MatchedClassificationByScope += foreach ($ClassEntry in $AzureResourcesByClassificationJSON) {
            if (-not ($RoleAssignmentScopeId -like $ClassEntry.RoleAssignmentScopeName)) { continue }

            # Check scope exclusions (one-directional: assignment scope -like exclusion pattern)
            $IsExcluded = $false
            foreach ($ExcludedScope in @($ClassEntry.ExcludedRoleAssignmentScopeName)) {
                if (-not [string]::IsNullOrEmpty($ExcludedScope) -and ($RoleAssignmentScopeId -like $ExcludedScope)) {
                    $IsExcluded = $true
                    break
                }
            }
            if (-not $IsExcluded) { $ClassEntry }
        }

        if ($MatchedClassificationByScope.Count -eq 0) { continue }

        # --- Action matching ---
        # Bidirectional wildcard: ($roleAction -like $classAction) handles specific role actions matching
        # a wildcard classification pattern (e.g. "Microsoft.Authorization/roleAssignments/write" -like
        # "Microsoft.Authorization/*/write"). ($classAction -like $roleAction) handles a broad role action
        # (e.g. "*") matching any specific classification action.
        # notActions exclusion: if the classification action is covered by a notAction, skip it
        # (e.g. Contributor's notActions prevent it from matching authorization write patterns).
        $ClassifiedWithMatchedActions = @()
        foreach ($ClassEntry in $MatchedClassificationByScope) {
            $IsDataActionRule = "$($ClassEntry.ActionType)" -ieq 'DataAction'
            foreach ($ClassAction in @($ClassEntry.RoleDefinitionActions)) {
                if ([string]::IsNullOrEmpty($ClassAction)) { continue }

                $ActionMatched = $false
                foreach ($Permission in @($RoleDef.properties.permissions)) {
                    if ($ActionMatched) { break }

                    if (-not $IsDataActionRule) {
                        # Management/control plane Actions must not match data-plane rules.
                        foreach ($RoleAction in @($Permission.actions)) {
                            if ([string]::IsNullOrEmpty($RoleAction)) { continue }
                            # Azure documents */read as control-plane metadata access. It must not
                            # satisfy an explicit telemetry or other sensitive-read classification.
                            if ($RoleAction -eq '*/read' -and $ClassAction -ne '*/read') { continue }
                            if ($RoleAction -like $ClassAction -or $ClassAction -like $RoleAction) {
                                $IsExcludedByClassification = @($ClassEntry.ExcludedRoleDefinitionActions | Where-Object {
                                        -not [string]::IsNullOrEmpty($_) -and $RoleAction -like $_
                                    }).Count -gt 0
                                if ($IsExcludedByClassification) { continue }
                                # Verify the classification action is not excluded by a notAction
                                $IsNotActioned = $false
                                foreach ($NotAction in @($Permission.notActions)) {
                                    if (-not [string]::IsNullOrEmpty($NotAction) -and ($ClassAction -like $NotAction)) {
                                        $IsNotActioned = $true
                                        break
                                    }
                                }
                                if (-not $IsNotActioned) {
                                    $ActionMatched = $true
                                    break
                                }
                            }
                        }
                    }

                    if ($ActionMatched) { break }

                    if ($IsDataActionRule) {
                        # DataActions grant access to resource data and must not match Actions rules.
                        foreach ($DataAction in @($Permission.dataActions)) {
                            if ([string]::IsNullOrEmpty($DataAction)) { continue }
                            if ($DataAction -like $ClassAction -or $ClassAction -like $DataAction) {
                                $IsExcludedByClassification = @($ClassEntry.ExcludedRoleDefinitionActions | Where-Object {
                                        -not [string]::IsNullOrEmpty($_) -and $DataAction -like $_
                                    }).Count -gt 0
                                if ($IsExcludedByClassification) { continue }
                                $IsNotDataActioned = $false
                                foreach ($NotDataAction in @($Permission.notDataActions)) {
                                    if (-not [string]::IsNullOrEmpty($NotDataAction) -and ($ClassAction -like $NotDataAction)) {
                                        $IsNotDataActioned = $true
                                        break
                                    }
                                }
                                if (-not $IsNotDataActioned) {
                                    $ActionMatched = $true
                                    break
                                }
                            }
                        }
                    }
                }

                if ($ActionMatched) {
                    $ClassifiedWithMatchedActions += [PSCustomObject]@{
                        EAMTierLevelName     = $ClassEntry.EAMTierLevelName
                        EAMTierLevelTagValue = $ClassEntry.EAMTierLevelTagValue
                        Service              = $ClassEntry.Service
                        MatchedAction        = $ClassAction
                    }
                }
            }
        }

        if ($ClassifiedWithMatchedActions.Count -gt 0) {
            $UniqueClassifications = $ClassifiedWithMatchedActions | Select-Object -Unique EAMTierLevelName, EAMTierLevelTagValue, Service

            $Classification = foreach ($UniqueClass in $UniqueClassifications) {
                [array]$MatchedActions = @($ClassifiedWithMatchedActions | Where-Object {
                        $_.EAMTierLevelName -eq $UniqueClass.EAMTierLevelName -and
                        $_.EAMTierLevelTagValue -eq $UniqueClass.EAMTierLevelTagValue -and
                        $_.Service -eq $UniqueClass.Service
                    } | ForEach-Object { $_.MatchedAction } | Select-Object -Unique)

                [PSCustomObject]@{
                    'AdminTierLevel'             = $UniqueClass.EAMTierLevelTagValue
                    'AdminTierLevelName'         = $UniqueClass.EAMTierLevelName
                    'Service'                    = $UniqueClass.Service
                    'MatchedActions'             = if ($MatchedActions.Count -gt 0) { , @($MatchedActions) } else { $null }
                    'ScopedObjects'              = $null
                    'TaggedBy'                   = "JSONwithAction"
                    'TaggedByObjectIds'          = $null
                    'TaggedByObjectDisplayNames' = $null
                    'TaggedByRoleSystem'         = "Azure"
                }
            }

            [PSCustomObject]@{
                'RoleDefinitionId'      = $RoleDefinitionId
                'RoleAssignmentScopeId' = $RoleAssignmentScopeId
                'Classification'        = $Classification
            }
        }
    }

    $Stage2Duration = ((Get-Date) - $Stage2Start).TotalSeconds
    Write-Host "✓ Stage 2 completed in $([Math]::Round($Stage2Duration, 2)) seconds ($($AzureRbacClassificationsByJSON.Count) classified (RoleDefinitionId, Scope) pairs)" -ForegroundColor Green
    Write-Progress -Activity "Stage 2/4: Loading Classification Rules" -Completed
    #endregion

    #region Stage 2b: Resolve role definitions referenced by constrained-delegation conditions
    # Build a tier tag -> tier name lookup (ordered ascending; 0 = most privileged) used to downgrade
    # condition-based (ABAC) constrained-delegation Authorization classifications.
    $TierNameByTag = @{}
    foreach ($ClassEntry in $AzureResourcesByClassificationJSON) {
        if ($null -ne $ClassEntry.EAMTierLevelTagValue -and -not $TierNameByTag.ContainsKey("$($ClassEntry.EAMTierLevelTagValue)")) {
            $TierNameByTag["$($ClassEntry.EAMTierLevelTagValue)"] = $ClassEntry.EAMTierLevelName
        }
    }

    # Collect RoleDefinitionId GUIDs referenced inside RoleAssignmentCondition (constrained delegation) so we
    # can classify which roles the delegate may/may not grant. Fetch any that are not already cached.
    $ConditionRoleDefGuidPattern = "RoleDefinitionId\]\s*\w+:Guid(?:Not)?Equals\s*\{([^}]*)\}"
    $ReferencedRoleDefIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($AzureRbacAssignment in $AzureRbacAssignments) {
        if ([string]::IsNullOrEmpty($AzureRbacAssignment.RoleAssignmentCondition)) { continue }
        foreach ($Match in [regex]::Matches($AzureRbacAssignment.RoleAssignmentCondition, $ConditionRoleDefGuidPattern)) {
            foreach ($Guid in ($Match.Groups[1].Value -split ',')) {
                $TrimmedGuid = $Guid.Trim()
                if (-not [string]::IsNullOrEmpty($TrimmedGuid)) { [void]$ReferencedRoleDefIds.Add($TrimmedGuid) }
            }
        }
    }

    # Built-in constrained-delegation roles (e.g. Key Vault Data Access Administrator) carry their ABAC
    # condition on the role definition's permissions instead of the assignment. Track those role definitions
    # so Stage 3 also invokes the downgrade helper for them, and cache the roles their conditions reference.
    $RoleDefIdsWithPermissionCondition = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($CachedRoleDefId in @($RoleDefinitionCache.Keys)) {
        foreach ($Permission in @($RoleDefinitionCache[$CachedRoleDefId].properties.permissions)) {
            if ([string]::IsNullOrEmpty($Permission.condition)) { continue }
            [void]$RoleDefIdsWithPermissionCondition.Add($CachedRoleDefId)
            foreach ($Match in [regex]::Matches($Permission.condition, $ConditionRoleDefGuidPattern)) {
                foreach ($Guid in ($Match.Groups[1].Value -split ',')) {
                    $TrimmedGuid = $Guid.Trim()
                    if (-not [string]::IsNullOrEmpty($TrimmedGuid)) { [void]$ReferencedRoleDefIds.Add($TrimmedGuid) }
                }
            }
        }
    }
    $MissingConditionRoleDefIds = @($ReferencedRoleDefIds | Where-Object { -not $RoleDefinitionCache.ContainsKey($_) })
    if ($SampleMode -ne $True -and $MissingConditionRoleDefIds.Count -gt 0) {
        Write-Host "Fetching $($MissingConditionRoleDefIds.Count) role definition(s) referenced by delegation conditions..." -ForegroundColor Gray
        for ($BatchStart = 0; $BatchStart -lt $MissingConditionRoleDefIds.Count; $BatchStart += 100) {
            $BatchEnd = [Math]::Min($BatchStart + 99, $MissingConditionRoleDefIds.Count - 1)
            $BatchIds = @($MissingConditionRoleDefIds[$BatchStart..$BatchEnd])
            $RoleDefIdFilter = ($BatchIds | ForEach-Object { "'$_'" }) -join ', '
            $RoleDefKql = "authorizationresources | where type =~ 'microsoft.authorization/roledefinitions' | where name in ($RoleDefIdFilter) | project name, properties"
            try {
                foreach ($RoleDef in @(Invoke-EntraOpsAzGraphQuery -KqlQuery $RoleDefKql -ThrowOnFailure)) {
                    if (-not [string]::IsNullOrEmpty($RoleDef.name) -and -not $RoleDefinitionCache.ContainsKey($RoleDef.name)) {
                        $RoleDefinitionCache[$RoleDef.name] = $RoleDef
                    }
                }
            } catch {
                $WarningMessages.Add([PSCustomObject]@{ Type = "Stage2b"; Message = "Failed to fetch condition role definitions batch (start=$BatchStart): $($_.Exception.Message)" })
            }
        }
    }
    #endregion

    #region Stage 3: Apply Classifications to All Role Assignments
    $Stage3Start = Get-Date
    Write-Host ""
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " Stage 3/4: Classifying Principals" -ForegroundColor Cyan
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "Matching principals against classified role assignments..." -ForegroundColor Gray
    Write-Progress -Activity "Stage 3/4: Classifying Principals" -Status "Processing role assignments..." -PercentComplete 50

    $ClassificationsByRoleAndScope = @{}
    foreach ($ClassifiedPair in $AzureRbacClassificationsByJSON) {
        $ClassificationKey = "$($ClassifiedPair.RoleDefinitionId)|$($ClassifiedPair.RoleAssignmentScopeId)"
        if (-not $ClassificationsByRoleAndScope.ContainsKey($ClassificationKey)) {
            $ClassificationsByRoleAndScope[$ClassificationKey] = [System.Collections.Generic.List[object]]::new()
        }
        foreach ($ClassItem in @($ClassifiedPair.Classification)) {
            $ClassificationsByRoleAndScope[$ClassificationKey].Add($ClassItem)
        }
    }

    $AzureRbacClassifications = foreach ($AzureRbacAssignment in $AzureRbacAssignments) {
        $AzureRbacAssignment = $AzureRbacAssignment | Select-Object -ExcludeProperty Classification
        $ClassificationKey = "$($AzureRbacAssignment.RoleDefinitionId)|$($AzureRbacAssignment.RoleAssignmentScopeId)"
        $Classification = @($ClassificationsByRoleAndScope[$ClassificationKey])
        $Classification = $Classification | Select-Object -Unique AdminTierLevel, AdminTierLevelName, MatchedActions, ScopedObjects, Service, TaggedBy, TaggedByObjectIds, TaggedByObjectDisplayNames, TaggedByRoleSystem |
        Sort-Object AdminTierLevel, AdminTierLevelName, Service, TaggedBy
        $AzureRbacAssignment | Add-Member -NotePropertyName "Classification" -NotePropertyValue $Classification -Force

        $AssignedRoleDefinitionForCondition = $RoleDefinitionCache["$($AzureRbacAssignment.RoleDefinitionId)"]
        $RoleDefinitionConditions = if ($null -eq $AssignedRoleDefinitionForCondition) {
            @()
        } else {
            @($AssignedRoleDefinitionForCondition.properties.permissions | Where-Object {
                    -not [string]::IsNullOrEmpty($_.condition)
                } | ForEach-Object {
                    [PSCustomObject]@{
                        Condition        = $_.condition
                        ConditionVersion = $_.conditionVersion
                    }
                })
        }
        $AzureRbacAssignment | Add-Member -NotePropertyName "RoleDefinitionConditions" -NotePropertyValue $RoleDefinitionConditions -Force

        $ConditionConstraints = @(
            @([PSCustomObject]@{
                    Source           = "Assignment"
                    Condition        = $AzureRbacAssignment.RoleAssignmentCondition
                    ConditionVersion = $AzureRbacAssignment.RoleAssignmentConditionVersion
                }) +
            @($RoleDefinitionConditions | ForEach-Object {
                    [PSCustomObject]@{
                        Source           = "RoleDefinition"
                        Condition        = $_.Condition
                        ConditionVersion = $_.ConditionVersion
                    }
                }) |
            Where-Object { -not [string]::IsNullOrEmpty($_.Condition) } |
            ForEach-Object {
                $ConditionSource = $_
                foreach ($Match in [regex]::Matches($_.Condition, "RoleDefinitionId\]\s*\w+:(Guid(?:Not)?Equals)\s*\{([^}]*)\}")) {
                    [PSCustomObject]@{
                        Source            = $ConditionSource.Source
                        Operator          = $Match.Groups[1].Value
                        RoleDefinitionIds = @($Match.Groups[2].Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrEmpty($_) })
                    }
                }
            }
        )

        $HasCondition = -not [string]::IsNullOrEmpty($AzureRbacAssignment.RoleAssignmentCondition) -or $RoleDefinitionConditions.Count -gt 0
        $HasControlPlaneAuthorization = @($Classification | Where-Object {
                "$($_.AdminTierLevel)" -eq "0" -and (
                    $_.Service -eq "Authorization" -or
                    (@($_.MatchedActions) | Where-Object {
                        -not [string]::IsNullOrEmpty($_) -and ($_ -like "*roleAssignments/write" -or $_ -like "*roleAssignments/delete")
                    }).Count -gt 0
                )
            }).Count -gt 0
        $ConditionEvaluation = if (-not $HasCondition) {
            $null
        } elseif ($ClassifyConstrainedDelegationAlwaysAsControlPlane) {
            [PSCustomObject]@{
                Status      = "RetainedControlPlaneByConfiguration"
                Summary     = "Conditional delegation evaluation is disabled by configuration; the assignment remains Control Plane."
                Constraints = $ConditionConstraints
            }
        } else {
            [PSCustomObject]@{
                Status      = "Pending"
                Summary     = "The condition was evaluated against delegable role definitions at the assignment scope."
                Constraints = $ConditionConstraints
            }
        }

        # Condition-based (ABAC) constrained delegation: if the role can write role assignments but a
        # condition (on the assignment or on the role definition's permissions) limits which roles it can
        # grant, downgrade the Authorization Control Plane classification based on the roles referenced in
        # the condition (classified against the same scope).
        # When ClassifyConstrainedDelegationAlwaysAsControlPlane is enabled (config flag), this dynamic downgrade is
        # skipped so constrained-delegation authorization assignments always remain classified as Control Plane.
        if (-not $ClassifyConstrainedDelegationAlwaysAsControlPlane -and (
                -not [string]::IsNullOrEmpty($AzureRbacAssignment.RoleAssignmentCondition) -or
                $RoleDefIdsWithPermissionCondition.Contains("$($AzureRbacAssignment.RoleDefinitionId)")
            )) {
            $AzureRbacAssignment.Classification = @(Resolve-EntraOpsAzureConstrainedDelegationTier `
                    -Assignment $AzureRbacAssignment `
                    -ClassificationDefinitions $AzureResourcesByClassificationJSON `
                    -RoleDefinitionCache $RoleDefinitionCache `
                    -TierNameByTag $TierNameByTag)

            $DowngradedEntries = @($AzureRbacAssignment.Classification | Where-Object { $_.TaggedBy -eq "JSONwithConditionInScope" })
            if ($DowngradedEntries.Count -gt 0) {
                $ConditionEvaluation.Status = "DowngradedConstrainedActions"
                $ConditionEvaluation.Summary = "The condition limits role-assignment delegation; constrained actions were classified at $(@($DowngradedEntries.AdminTierLevelName | Select-Object -Unique) -join ', ')."
            } elseif (-not $HasControlPlaneAuthorization) {
                $ConditionEvaluation.Status = "AuthorizationNotClassifiedAtScope"
                $ConditionEvaluation.Summary = "No Control Plane Authorization classification matched this assignment scope, so there was nothing to downgrade. Verify that Classification_Azure.json covers the Authorization service at '$($AzureRbacAssignment.RoleAssignmentScopeId)' - otherwise the role-assignment delegation capability is not represented in this classification."
            } else {
                $ConditionEvaluation.Status = "RetainedControlPlane"
                $ConditionEvaluation.Summary = "The condition did not prove that every Control Plane capability is constrained, so the applicable classification remains Control Plane."
            }
        }
        $AzureRbacAssignment | Add-Member -NotePropertyName "ConditionEvaluation" -NotePropertyValue $ConditionEvaluation -Force
        $AzureRbacAssignment
    }

    # Apply role definition classification overwrites (down-/upgrade by RoleDefinitionId or RoleDefinitionName).
    # Applied after the constrained-delegation evaluation so an explicit operator pin always wins.
    $ClassificationOverwrites = Import-EntraOpsClassificationOverwrites -RbacSystem "Azure" -FolderClassification $FolderClassification
    if ($ClassificationOverwrites.RoleDefinitionOverwrites.Count -gt 0) {
        Write-Host "Applying $($ClassificationOverwrites.RoleDefinitionOverwrites.Count) role definition classification overwrite(s) from Classification_RoleDefinitionOverwrites.json..." -ForegroundColor Yellow
        $AzureRbacClassifications = @(Invoke-EntraOpsClassificationRoleOverwrite -RbacClassifications $AzureRbacClassifications -RoleDefinitionOverwrites $ClassificationOverwrites.RoleDefinitionOverwrites -RoleSystem "Azure")
    }

    $Stage3Duration = ((Get-Date) - $Stage3Start).TotalSeconds
    Write-Host "✓ Stage 3 completed in $([Math]::Round($Stage3Duration, 2)) seconds ($($AzureRbacClassifications.Count) role assignments classified)" -ForegroundColor Green
    Write-Progress -Activity "Stage 3/4: Classifying Principals" -Completed
    #endregion

    #region Stage 4: Resolve Object Details and Finalize Output
    $Stage4Start = Get-Date
    Write-Host ""
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " Stage 4/4: Resolving Object Details and Finalizing" -ForegroundColor Cyan
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "Enriching principals with detailed attributes and applying exclusions..." -ForegroundColor Gray
    Write-Progress -Activity "Stage 4/4: Resolving Objects" -Status "Resolving object details..." -PercentComplete 75

    $AzureRbacByObject = $AzureRbacClassifications | Group-Object ObjectId -AsHashTable -AsString
    # Case-insensitive dedup: a principal id returned with different casing by two ARM/Graph endpoints
    # (for example roleAssignmentScheduleInstances vs. the remoteTenantGroups re-keying in Stage 1)
    # would otherwise be resolved twice and emitted as two rows for the same object.
    $UniqueObjects = @(
        $AzureRbacAssignments |
            Where-Object { $null -ne $_.ObjectId } |
            Group-Object -Property { "$($_.ObjectId)".ToLowerInvariant() } |
            ForEach-Object { $_.Group[0] | Select-Object ObjectId, ObjectType }
    )

    $ObjectDetailsCache = Invoke-EntraOpsParallelObjectResolution `
        -UniqueObjects $UniqueObjects `
        -TenantId $TenantId `
        -EnableParallelProcessing $EnableParallelProcessing `
        -ParallelThrottleLimit $ParallelThrottleLimit

    #region Stage 4b: Cross-tenant resolution of objects living in the managing tenant
    # Objects that could not be resolved in the home tenant (e.g. ForeignGroups re-keyed to their remote group
    # identity in Stage 1) are resolved against the configured managing tenant, mirroring the Entra ID RBAC flow.
    # No-op when no managing tenant is configured or all objects were already resolved in the home tenant.
    $ObjectDetailsCache = Invoke-EntraOpsCrossTenantObjectResolution `
        -UniqueObjects $UniqueObjects `
        -ObjectDetailsCache $ObjectDetailsCache `
        -EnableParallelProcessing $EnableParallelProcessing `
        -ParallelThrottleLimit $ParallelThrottleLimit

    # Fallback: any ForeignGroup still unresolved (managing tenant not configured, remote tenant differs, or
    # not reachable) uses the remoteTenantGroups details captured in Stage 1 so it still appears with its
    # remote display name and role-assignable group subtype instead of being dropped.
    if ($ForeignGroupDetailsCache.Count -gt 0) {
        foreach ($RemoteGroupId in $ForeignGroupDetailsCache.Keys) {
            $ExistingDetails = $ObjectDetailsCache[$RemoteGroupId]
            if ($null -eq $ExistingDetails -or "$($ExistingDetails.ObjectType)" -ieq 'unknown') {
                $ObjectDetailsCache[$RemoteGroupId] = $ForeignGroupDetailsCache[$RemoteGroupId]
            }
        }
    }
    #endregion

    if ($DeletedPrincipalAssignmentHandling -eq 'Filter') {
        $ObjectsBeforeDeletedPrincipalFilter = @($UniqueObjects)
        $UniqueObjects = @(Select-EntraOpsAzureRbacPrincipal `
            -UniqueObjects $UniqueObjects `
            -ObjectDetailsCache $ObjectDetailsCache `
            -DeletedPrincipalAssignmentHandling $DeletedPrincipalAssignmentHandling)

        # O(1) cache lookup per principal instead of scanning the retained-id array per id
        # (O(N x M) in large tenants just to build this informational message).
        $FilteredDeletedPrincipalIds = @($ObjectsBeforeDeletedPrincipalFilter |
                Where-Object { $ObjectDetailsCache[$_.ObjectId].ResolutionStatus -eq 'NotFound' } |
                ForEach-Object { $_.ObjectId })
        if ($FilteredDeletedPrincipalIds.Count -gt 0) {
            $FilteredAssignmentCount = @($AzureRbacClassifications | Where-Object { $_.ObjectId -in $FilteredDeletedPrincipalIds }).Count
            $FilteredPrincipalMessage = "Filtered $FilteredAssignmentCount Azure RBAC role assignment(s) for $($FilteredDeletedPrincipalIds.Count) principal(s) confirmed deleted by Microsoft Graph: $($FilteredDeletedPrincipalIds -join ', ')"
            Write-Host $FilteredPrincipalMessage -ForegroundColor Yellow
        }
    }

    # Explicit short-circuit: after the deleted-principal filter (or in an empty-scope tenant) no
    # principals may remain. Invoke-EntraOpsEAMClassificationAggregation deliberately keeps a strict
    # mandatory-collection contract, so an empty set is handled here with an empty result instead.
    if (@($UniqueObjects).Count -eq 0) {
        Write-Host "No Azure RBAC principals remain for classification aggregation - producing an empty export." -ForegroundColor Yellow
        $AzureRbacClassifiedObjects = @()
    } else {
        $AzureRbacClassifiedObjects = Invoke-EntraOpsEAMClassificationAggregation `
            -UniqueObjects $UniqueObjects `
            -ObjectDetailsCache $ObjectDetailsCache `
            -RbacClassificationsByObject $AzureRbacByObject `
            -RoleSystem "Azure" `
            -EnableParallelProcessing $EnableParallelProcessing `
            -ParallelThrottleLimit $ParallelThrottleLimit `
            -WarningMessages $WarningMessages
    }

    Write-Progress -Activity "Stage 4/4: Resolving Objects" -Status "Applying global exclusions and sorting..." -PercentComplete 90
    $FilteredAzureObjects = $AzureRbacClassifiedObjects | Where-Object { $GlobalExclusionList -notcontains $_.ObjectId }

    $Stage4Duration = ((Get-Date) - $Stage4Start).TotalSeconds
    $TotalDuration = ((Get-Date) - $Stage1Start).TotalSeconds

    Write-Progress -Activity "Stage 4/4: Resolving Objects" -Completed
    Write-Host "✓ Stage 4 completed in $([Math]::Round($Stage4Duration, 2)) seconds ($($FilteredAzureObjects.Count) privileged objects after exclusions)" -ForegroundColor Green

    Show-EntraOpsWarningSummary -WarningMessages $WarningMessages -IncludeObjectDetails $IncludeObjectDetails

    Write-Host ""
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Green
    Write-Host " ✓ All Stages Completed Successfully" -ForegroundColor Green
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Green
    Write-Host "Total execution time: $([Math]::Round($TotalDuration, 2)) seconds" -ForegroundColor Gray
    Write-Host "Final result: $($FilteredAzureObjects.Count) privileged objects ready for export" -ForegroundColor Gray
    #endregion

    $FilteredAzureObjects | Where-Object { $null -ne $_.ObjectType -and $null -ne $_.ObjectId } | Set-EntraOpsEAMClassificationJustification -IncludeJustification:$IncludeJustification | Sort-Object ObjectAdminTierLevel, ObjectDisplayName
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAH7/e2+2ioKygB
# t1omsiK6Ppcq1RlCp+6GAkU9axBSlaCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIB9AGxj6SfbXID0Xi1UsBCc9XZ7JnVzfNR0jQmRe
# yFUaMA0GCSqGSIb3DQEBAQUABIIBgDoAOUQ6lZl8eDRVyxFxILZSlLOeicw3OhBR
# QsGCtlzM5wHVO7ncwgrZgOAjwqW1zbWKX4DDaL0Ylr9qma2xXdHJLo8nwjGnzhzF
# TiVN4vUG0edBTT0LCcBMv/eqIsrV5ZKayEmwQWkjtHsRHH/DUnuHPlAMS2hFBeLG
# OleRZdMoPx70+8qVB870DvDGU7SDXt3SUgar4oqeRAjEPDLkx9OTcz3cmlRm4Tzx
# b2EKU7RLFkcMh7vsiItdp4yxVYHDro3vUI06GYeRGiNvbDiG/oKNA+B+zH7933lH
# WC9mHPpct5el7C9mLfCAEsYADVCdzJcZAglrSs0iN/e4vpEMAsJ6U5StCkqR0ONB
# f/dq0mQjx+IQYTke3mnqqd9rTu/N5ZmckIOw53mjV2OlKUs4oI+tESH9/9TVGYcq
# R6KGk6PqUxrnXFoWWcsEUhc8t+WnC+MHkGGpcgiMLSVu8cqlfbJ0bzADgLKlcPO/
# G3lWfsPRqEnbaHe/qR5HeXZGTtu/UqGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxMTZaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDDt44LuRNq9EtM9yFlOpqeoOKCsujWYY9QS
# InddqfEMTP8TmxxOkBl6//nQzLTzTVswgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAkvsqJpJdW/JcLUTO6a1ydBBf10BIBjwT+3VwXPOcetqybdxX
# tZfNVJNpMp8LpC4V0aELUBzZqusi8AL5hpMO5RGeseCXdhXijxIh3ecxHmqb2aWp
# 9TRfaVWmH8Geih5mU1vjl8PSMIHP7t8/UIv2C4xM9MzW/fM8ndiCellMqreEXkqB
# qQ19tVaAaoncv+np1S8qkluEx7RhIVWp1CnKyRtA5rsGI1ops3BSHSttwymxIMvs
# 4F86fREWi8RqdmNfv9yTnTaubMtdIXbrN4D701DwdAQwJRb8w7ZEs6DGV8EAg31g
# OuEkQd/Q+YosmOtWyPam2Y0PCSIIU41mouBf5EUi2oZkUdetiblvnPCbZ/yjpiIG
# sew4K2dQ1TvYkzBa0NrDOHFOIw76gUBG2sAnQUOvzj+k9FLvmBy+tSkhcSIq6zrL
# GUTVuXaEYSD3jjhvCeD/RYmTkOK6U6EisY4dYLY+6DYgAhzzBRafVEPulhjxC5uk
# n8Hu5oZmpZ8WYIqqaZRmj2+dY3cVk4sGM32w6CZtlojB/hnNNCFEemr0+SigDVwI
# GQS247RpGaNSaSDlISoECqV3H+B7ghINd5Udwbmt4X8Wg1jJb2My/pGYNJJJV5lz
# nGF4z/6kJSTRYoDn534c6+rYUTMw/ZpklhY+9gzGSgqEeSC+VsI89hfc4Sc=
# SIG # End signature block