Private/Get-EntraOpsClassificationAzureResourceScope.ps1

<#
.SYNOPSIS
    Build Azure resource/subscription scope buckets (Tier0/Tier1/all subscriptions) from Exposure Management
    and Managed Identity host resources, for reuse by Azure RBAC and Microsoft Defender for Cloud scope
    parameterization.

.DESCRIPTION
    Queries Microsoft Security Exposure Management for critical Azure resources and Azure Resource Graph for
    resources hosting system-assigned or user-assigned managed identities (and their consumers), then expands
    each resource to its full ARM hierarchy (resource -> resource group -> subscription -> management groups).
    Resources are bucketed into Tier0 (ControlPlane, or Exposure Management critical assets) and Tier1
    (ManagementPlane) based on the EFFECTIVE tier of the associated managed identity in EntraOps EAM data - the
    most privileged tier found across (1) ObjectAdminTierLevelName (a manually-tagged custom security attribute,
    rarely set on managed identities) and (2) the identity's own aggregated Classification entries (its actually
    granted, classified role/API-permission assignments, e.g. a Microsoft Graph "Sites.Selected" permission
    classified as ManagementPlane). Considering only ObjectAdminTierLevelName would report "Unclassified" for
    every un-tagged managed identity regardless of its real privileges. Also returns the full list of tenant
    subscriptions for building an "everything else" Tier2 scope.

    Tier bucketing fallback: only an empty or "Unknown" effective tier defaults to Tier0 (nothing could be
    evaluated - fail closed). An effective tier of "Unclassified" means the identity WAS evaluated against the
    loaded EAM data and no privileged classification was found, so its host resource is bucketed Tier1
    (ManagementPlane scope) rather than widening the Control Plane. Note this makes the Tier1 bucket sensitive
    to the EAM data actually loaded: with narrowed EntraOpsScopes or a stale/missing EAM export, a genuinely
    privileged managed identity can come back "Unclassified" and land in Tier1.

.PARAMETER EntraOpsEamFolder
    Path to the folder where the EntraOps classification definition (EAM) files are stored.

.PARAMETER EntraOpsScopes
    Array of EntraOps scopes whose EAM json files should be loaded to identify managed identity objects.

.PARAMETER ExposureCriticalityLevel
    Criticality level filter (KQL comparison, e.g. "<1") for Exposure Management critical Azure resources.

.PARAMETER WarningMessages
    Optional list to collect warning messages raised during resource resolution.

.PARAMETER ExposureManagementBaselineFile
    Previous ScopeReasoning_Azure.json. When the Exposure Management query fails, the critical resources of the
    last successful query recorded in this file are used instead. The query outcome is returned as
    ExposureManagement (Status, Metadata, Results, Warnings, HasUnusablePreviousResults); its warnings are
    reported by the caller.

.EXAMPLE
    Get-EntraOpsClassificationAzureResourceScope -EntraOpsEamFolder ".\Classification\EAM" -EntraOpsScopes @("Azure","EntraID") -ExposureCriticalityLevel "<1"
#>


function Get-EntraOpsClassificationAzureResourceScope {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$EntraOpsEamFolder
        ,
        [Parameter(Mandatory = $true)]
        [object]$EntraOpsScopes
        ,
        [Parameter(Mandatory = $false)]
        [string]$ExposureCriticalityLevel = "<1"
        ,
        [Parameter(Mandatory = $false)]
        [System.Collections.Generic.List[psobject]]$WarningMessages
        ,
        [Parameter(Mandatory = $false)]
        [string]$ExposureManagementBaselineFile
    )

    $AzTenantId = (Get-AzContext).Tenant.Id
    $AzureResourceScopeDetails = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Helper: expand a resource ARM path to its full ARM hierarchy
    function Expand-AzureArmHierarchy {
        param(
            [string]$ResourceId,
            [hashtable]$SubToMgLookup
        )
        # Parsed generically rather than by matching a fixed set of resource shapes. The previous three
        # regexes only recognised "<sub>/resourcegroups/<rg>/...", a bare resource group and a bare
        # subscription, so a subscription-level provider resource
        # (/subscriptions/<id>/providers/microsoft.<rp>/...) or a management-group-level resource
        # contributed only its own path - leaving the containing subscription and management groups
        # unmarked, and an Owner assignment at the subscription covering that critical asset
        # unclassified.
        $Paths = [System.Collections.Generic.List[string]]::new()
        $Lower = $ResourceId.ToLower().TrimEnd('/')
        if ([string]::IsNullOrEmpty($Lower)) { return @() }
        $Paths.Add($Lower)

        # Management group scope: /providers/microsoft.management/managementgroups/<name>[/...]
        if ($Lower -match '^(/providers/microsoft\.management/managementgroups/[^/]+)') {
            $Paths.Add($Matches[1])
        }

        # Subscription-rooted scope of any depth. Everything below the subscription contributes its
        # ancestors: the resource group (when present) and always the subscription plus its management
        # group chain.
        if ($Lower -match '^/subscriptions/([^/]+)') {
            $SubId = $Matches[1]
            if ($Lower -match '^/subscriptions/[^/]+/resourcegroups/([^/]+)') {
                $Paths.Add("/subscriptions/$SubId/resourcegroups/$($Matches[1])")
            }
            $Paths.Add("/subscriptions/$SubId")
            if ($SubToMgLookup.ContainsKey($SubId)) {
                foreach ($Mg in $SubToMgLookup[$SubId]) { $Paths.Add($Mg) }
            }
        }

        # Case-insensitive dedup while preserving discovery order ($Lower is already lowercased, but
        # $SubToMgLookup values come from ARG and may differ in casing).
        return @($Paths | Group-Object -Property { "$_".ToLowerInvariant() } | ForEach-Object { $_.Group[0] })
    }

    #region Build subscription -> management group hierarchy map and full subscription list via ARG
    $MgHierarchyQuery = @'
resourcecontainers
| where type == 'microsoft.resources/subscriptions'
| mv-expand mgAncestor = properties.managementGroupAncestorsChain
| where isnotempty(mgAncestor.name)
| project subscriptionId, managementGroupId = tolower(strcat('/providers/microsoft.management/managementgroups/', tostring(mgAncestor.name)))
'@

    # The ARM hierarchy and subscription list are STRUCTURAL inputs: every Tier0/Tier1 scope below is derived
    # from them. A failure here must abort - silently continuing with an empty map collapses $Tier0Paths to
    # "/" and drops every management-group/subscription scoped Owner assignment out of the Control Plane.
    # -ThrowOnFailure is required because the query helpers otherwise report failure with a non-terminating
    # Write-Error and return a (possibly partial) result, which would never reach these catch blocks.
    $SubToMgMap = @{}
    try {
        $MgHierarchyData = Invoke-EntraOpsAzGraphQuery -KqlQuery $MgHierarchyQuery -ThrowOnFailure
        foreach ($Row in $MgHierarchyData) {
            $SubId = $Row.subscriptionId.ToLower()
            if (-not $SubToMgMap.ContainsKey($SubId)) {
                $SubToMgMap[$SubId] = [System.Collections.Generic.List[string]]::new()
            }
            if (-not [string]::IsNullOrEmpty($Row.managementGroupId)) {
                $SubToMgMap[$SubId].Add($Row.managementGroupId) | Out-Null
            }
        }
    } catch {
        if ($null -ne $WarningMessages) {
            $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = "Failed to build Azure ARM hierarchy map: $_" })
        }
        throw "Failed to build the subscription -> management group hierarchy map, which the entire Azure Control Plane scope is derived from: $_. Refusing to generate a narrowed Azure classification scope."
    }

    $AllSubsQuery = @'
resourcecontainers
| where type == 'microsoft.resources/subscriptions'
| project subscriptionId
'@

    $AllSubscriptionScope = @()
    try {
        $AllSubsData = Invoke-EntraOpsAzGraphQuery -KqlQuery $AllSubsQuery -ThrowOnFailure
        $AllSubscriptionScope = @($AllSubsData | Select-Object -ExpandProperty subscriptionId -Unique | ForEach-Object { "/subscriptions/$($_.ToLower())" })
    } catch {
        if ($null -ne $WarningMessages) {
            $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = "Failed to enumerate Azure subscriptions: $_" })
        }
        throw "Failed to enumerate Azure subscriptions, which the Azure Management Plane scope is derived from: $_. Refusing to generate a narrowed Azure classification scope."
    }

    # Plausibility guard: even when both queries "succeed", an empty result set would collapse the Control
    # Plane scope to the hard-coded "/" fallback and reclassify every management-group and subscription
    # scoped Owner / User Access Administrator assignment as Management Plane. A tenant with Azure enabled
    # as an RBAC system always has at least one subscription or management group.
    if ($SubToMgMap.Count -eq 0 -and $AllSubscriptionScope.Count -eq 0) {
        $PlausibilityError = "Azure scope discovery returned no subscriptions and no management groups. This usually indicates missing ARM Reader permissions or a throttled Resource Graph, not an empty tenant. Refusing to generate Classification_Azure.json with a collapsed Control Plane scope."
        if ($null -ne $WarningMessages) {
            $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = $PlausibilityError })
        }
        throw $PlausibilityError
    }
    #endregion

    #region Source 1: Exposure Management critical Azure resources (always ControlPlane)
    $ExposureMgmtQuery = @"
ExposureGraphNodes
| where EntityIds has "AzureResourceId"
| extend CriticalityLevel = toint(NodeProperties.rawData.criticalityLevel.criticalityLevel)
| extend CriticalityRules = tostring(NodeProperties.rawData.criticalityLevel.ruleNames)
| where isnotnull(CriticalityLevel) and CriticalityLevel $ExposureCriticalityLevel
| mv-expand EntityId = parse_json(EntityIds)
| where EntityId.type =~ "AzureResourceId"
| extend ResourceId = tolower(tostring(EntityId.id))
| distinct NodeName, ResourceId, CriticalityLevel, CriticalityRules
"@

    # Increase when the meaning of the results changes: older results are then no longer carried forward
    $ExposureManagementQueryVersion = 1
    $ExposureManagementBaseline = Get-EntraOpsExposureManagementBaseline -Path $ExposureManagementBaselineFile -TenantId $AzTenantId -ExposureCriticalityLevel $ExposureCriticalityLevel -QueryVersion $ExposureManagementQueryVersion -GetLegacyResults {
        param($Content)
        @($Content.ScopeDetails | Where-Object { $_.Source -eq 'ExposureManagement' })
    }
    # A failed query falls back to the critical resources of the last successful query
    $ExposureManagementQuery = Invoke-EntraOpsExposureManagementQuery -Query $ExposureMgmtQuery -Context "critical Azure resources" -Baseline $ExposureManagementBaseline -TenantId $AzTenantId -ExposureCriticalityLevel $ExposureCriticalityLevel -QueryVersion $ExposureManagementQueryVersion
    foreach ($ExposureManagementWarning in $ExposureManagementQuery.Warnings) {
        Write-Warning $ExposureManagementWarning.Message
    }
    $ExposureResources = @($ExposureManagementQuery.Results | Where-Object { -not [string]::IsNullOrEmpty($_.ResourceId) } | ForEach-Object {
            [PSCustomObject][ordered]@{
                NodeName         = $_.NodeName
                ResourceId       = $_.ResourceId
                CriticalityLevel = $_.CriticalityLevel
                CriticalityRules = $_.CriticalityRules
            }
        } | Sort-Object ResourceId, NodeName)
    foreach ($ExRes in $ExposureResources) {
        $ExposureResourceDetail = [PSCustomObject]@{
            ResourceId       = $ExRes.ResourceId
            ResourceName     = $ExRes.NodeName
            Source           = "ExposureManagement"
            EAMTier          = "ControlPlane"
            CriticalityLevel = $ExRes.CriticalityLevel
            CriticalityRules = $ExRes.CriticalityRules
            Reason           = "Critical asset (CriticalityLevel: $($ExRes.CriticalityLevel); CriticalityRules: $($ExRes.CriticalityRules))"
        }
        if ($ExposureManagementQuery.Status -eq 'CarriedForward') {
            $ExposureResourceDetail | Add-Member -NotePropertyName XspmLastSuccessfulQueryDate -NotePropertyValue $ExposureManagementQuery.Metadata.LastSuccessfulQueryDate
        }
        $AzureResourceScopeDetails.Add($ExposureResourceDetail) | Out-Null
    }
    #endregion

    #region Load EAM data and extract managed identity object IDs
    $LoadedEamFileCount = 0
    $AllEamObjects = foreach ($EamScope in $EntraOpsScopes) {
        $EamFile = Join-Path -Path $EntraOpsEamFolder -ChildPath $EamScope -AdditionalChildPath "$($EamScope).json"
        if (-not (Test-Path -LiteralPath $EamFile -PathType Leaf)) {
            Write-Verbose "No EAM data for ${EamScope}: $EamFile not found"
            continue
        }
        # Only a missing export is skipped: an unreadable one would silently drop its managed identities' tiers
        try {
            $EamObjects = Get-Content -LiteralPath $EamFile -Raw -ErrorAction Stop | ConvertFrom-Json -Depth 10 -ErrorAction Stop
        } catch {
            throw "EAM export '$EamFile' could not be read: $($_.Exception.Message). Refusing to generate a narrowed Azure classification scope."
        }
        $LoadedEamFileCount++
        $EamObjects
    }
    if ($LoadedEamFileCount -eq 0) {
        $MissingEamMessage = "No EntraOps EAM files were available in '$EntraOpsEamFolder'. Managed-identity host resources cannot contribute to Azure Tier0/Tier1 scope reasoning until an EAM export exists."
        Write-Warning $MissingEamMessage
        if ($null -ne $WarningMessages) {
            $WarningMessages.Add([PSCustomObject]@{ Type = 'AzureScopeReasoningEamUnavailable'; Message = $MissingEamMessage })
        }
    }

    # Rank tiers by privilege (lower = more privileged); anything unrecognized (blank/"Unclassified"/"Unknown")
    # ranks last so it never wins over a real ControlPlane/ManagementPlane tier.
    # Canonical model order: ControlPlane < ManagementPlane < WorkloadPlane < UserAccess.
    # UserAccess was previously 2 (tied with WorkloadPlane), diverging from every other tier-rank
    # map in the module - a WorkloadPlane scope must outrank UserAccess consistently.
    $TierRank = @{ ControlPlane = 0; ManagementPlane = 1; WorkloadPlane = 2; UserAccess = 3 }
    function Get-TierRank {
        param([string]$Tier)
        if ([string]::IsNullOrEmpty($Tier) -or -not $TierRank.ContainsKey($Tier)) { return 99 }
        return $TierRank[$Tier]
    }

    function Get-ManagedIdentityClassificationEvidence {
        param(
            [object]$ManagedIdentity,
            [string]$Tier,
            [string]$Service
        )

        $Evidence = [System.Collections.Generic.List[string]]::new()
        $RoleNames = [System.Collections.Generic.List[string]]::new()
        $Methods = [System.Collections.Generic.List[string]]::new()
        $Actions = [System.Collections.Generic.List[string]]::new()
        foreach ($Assignment in @($ManagedIdentity.RoleAssignments)) {
            foreach ($Classification in @($Assignment.Classification)) {
                if ($Classification.AdminTierLevelName -ne $Tier -or $Classification.Service -ne $Service) { continue }
                if (-not [string]::IsNullOrWhiteSpace("$($Assignment.RoleDefinitionName)")) {
                    $RoleNames.Add("$($Assignment.RoleDefinitionName)") | Out-Null
                }
                if (-not [string]::IsNullOrWhiteSpace("$($Classification.TaggedBy)")) {
                    $Methods.Add("$($Classification.TaggedBy)") | Out-Null
                }
                foreach ($Action in @($Classification.MatchedActions)) {
                    if (-not [string]::IsNullOrWhiteSpace("$Action")) { $Actions.Add("$Action") | Out-Null }
                }
            }
        }

        $Evidence.Add("Aggregated classification: $Service ($Tier)") | Out-Null
        $UniqueRoles = @($RoleNames | Sort-Object -Unique)
        if ($UniqueRoles.Count -gt 0) { $Evidence.Add("Classified role(s): $($UniqueRoles -join ', ')") | Out-Null }
        $UniqueMethods = @($Methods | Sort-Object -Unique)
        if ($UniqueMethods.Count -gt 0) { $Evidence.Add("Classification method(s): $($UniqueMethods -join ', ')") | Out-Null }
        $UniqueActions = @($Actions | Sort-Object -Unique)
        if ($UniqueActions.Count -gt 0) {
            $ActionPreview = @($UniqueActions | Select-Object -First 5)
            $ActionLabel = $ActionPreview -join ', '
            if ($UniqueActions.Count -gt $ActionPreview.Count) { $ActionLabel += " (+$($UniqueActions.Count - $ActionPreview.Count) more)" }
            $Evidence.Add("Matched role action(s): $ActionLabel") | Out-Null
        }
        return @($Evidence)
    }

    # A managed identity's own privilege tier can come from two independent sources that do not always agree:
    # (1) ObjectAdminTierLevelName - a manually-tagged custom security attribute on the directory object,
    # rarely set for managed identities - and (2) the object's aggregated Classification array, reflecting the
    # role/API-permission assignments EntraOps actually classified for it. Relying on ObjectAdminTierLevelName
    # alone silently reports "Unclassified" for every un-tagged managed identity regardless of its real granted
    # privileges, which would bucket its host resource Tier1 (see the bucketing fallback below) and hide a
    # genuine ControlPlane tier. The same managed identity can also appear in more than one loaded EntraOpsScopes EAM
    # file (e.g. both ResourceApps.json and Azure.json); its effective tier must be the MOST PRIVILEGED tier
    # found across every appearance and every Classification entry, not an arbitrary "first match".
    $ManagedIdentityLookup = @{}
    foreach ($MiObj in ($AllEamObjects | Where-Object { $_.ObjectType -eq "serviceprincipal" -and $_.ObjectSubType -eq "ManagedIdentity" })) {
        $ObjId = $MiObj.ObjectId
        if ([string]::IsNullOrEmpty($ObjId)) { continue }
        if (-not $ManagedIdentityLookup.ContainsKey($ObjId)) {
            $ManagedIdentityLookup[$ObjId] = [PSCustomObject]@{
                ObjectId          = $ObjId
                ObjectDisplayName = $MiObj.ObjectDisplayName
                EffectiveTier     = "Unclassified"
                TierSource        = $null
                TierEvidence      = @()
            }
        }
        $Entry = $ManagedIdentityLookup[$ObjId]
        if ([string]::IsNullOrEmpty($Entry.ObjectDisplayName)) { $Entry.ObjectDisplayName = $MiObj.ObjectDisplayName }

        # Candidate 1: the manually-tagged directory-level tier (ObjectAdminTierLevelName)
        if ((Get-TierRank $MiObj.ObjectAdminTierLevelName) -lt (Get-TierRank $Entry.EffectiveTier)) {
            $Entry.EffectiveTier = $MiObj.ObjectAdminTierLevelName
            $Entry.TierSource = "tagged"
            $Entry.TierEvidence = @("Directory object tier tag: ObjectAdminTierLevelName = $($MiObj.ObjectAdminTierLevelName)")
        }
        # Candidate 2: the most-privileged tier among this object's own classified role/permission grants
        foreach ($ClassEntry in @($MiObj.Classification)) {
            if ((Get-TierRank $ClassEntry.AdminTierLevelName) -lt (Get-TierRank $Entry.EffectiveTier)) {
                $Entry.EffectiveTier = $ClassEntry.AdminTierLevelName
                $Entry.TierSource = "classified via $($ClassEntry.Service)"
                $Entry.TierEvidence = @(Get-ManagedIdentityClassificationEvidence -ManagedIdentity $MiObj -Tier $ClassEntry.AdminTierLevelName -Service $ClassEntry.Service)
            }
        }
    }
    $AllManagedIdentities = @($ManagedIdentityLookup.Values)
    #endregion

    #region Source 2: ARG queries for managed identity host resources and UAMI consumers
    if ($AllManagedIdentities.Count -gt 0) {
        $MiObjectIds = @($AllManagedIdentities | ForEach-Object { $_.ObjectId } | Select-Object -Unique)
        $ObjIdFilterList = ($MiObjectIds | ForEach-Object { "'$_'" }) -join ','

        $SysAssignedQuery = @"
resources
| where tenantId == '$AzTenantId'
| where isnotempty(identity.principalId)
| where tostring(identity.principalId) in ($ObjIdFilterList)
| project id = tolower(id), name, type, resourceGroup, subscriptionId, objectid = tostring(identity.principalId)
"@

        $UamiQuery = @"
resources
| where tenantId == '$AzTenantId'
| where type =~ "microsoft.managedidentity/userassignedidentities"
| where tostring(properties.principalId) in ($ObjIdFilterList)
| project id = tolower(id), name, type, resourceGroup, subscriptionId, objectid = tostring(properties.principalId)
"@


        $SysAssignedResults = @()
        $UamiResults = @()
        # Failed managed identity queries stop the update like the hierarchy query: the host scopes would drop out

        try {
            $SysAssignedResults = @(Invoke-EntraOpsAzGraphQuery -KqlQuery $SysAssignedQuery -ThrowOnFailure)
        } catch {
            if ($null -ne $WarningMessages) {
                $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = "ARG system-assigned MI query failed: $_" })
            }
            throw "Azure Resource Graph system-assigned MI query failed: $_. Refusing to generate a narrowed Azure classification scope."
        }

        try {
            $UamiResults = @(Invoke-EntraOpsAzGraphQuery -KqlQuery $UamiQuery -ThrowOnFailure)
        } catch {
            if ($null -ne $WarningMessages) {
                $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = "ARG user-assigned MI query failed: $_" })
            }
            throw "Azure Resource Graph user-assigned MI query failed: $_. Refusing to generate a narrowed Azure classification scope."
        }

        foreach ($SysRes in $SysAssignedResults) {
            $MiInfo = $ManagedIdentityLookup[$SysRes.objectid]
            $EamTier = if ($null -ne $MiInfo) { $MiInfo.EffectiveTier } else { "Unknown" }
            $MiLabel = if ($null -ne $MiInfo) { "$($MiInfo.ObjectDisplayName) ($($SysRes.objectid))" } else { $SysRes.objectid }
            $TierNote = if ($null -ne $MiInfo -and @($MiInfo.TierEvidence).Count -gt 0) { " [$($MiInfo.TierEvidence[0])]" } elseif ($null -ne $MiInfo -and $MiInfo.TierSource) { " [$($MiInfo.TierSource)]" } else { "" }
            $AzureResourceScopeDetails.Add([PSCustomObject]@{
                    ResourceId              = $SysRes.id
                    ResourceName            = $SysRes.name
                    Source                  = "SystemAssignedMI"
                    EAMTier                 = $EamTier
                    TierSource              = if ($null -ne $MiInfo) { $MiInfo.TierSource } else { $null }
                    TierEvidence            = if ($null -ne $MiInfo) { @($MiInfo.TierEvidence) } else { @() }
                    ManagedIdentityObjectId = $SysRes.objectid
                    Reason                  = "Hosts system-assigned MI: $MiLabel$TierNote"
                }) | Out-Null
        }

        foreach ($UamiRes in $UamiResults) {
            $MiInfo = $ManagedIdentityLookup[$UamiRes.objectid]
            $EamTier = if ($null -ne $MiInfo) { $MiInfo.EffectiveTier } else { "Unknown" }
            $MiLabel = if ($null -ne $MiInfo) { "$($MiInfo.ObjectDisplayName) ($($UamiRes.objectid))" } else { $UamiRes.objectid }
            $TierNote = if ($null -ne $MiInfo -and @($MiInfo.TierEvidence).Count -gt 0) { " [$($MiInfo.TierEvidence[0])]" } elseif ($null -ne $MiInfo -and $MiInfo.TierSource) { " [$($MiInfo.TierSource)]" } else { "" }
            $AzureResourceScopeDetails.Add([PSCustomObject]@{
                    ResourceId              = $UamiRes.id
                    ResourceName            = $UamiRes.name
                    Source                  = "UserAssignedMI"
                    EAMTier                 = $EamTier
                    TierSource              = if ($null -ne $MiInfo) { $MiInfo.TierSource } else { $null }
                    TierEvidence            = if ($null -ne $MiInfo) { @($MiInfo.TierEvidence) } else { @() }
                    ManagedIdentityObjectId = $UamiRes.objectid
                    Reason                  = "User-assigned MI resource: $MiLabel$TierNote"
                }) | Out-Null
        }

        if ($UamiResults.Count -gt 0) {
            $UamiIdFilterList = ($UamiResults | ForEach-Object { "'$($_.id)'" }) -join ','
            $UamiConsumerQuery = @"
resources
| where tenantId == '$AzTenantId'
| where isnotempty(identity.userAssignedIdentities)
| mv-expand uamiKey = bag_keys(identity.userAssignedIdentities)
| where tolower(tostring(uamiKey)) in ($UamiIdFilterList)
| project id = tolower(id), name, type, resourceGroup, subscriptionId, uamiId = tolower(tostring(uamiKey))
"@

            $UamiConsumerResults = @()
            try {
                $UamiConsumerResults = @(Invoke-EntraOpsAzGraphQuery -KqlQuery $UamiConsumerQuery -ThrowOnFailure)
            } catch {
                if ($null -ne $WarningMessages) {
                    $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = "ARG UAMI consumer query failed: $_" })
                }
                throw "Azure Resource Graph UAMI consumer query failed: $_. Refusing to generate a narrowed Azure classification scope."
            }
            foreach ($Consumer in $UamiConsumerResults) {
                $MatchedUami = $UamiResults | Where-Object { $_.id -eq $Consumer.uamiId } | Select-Object -First 1
                $MiInfo = if ($null -ne $MatchedUami) { $ManagedIdentityLookup[$MatchedUami.objectid] } else { $null }
                $EamTier = if ($null -ne $MiInfo) { $MiInfo.EffectiveTier } else { "Unknown" }
                $UamiName = if ($null -ne $MatchedUami) { $MatchedUami.name } else { $Consumer.uamiId }
                $MiName = if ($null -ne $MiInfo) { $MiInfo.ObjectDisplayName } else { "" }
                $TierNote = if ($null -ne $MiInfo -and @($MiInfo.TierEvidence).Count -gt 0) { " [$($MiInfo.TierEvidence[0])]" } elseif ($null -ne $MiInfo -and $MiInfo.TierSource) { " [$($MiInfo.TierSource)]" } else { "" }
                $AzureResourceScopeDetails.Add([PSCustomObject]@{
                        ResourceId              = $Consumer.id
                        ResourceName            = $Consumer.name
                        Source                  = "UAMIConsumer"
                        EAMTier                 = $EamTier
                        TierSource              = if ($null -ne $MiInfo) { $MiInfo.TierSource } else { $null }
                        TierEvidence            = if ($null -ne $MiInfo) { @($MiInfo.TierEvidence) } else { @() }
                        ManagedIdentityObjectId = if ($null -ne $MatchedUami) { $MatchedUami.objectid } else { $null }
                        Reason                  = "Uses UAMI: $UamiName$(if ($MiName) { " / MI: $MiName" })$TierNote"
                    }) | Out-Null
            }
        }
    }
    #endregion

    #region Expand each resource to its ARM hierarchy and bucket into Tier0 (ControlPlane) / Tier1 (ManagementPlane)
    $Tier0Paths = [System.Collections.Generic.List[string]]::new()
    $Tier1Paths = [System.Collections.Generic.List[string]]::new()
    foreach ($Detail in $AzureResourceScopeDetails) {
        $HierarchyPaths = Expand-AzureArmHierarchy -ResourceId $Detail.ResourceId -SubToMgLookup $SubToMgMap
        $Detail | Add-Member -NotePropertyName ExpandedScopePaths -NotePropertyValue @($HierarchyPaths | Sort-Object -Unique) -Force
        if ($Detail.EAMTier -eq "ControlPlane" -or [string]::IsNullOrEmpty($Detail.EAMTier) -or $Detail.EAMTier -eq "Unknown") {
            # ControlPlane, or nothing could be evaluated (empty/"Unknown") - err on the side of Tier0.
            foreach ($Path in $HierarchyPaths) { if (-not [string]::IsNullOrEmpty($Path)) { $Tier0Paths.Add($Path) | Out-Null } }
        } else {
            # ManagementPlane, UserAccess, WorkloadPlane, or "Unclassified" -> Tier1. "Unclassified" is an
            # intentional Tier1 fallback: the identity was evaluated against the loaded EAM data and no
            # privileged classification was found, so it must not widen the Control Plane. Caveat: narrowed
            # EntraOpsScopes or a stale/missing EAM export can also produce "Unclassified" for a genuinely
            # privileged identity - keep the EAM exports current when relying on this bucketing.
            foreach ($Path in $HierarchyPaths) { if (-not [string]::IsNullOrEmpty($Path)) { $Tier1Paths.Add($Path) | Out-Null } }
        }
    }
    if ($AzureResourceScopeDetails.Count -eq 0) {
        $DegenerateScopeMessage = "Azure resource scope discovery found no Exposure Management critical resources or managed-identity host resources. ScopeReasoning_Azure.json will contain only the synthetic '/' Tier0 fallback and cannot classify Defender CloudSet subscriptions as privileged."
        Write-Warning $DegenerateScopeMessage
        if ($null -ne $WarningMessages) {
            $WarningMessages.Add([PSCustomObject]@{ Type = 'AzureScopeReasoningRootOnly'; Message = $DegenerateScopeMessage })
        }
    }
    # Always include the tenant root group scope ("/") in Tier0, since role assignments made directly at the
    # root scope are not covered by the expanded ARM hierarchy paths.
    $Tier0Paths.Add('/') | Out-Null

    $Tier0ResourceScope = @($Tier0Paths | Select-Object -Unique | Sort-Object)
    $Tier1ResourceScope = @($Tier1Paths | Select-Object -Unique | Where-Object { $Tier0ResourceScope -notcontains $_ } | Sort-Object)
    #endregion

    return [PSCustomObject]@{
        Tier0ResourceScope      = $Tier0ResourceScope
        Tier1ResourceScope      = $Tier1ResourceScope
        AllSubscriptionScope    = @($AllSubscriptionScope | Select-Object -Unique | Sort-Object)
        ResourceScopeDetails    = $AzureResourceScopeDetails
        SubToManagementGroupMap = $SubToMgMap
        ExposureManagement      = [PSCustomObject]@{
            Status                     = $ExposureManagementQuery.Status
            Metadata                   = $ExposureManagementQuery.Metadata
            Results                    = $ExposureResources
            Warnings                   = @($ExposureManagementQuery.Warnings)
            HasUnusablePreviousResults = $ExposureManagementQuery.HasUnusablePreviousResults
        }
    }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCJP/kmfp0W6cuD
# VCzpJe2maJ8QAB/torx0ql/h+/RePKCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEINZvVoEeII7mGsZgHgG+K2Dh6I3OJY7iwQ5+DduB
# dCN3MA0GCSqGSIb3DQEBAQUABIIBgF9+rs1i075Uuh9o10WCG11mCtnXcAZKHG7v
# g2htP7Goq34Br6dZuud1c37D1HV8xP1LZkR3lQ3CxXYyC7tU909UMokB0s+RmMm8
# lBw+3NNGPpEmSQAwV9h3S5kL0U3sGnX1lpAvXo8bzhpjHjvNGSWK9g41bV6Q3nM2
# Ew8Zrqt7GdURuXnXZEsuj4yQSow7JR2dShEkU97a6j7CgyIAr9d0d2tPYCWr5UjV
# 5WhANQtvK7KoOkefb/KwETjOtbiF3vI3pC13FX5bY1BL+enq0IULvgJcj9PyxTFW
# e8hFWmxd0/uKE9nuqbgWBkmIkq8he+f7QB+xr5cs0bw2b7LtJa8XveWlJ0qplCxB
# zMqYTpWGwlxJP7pxR01yH7Uu5z2pz8nq0scCH1LjkoWqR2PtvEU/1zZanzRQTPB8
# hHrNIVz/5yui9pOhmmKummCqXf2EwbbI1CGo3ts+Ylcy5Mb34Obu7HPNfbJWWk0U
# PTmSzO7lddS5FIv3hKIq3FqeK+lEXKGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTM3MjVaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBo5RvEoHtd9MtUK/0+6R4fli6aT55DWNIk
# hdtVUKC+3HZb3tS+ggeqh+jwX5RP0zMwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAoiTo8GN80p0qEAjzfOBZUnjs/c21f0ZrNJ1Zaittm1rwhoY5
# kJSb8WZDgJMzcTxgZsxlNYDumWS6H94HKeQyo6KenwQuw4rtejXReqOc9cUSx9on
# 0LXLJsAsqI4vLzh1UgzNcmqRES68HhSD7izX6+wvI6hmanEVTW41uQDoGPwqgjRW
# 3pb03iJPTtUkx48HXOdqOO1UTk878yfE4KTJmCfuo6Yy9S+CUrHpIHJqIL3cRNce
# zYyAw/D3E2AG8MUcMZqJ/mATZikd1fPGFPVmurHQlQjPUcZ+wLOp5wDsE5haywuA
# uE+gS+fYsRNfpRZm+KmUQ1HOhn0S6Se9JVjy4f0vSYXggpyUNO4mbdyKKS6djN8u
# 8exa1DX3QZNgpdm2DKmxaM4MZVFl1QwHb643PX4+4aa6wXpRxyFWWlTbeta3+N26
# 7D21ZaQjsiD09T5f5SfgAMRfiKqNXkIdC3ntwWPDbrbD42GwpF07s+0MeFfij38d
# AzbSXq7WoDiSSryak03qDYzZm8HRrtm3HretOwGCYoafJwS6sVKUh9j4m6iOKCfz
# 48Ke3avYUz0z5fngqIwZIHIBb0zfUU18/iZX4NS7imOlpQizo2X6FJ0EjeQ4gY8y
# +ywJdcFTW7AiddWWtjraU/Y1agRzfq8DNcoXDAjUHd7DxcAozxX81AHxUOA=
# SIG # End signature block