Private/Get-EntraOpsPrivilegedEamDashboardObjects.ps1

<#
.SYNOPSIS
    Transform a Privileged EAM export folder into the EAM Dashboard object schema.

.DESCRIPTION
    Shared transform used by New-EntraOpsPrivilegedEamDashboardData (current/live
    dataset) and New-EntraOpsPrivilegedEamPrivilegeHistoryData (historic snapshots read
    from git history), so both generators compute identical columns
    (EligibilityBy, RestrictedManagement, SyncSource, PrivilegedType, ...) from
    the same PrivilegedEAM/<RbacSystem>/<RbacSystem>.json export shape.

    Only JSON files directly in an RBAC system folder are read (not the
    per-object subfolders user/, group/, serviceprincipal/, ...), matching
    Save-EntraOpsPrivilegedEAMJson.

.PARAMETER ImportPath
    Folder with the Privileged EAM export (contains one subfolder per RBAC
    system, each with a <RbacSystem>.json file).

.PARAMETER TenantId
    Home tenant id used to detect foreign objects for the PrivilegedType column
    (Multi-Tenant Apps, B2B Collaboration, Tenant Governance). Defaults to the
    most common ObjectTenantId in the export among objects that are neither
    guest users nor Tenant Governance delegated admins.

.PARAMETER ScopeReasoningDetails
    Scope reasoning loaded from ScopeReasoning_Azure.json,
    ScopeReasoning_EntraID.json and ScopeReasoning_IdentityGovernance.json.

.PARAMETER ControlPlaneReasoningDetails
    Object classification reasoning loaded from ScopeReasoning_ControlPlane.json.

.OUTPUTS
    System.Collections.Generic.List[object] of [ordered] hashtables (one per
    privileged object) plus the list of source files read is available via
    -SourceFiles (out parameter pattern using a ref variable).
#>


function Get-EntraOpsPrivilegedEamDashboardObjects {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]$ImportPath,

        [Parameter(Mandatory = $false)]
        [System.String]$TenantId,

        [Parameter(Mandatory = $false)]
        [object[]]$ScopeReasoningDetails = @(),

        [Parameter(Mandatory = $false)]
        [object[]]$ControlPlaneReasoningDetails = @(),

        [Parameter(Mandatory = $false)]
        [ref]$SourceFiles
    )

    function Get-PropValue {
        # Safe property access for objects from ConvertFrom-Json whose schema can
        # vary between RBAC systems (also strict-mode proof).
        param($Object, [string] $Name)
        if ($null -eq $Object) { return $null }
        if ($Object -is [System.Collections.IDictionary] -and $Object.Contains($Name)) {
            return $Object[$Name]
        }
        $p = $Object.PSObject.Properties[$Name]
        if ($null -ne $p) { return $p.Value }
        return $null
    }

    function ConvertTo-StringArray {
        # Normalize $null / scalar / array values to a flat string array.
        param($Value)
        if ($null -eq $Value) { return @() }
        return @(@($Value) | Where-Object { $null -ne $_ -and "$_" -ne '' } | ForEach-Object { "$_" })
    }

    function ConvertTo-ScopedObjects {
        param($Value)
        return @(@($Value) | Where-Object { $null -ne $_ } | ForEach-Object {
                [ordered]@{
                    id          = "$(Get-PropValue $_ 'id')"
                    displayName = "$(Get-PropValue $_ 'displayName')"
                }
            })
    }

    function ConvertTo-ClassifiedResources {
        param($Value)
        return @(@($Value) | Where-Object { $null -ne $_ } | ForEach-Object {
                [ordered]@{
                    resourceName = "$(Get-PropValue $_ 'ResourceName')"
                    resourceId   = "$(Get-PropValue $_ 'ResourceId')"
                    originSystem = "$(Get-PropValue $_ 'OriginSystem')"
                    source       = "$(Get-PropValue $_ 'Source')"
                    eamTier      = "$(Get-PropValue $_ 'EAMTier')"
                    reason       = "$(Get-PropValue $_ 'Reason')"
                }
            })
    }

    function ConvertTo-AzureScopeEvidence {
        param($Value)
        return @(@($Value) | Where-Object { $null -ne $_ } | ForEach-Object {
                [ordered]@{
                    subscriptionScope = "$(Get-PropValue $_ 'SubscriptionScope')"
                    resultingScope    = "$(Get-PropValue $_ 'ResultingScope')"
                    scopeDetails      = @(ConvertTo-ClassifiedResources (Get-PropValue $_ 'ScopeDetails'))
                }
            })
    }

    function ConvertTo-ConditionEvaluation {
        param($Value)
        if ($null -eq $Value) { return $null }
        return [ordered]@{
            status      = "$(Get-PropValue $Value 'Status')"
            summary     = "$(Get-PropValue $Value 'Summary')"
            constraints = @(@(Get-PropValue $Value 'Constraints') | Where-Object { $null -ne $_ } | ForEach-Object {
                    [ordered]@{
                        source            = "$(Get-PropValue $_ 'Source')"
                        operator          = "$(Get-PropValue $_ 'Operator')"
                        roleDefinitionIds = @(ConvertTo-StringArray (Get-PropValue $_ 'RoleDefinitionIds'))
                    }
                })
        }
    }

    function Test-AzureControlPlaneRoleAppliesToScopeResource {
        param(
            [object[]]$Classification,
            [string]$ScopeResourceId,
            [string]$RoleDefinitionId
        )

        $broadResourceRoleIds = @(
            '8e3af657-a8ff-443c-a75c-2fe8c4bcb635', # Owner
            'b24988ac-6180-42a0-ab88-20f7382dd24c'  # Contributor
        )
        if ($broadResourceRoleIds -contains $RoleDefinitionId) { return $true }

        # A broad assignment can cover many classified descendants, but only
        # provider-specific Control Plane actions explain why this role reaches one.
        $resourceProviderMatch = [regex]::Match($ScopeResourceId, '(?i)/providers/([^/]+)')
        if (-not $resourceProviderMatch.Success) { return $true }

        $resourceProvider = $resourceProviderMatch.Groups[1].Value
        $actionProviders = foreach ($entry in @($Classification)) {
            if ("$(Get-PropValue $entry 'AdminTierLevelName')" -ne 'ControlPlane') { continue }
            foreach ($action in @(Get-PropValue $entry 'MatchedActions')) {
                $actionProviderMatch = [regex]::Match("$action", '^(?i)([^/*]+)(?:/|$)')
                if ($actionProviderMatch.Success) { $actionProviderMatch.Groups[1].Value }
            }
        }

        return @($actionProviders | Where-Object { $_ -ieq $resourceProvider }).Count -gt 0
    }

    function ConvertTo-ControlPlaneReasons {
        param($Value)
        return @(@($Value) | Where-Object { $null -ne $_ } | ForEach-Object {
                if ($_ -is [string]) {
                    return [ordered]@{ value = "$_" }
                }
                [ordered]@{
                    roleSystem     = "$(Get-PropValue $_ 'RoleSystem')"
                    roleName       = "$(Get-PropValue $_ 'RoleName')"
                    roleScope      = "$(Get-PropValue $_ 'RoleScope')"
                    edgeLabel      = "$(Get-PropValue $_ 'EdgeLabel')"
                    targetNodeName = "$(Get-PropValue $_ 'TargetNodeName')"
                }
            })
    }

    function Get-EligibilityBy {
        # Same case logic as the PrivilegedEAM_WatchLists / PrivilegedEAM_CustomTable
        # parsers (EligibilityBy computed column).
        param([string]$RoleSystem, [string]$PIMAssignmentType, [string]$RoleAssignmentSubType)
        if ($RoleSystem -eq 'EntraID' -and $PIMAssignmentType -eq 'Eligible' -and
            $RoleAssignmentSubType -in @('Nested Eligible member', 'Eligible member')) {
            return 'PIM for Entra ID Roles and Groups'
        }
        if ($RoleSystem -eq 'EntraID' -and $PIMAssignmentType -eq 'Eligible') { return 'PIM for Entra ID Roles' }
        if ($RoleSystem -eq 'Azure' -and $PIMAssignmentType -eq 'Eligible') { return 'PIM for Azure Roles' }
        if ($RoleAssignmentSubType -in @('Nested Eligible group member', 'Eligible member')) { return 'PIM for Groups' }
        return 'N/A'
    }

    function Get-RestrictedManagement {
        # Same case logic as the "List of Privileged Assets" grid in the
        # EntraOps Privileged EAM - Overview workbook.
        param(
            [string]$ObjectType,
            [string]$ObjectSubType,
            [bool]$ByAadRole,
            [bool]$ByRAG,
            [bool]$ByRMAU
        )
        if ($ObjectType -eq 'serviceprincipal') { return 'Not available' }
        if ($ObjectType -eq 'group') {
            if ($ObjectSubType -eq 'Role-assignable' -and $ByRMAU) { return 'Conflict' }
            if ($ByRMAU) { return 'Applied' }
            if ($ByRAG) { return 'Applied' }
            return 'Not applied'
        }
        if ($ObjectType -eq 'user') {
            if ($ByAadRole -or $ByRAG -or $ByRMAU) { return 'Applied' }
            return 'Not applied'
        }
        return 'Not applied'
    }

    # Match PrivilegedEAM/<System>/<System>.json only (one level deep), not the
    # per-object subfolders (user/, group/, serviceprincipal/ ...).
    $files = Get-ChildItem -Path $ImportPath -Directory -ErrorAction SilentlyContinue |
    ForEach-Object { Get-ChildItem -Path $_.FullName -Filter '*.json' -File } |
    Sort-Object FullName

    if ($SourceFiles) { $SourceFiles.Value = @($files) }

    $objects = [System.Collections.Generic.List[object]]::new()

    foreach ($file in @($files)) {
        $roleSystem = Split-Path -Leaf (Split-Path -Parent $file.FullName)

        try {
            $data = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 |
            ConvertFrom-Json
        } catch {
            # Fail closed: an export file that exists but cannot be read/parsed must abort the
            # generation - silently dropping a whole RBAC system would produce a plausible but
            # truncated dataset downstream. A legitimately absent file is simply not enumerated.
            throw "Failed to read or parse Privileged EAM export file '$($file.FullName)' (RBAC system '$roleSystem'): $($_.Exception.Message)"
        }

        foreach ($o in @($data)) {
            if ($null -eq $o) { continue }

            $objType = "$(Get-PropValue $o 'ObjectType')"
            $objSubType = "$(Get-PropValue $o 'ObjectSubType')"
            $objectId = "$(Get-PropValue $o 'ObjectId')"
            $display = "$(Get-PropValue $o 'ObjectDisplayName')"
            if (-not $display) { $display = $objectId }

            $onPremSync = [bool](Get-PropValue $o 'OnPremSynchronized')
            $outsideOfHomeTenant = [bool](Get-PropValue $o 'OutsideOfHomeTenant')
            $byRAG = [bool](Get-PropValue $o 'RestrictedManagementByRAG')
            $byAadRole = [bool](Get-PropValue $o 'RestrictedManagementByAadRole')
            $byRMAU = [bool](Get-PropValue $o 'RestrictedManagementByRMAU')

            $objTierName = "$(Get-PropValue $o 'ObjectAdminTierLevelName')"
            if (-not $objTierName) { $objTierName = 'Unclassified' }

            # Object-level classification (aggregated tier/service pairs).
            $objClassification = [System.Collections.Generic.List[object]]::new()
            foreach ($c in @(Get-PropValue $o 'Classification')) {
                if ($null -eq $c) { continue }
                $tierName = "$(Get-PropValue $c 'AdminTierLevelName')"
                if (-not $tierName) { $tierName = 'Unclassified' }
                $objClassification.Add([ordered]@{
                        adminTierLevel     = "$(Get-PropValue $c 'AdminTierLevel')"
                        adminTierLevelName = $tierName
                        service            = "$(Get-PropValue $c 'Service')"
                    })
            }

            # Administrative units (id + displayName pairs).
            $adminUnits = [System.Collections.Generic.List[object]]::new()
            foreach ($au in @(Get-PropValue $o 'AssignedAdministrativeUnits')) {
                if ($null -eq $au) { continue }
                $adminUnits.Add([ordered]@{
                        id          = "$(Get-PropValue $au 'id')"
                        displayName = "$(Get-PropValue $au 'displayName')"
                    })
            }

            # Role assignments incl. per-assignment classification.
            $assignments = [System.Collections.Generic.List[object]]::new()
            foreach ($ra in @(Get-PropValue $o 'RoleAssignments')) {
                if ($null -eq $ra) { continue }

                $pimType = "$(Get-PropValue $ra 'PIMAssignmentType')"
                $assignSub = "$(Get-PropValue $ra 'RoleAssignmentSubType')"

                $raClassification = [System.Collections.Generic.List[object]]::new()
                foreach ($c in @(Get-PropValue $ra 'Classification')) {
                    if ($null -eq $c) { continue }
                    $tierName = "$(Get-PropValue $c 'AdminTierLevelName')"
                    if (-not $tierName) { $tierName = 'Unclassified' }
                    $raClassification.Add([ordered]@{
                            adminTierLevel             = "$(Get-PropValue $c 'AdminTierLevel')"
                            adminTierLevelName         = $tierName
                            service                    = "$(Get-PropValue $c 'Service')"
                            taggedBy                   = "$(Get-PropValue $c 'TaggedBy')"
                            taggedByObjectIds          = @(ConvertTo-StringArray (Get-PropValue $c 'TaggedByObjectIds'))
                            taggedByObjectDisplayNames = @(ConvertTo-StringArray (Get-PropValue $c 'TaggedByObjectDisplayNames'))
                            taggedByRoleSystem         = "$(Get-PropValue $c 'TaggedByRoleSystem')"
                            matchedActions             = @(ConvertTo-StringArray (Get-PropValue $c 'MatchedActions'))
                            scopedObjects              = @(ConvertTo-ScopedObjects (Get-PropValue $c 'ScopedObjects'))
                        })
                }

                $scopeReasoning = [System.Collections.Generic.List[object]]::new()
                if ($ScopeReasoningDetails.Count -gt 0) {
                    $scopeId = "$(Get-PropValue $ra 'RoleAssignmentScopeId')"
                    $normalizedScopeId = $scopeId.Trim().TrimEnd('/')
                    $classificationTaggedObjectIds = @($raClassification | ForEach-Object { @(Get-PropValue $_ 'taggedByObjectIds') } | Where-Object { $_ })
                    $classificationTiers = @($raClassification | ForEach-Object { "$(Get-PropValue $_ 'adminTierLevelName')" } | Where-Object { $_ } | Select-Object -Unique)
                    $matchedScopeReasons = @($ScopeReasoningDetails | Where-Object {
                            $reasoningRoleSystem = "$(Get-PropValue $_ 'RoleSystem')"
                            $reasoningScopeId = "$(Get-PropValue $_ 'ScopeId')".Trim().TrimEnd('/')
                            if (-not $reasoningScopeId) { return $false }
                            if ($roleSystem -eq 'DeviceManagement') {
                                return $reasoningRoleSystem -eq 'DeviceManagement' -and
                                $classificationTaggedObjectIds -contains $reasoningScopeId -and
                                (-not (Get-PropValue $_ 'EAMTier') -or $classificationTiers -contains "$(Get-PropValue $_ 'EAMTier')")
                            }
                            if ($roleSystem -eq 'Defender' -and $reasoningRoleSystem -eq 'Azure') {
                                $isArmScope = $normalizedScopeId -match '^/(subscriptions|providers/microsoft\.management)/'
                                return $isArmScope -and ($reasoningScopeId -ieq $normalizedScopeId -or $reasoningScopeId.StartsWith("$normalizedScopeId/", [System.StringComparison]::OrdinalIgnoreCase))
                            }
                            if ($reasoningRoleSystem -ne $roleSystem) { return $false }
                            if ($roleSystem -eq 'Azure') {
                                $expandedScopePaths = @(Get-PropValue $_ 'ExpandedScopePaths' | ForEach-Object { "$_".Trim().TrimEnd('/') })
                                return $reasoningScopeId -eq $normalizedScopeId -or $reasoningScopeId.StartsWith("$normalizedScopeId/", [System.StringComparison]::OrdinalIgnoreCase) -or $expandedScopePaths -contains $normalizedScopeId
                            }
                            return $reasoningScopeId -ieq $normalizedScopeId
                        })
                    foreach ($scopeReason in $matchedScopeReasons) {
                        $scopeResourceId = "$(Get-PropValue $scopeReason 'ScopeId')"
                        if ($roleSystem -eq 'Azure' -and -not (Test-AzureControlPlaneRoleAppliesToScopeResource -Classification $raClassification -ScopeResourceId $scopeResourceId -RoleDefinitionId "$(Get-PropValue $ra 'RoleDefinitionId')")) {
                            continue
                        }
                        $scopeReasoning.Add([ordered]@{
                                resourceName            = "$(Get-PropValue $scopeReason 'ScopeName')"
                                resourceId              = $scopeResourceId
                                roleSystem              = $roleSystem
                                source                  = "$(Get-PropValue $scopeReason 'Source')"
                                eamTier                 = "$(Get-PropValue $scopeReason 'EAMTier')"
                                resultingScope          = "$(Get-PropValue $scopeReason 'ResultingScope')"
                                tierSource              = "$(Get-PropValue $scopeReason 'TierSource')"
                                tierEvidence            = @(ConvertTo-StringArray (Get-PropValue $scopeReason 'TierEvidence'))
                                reason                  = "$(Get-PropValue $scopeReason 'Reason')"
                                criticalityLevel        = "$(Get-PropValue $scopeReason 'CriticalityLevel')"
                                criticalityRules        = "$(Get-PropValue $scopeReason 'CriticalityRules')"
                                managedIdentityObjectId = "$(Get-PropValue $scopeReason 'ManagedIdentityObjectId')"
                                scopeCategory           = "$(Get-PropValue $scopeReason 'ScopeCategory')"
                                scopeType               = "$(Get-PropValue $scopeReason 'ScopeType')"
                                catalogDisplayName      = "$(Get-PropValue $scopeReason 'CatalogDisplayName')"
                                classifiedResources     = @(ConvertTo-ClassifiedResources (Get-PropValue $scopeReason 'ClassifiedResources'))
                                expandedScopePaths      = @(ConvertTo-StringArray (Get-PropValue $scopeReason 'ExpandedScopePaths'))
                                affectedObjects         = @(ConvertTo-ScopedObjects (Get-PropValue $scopeReason 'AffectedObjects'))
                                resolutionStatus        = "$(Get-PropValue $scopeReason 'ResolutionStatus')"
                                subscriptionScopes      = @(ConvertTo-StringArray (Get-PropValue $scopeReason 'SubscriptionScopes'))
                                tier0SubscriptionScopes = @(ConvertTo-StringArray (Get-PropValue $scopeReason 'Tier0SubscriptionScopes'))
                                tier1SubscriptionScopes = @(ConvertTo-StringArray (Get-PropValue $scopeReason 'Tier1SubscriptionScopes'))
                                azureScopeEvidence      = @(ConvertTo-AzureScopeEvidence (Get-PropValue $scopeReason 'AzureScopeEvidence'))
                                scopeRelation           = $(if ($roleSystem -eq 'DeviceManagement') { 'Assigned scope group' } elseif ("$(Get-PropValue $scopeReason 'ScopeId')".TrimEnd('/') -ieq $scopeId.TrimEnd('/')) { 'Exact' } else { 'Descendant resource' })
                            })
                    }
                }

                $assignments.Add([ordered]@{
                    roleAssignmentInstanceId              = if (Get-PropValue $ra 'RoleAssignmentInstanceId') { "$(Get-PropValue $ra 'RoleAssignmentInstanceId')" } else { Get-EntraOpsRoleAssignmentInstanceId -RoleSystem $roleSystem -RoleAssignment $ra }
                        roleAssignmentId                      = "$(Get-PropValue $ra 'RoleAssignmentId')"
                        roleAssignmentScopeId                 = "$(Get-PropValue $ra 'RoleAssignmentScopeId')"
                        roleAssignmentScopeName               = "$(Get-PropValue $ra 'RoleAssignmentScopeName')"
                        applicationScopes                     = @(ConvertTo-StringArray (Get-PropValue $ra 'AppScopeName'))
                        roleAssignmentType                    = "$(Get-PropValue $ra 'RoleAssignmentType')"
                        roleAssignmentSubType                 = $assignSub
                        roleAssignmentCondition               = "$(Get-PropValue $ra 'RoleAssignmentCondition')"
                        roleAssignmentConditionVersion        = "$(Get-PropValue $ra 'RoleAssignmentConditionVersion')"
                        roleDefinitionConditions              = @(@(Get-PropValue $ra 'RoleDefinitionConditions') | Where-Object { $null -ne $_ } | ForEach-Object {
                                [ordered]@{
                                    condition        = "$(Get-PropValue $_ 'Condition')"
                                    conditionVersion = "$(Get-PropValue $_ 'ConditionVersion')"
                                }
                            })
                        conditionEvaluation                   = ConvertTo-ConditionEvaluation (Get-PropValue $ra 'ConditionEvaluation')
                        pimManagedRole                        = [bool](Get-PropValue $ra 'PIMManagedRole')
                        pimAssignmentType                     = $pimType
                        roleDefinitionName                    = "$(Get-PropValue $ra 'RoleDefinitionName')"
                        roleDefinitionId                      = "$(Get-PropValue $ra 'RoleDefinitionId')"
                        roleType                              = "$(Get-PropValue $ra 'RoleType')"
                        roleIsPrivileged                      = [bool](Get-PropValue $ra 'RoleIsPrivileged')
                        eligibilityBy                         = Get-EligibilityBy -RoleSystem $roleSystem -PIMAssignmentType $pimType -RoleAssignmentSubType $assignSub
                        transitiveByObjectDisplayName         = "$(Get-PropValue $ra 'TransitiveByObjectDisplayName')"
                        transitiveByObjectId                  = "$(Get-PropValue $ra 'TransitiveByObjectId')"
                        transitiveByNestingObjectDisplayNames = @(ConvertTo-StringArray (Get-PropValue $ra 'TransitiveByNestingObjectDisplayNames'))
                        cloudSetSubscriptionScopes            = @(ConvertTo-StringArray (Get-PropValue $ra 'CloudSetSubscriptionScopes'))
                        scopeResolutionStatus                 = "$(Get-PropValue $ra 'ScopeResolutionStatus')"
                        classification                        = $raClassification
                        scopeReasoning                        = $scopeReasoning
                    })
            }

            $controlPlaneReasoning = [System.Collections.Generic.List[object]]::new()
            foreach ($reasoningObject in (Find-EntraOpsControlPlaneReasoning -ReasoningObjects $ControlPlaneReasoningDetails -ObjectId $objectId)) {
                $controlPlaneReasoning.Add([ordered]@{
                        classificationSources = @(ConvertTo-StringArray (Get-PropValue $reasoningObject 'ClassificationSource'))
                        classificationReasons = @(ConvertTo-ControlPlaneReasons (Get-PropValue $reasoningObject 'ClassificationReason'))
                    })
            }

            $objects.Add([ordered]@{
                    objectId                      = $objectId
                    objectTenantId                = "$(Get-PropValue $o 'ObjectTenantId')"
                    objectType                    = $objType
                    objectSubType                 = $objSubType
                    objectDisplayName             = $display
                    objectUserPrincipalName       = "$(Get-PropValue $o 'ObjectUserPrincipalName')"
                    objectAdminTierLevel          = "$(Get-PropValue $o 'ObjectAdminTierLevel')"
                    objectAdminTierLevelName      = $objTierName
                    onPremSynchronized            = $onPremSync
                    syncSource                    = $(if ($onPremSync) { 'Hybrid' } else { 'Cloud-Only' })
                    outsideOfHomeTenant           = $outsideOfHomeTenant
                    restrictedManagementByAadRole = $byAadRole
                    restrictedManagementByRAG     = $byRAG
                    restrictedManagementByRMAU    = $byRMAU
                    restrictedManagement          = Get-RestrictedManagement -ObjectType $objType -ObjectSubType $objSubType -ByAadRole $byAadRole -ByRAG $byRAG -ByRMAU $byRMAU
                    roleSystem                    = $roleSystem
                    assignedAdministrativeUnits   = $adminUnits
                    associatedWorkAccount         = @(ConvertTo-StringArray (Get-PropValue $o 'AssociatedWorkAccount'))
                    ownedDevices                  = @(ConvertTo-StringArray (Get-PropValue $o 'OwnedDevices'))
                    associatedPawDevice           = @(ConvertTo-StringArray (Get-PropValue $o 'AssociatedPawDevice'))
                    controlPlaneReasoning         = $controlPlaneReasoning
                    classification                = $objClassification
                    roleAssignments               = $assignments
                })
        }
    }

    # PrivilegedType (workbook-style "origin" category of the privileged object):
    # * Tenant Governance - foreign object whose role assignments ALL originate from a
    # Tenant Governance delegation (RoleAssignmentSubType
    # "Tenant Governance Delegated Admin"), or any other foreign
    # object that is neither a service principal nor a user.
    # * Multi-Tenant Apps - service principal whose ObjectTenantId differs from the
    # home tenant (owning application lives in another tenant).
    # * B2B Collaboration - external (guest) user whose ObjectTenantId differs from the
    # home tenant and who holds at least one role assignment that
    # is not a Tenant Governance delegation.
    # * Local Identities - everything else: local user, group, single-tenant application.
    # The home tenant is -TenantId when given, otherwise derived from the export itself:
    # the most common ObjectTenantId among objects that are neither guest users nor
    # Tenant Governance delegated admins (the OutsideOfHomeTenant flag of
    # Get-EntraOpsPrivilegedEntraObject is not part of the Save-EntraOpsPrivilegedEAMJson
    # export, so ObjectTenantId is the reliable signal for foreign objects).
    $tgSubType = 'Tenant Governance Delegated Admin'
    $homeTenantId = $TenantId
    if ([string]::IsNullOrWhiteSpace($homeTenantId)) {
        $tenantCounts = @{}
        foreach ($obj in $objects) {
            if (-not $obj.objectTenantId) { continue }
            if ($obj.objectType -eq 'user' -and $obj.objectSubType -eq 'Guest') { continue }
            if (@($obj.roleAssignments | Where-Object { $_.roleAssignmentSubType -eq $tgSubType }).Count -gt 0) { continue }
            $tenantCounts[$obj.objectTenantId] = [int]$tenantCounts[$obj.objectTenantId] + 1
        }
        $homeTenantId = ($tenantCounts.GetEnumerator() | Sort-Object -Property Value -Descending | Select-Object -First 1).Key
    }
    foreach ($obj in $objects) {
        $isForeign = [bool]$obj.outsideOfHomeTenant -or
        ($homeTenantId -and $obj.objectTenantId -and $obj.objectTenantId -ne $homeTenantId)
        $privilegedType = 'Local Identities'
        if ($isForeign) {
            $nonTgAssignments = @($obj.roleAssignments | Where-Object { $_.roleAssignmentSubType -ne $tgSubType })
            if (@($obj.roleAssignments).Count -gt 0 -and $nonTgAssignments.Count -eq 0) {
                $privilegedType = 'Tenant Governance'
            } elseif ($obj.objectType -eq 'serviceprincipal') {
                $privilegedType = 'Multi-Tenant Apps'
            } elseif ($obj.objectType -eq 'user') {
                $privilegedType = 'B2B Collaboration'
            } else {
                $privilegedType = 'Tenant Governance'
            }
        }
        $obj['outsideOfHomeTenant'] = [bool]$isForeign
        $obj['privilegedType'] = $privilegedType
    }

    return , $objects
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAS4vB9ow1nDNIe
# GPltxTXuKov2ITEnveYgC48o6eeeBKCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIEGQbwfejj0/TSmOqZjcsv+cLjBZ9o94m8BUeYUh
# LXd5MA0GCSqGSIb3DQEBAQUABIIBgChvwKW6gZ3Dsbx5O5E+3zmxFfjS+eETgQqA
# Cfm+SPJvrsn6hlud2UOon3XWfH0cMD6TO9kuTNWtEohE83raQJ5gkAGbp36lIVZf
# 0UGBOppNZxSJiWoYqRJdFVmC4RHlTy2WcuWech6/Lmqv/M0d85f7dON8J1n1oiMC
# yl1KvFiEGTbphObsxjDfMgAvkmjtlyG7otlOjSr0yHrl/ucPOEX8sC8+Ae0oU5uo
# DRVk15qz2fMNP6Lrzr7Pu43pDNEh+AWtgW90CyoVLaEJqqDpYrkuvR9SUCB7Mj2f
# EW3tSPJ4//HbbdWlt+GfL+js+QZKYDaO39KKOxiZJW6mnJUhzN/2AskYr9xEylyu
# wJzIyQKRPQs7KNK80gG5u4vf5WBLx0bG1UnrQpDMYNuxMV23DD4D6FDaymL4GTk7
# mqx9i1ClqB0Nf2cQIVRGGSsG3fnSaJHFiaXpNZ2bQbrgbpe8PbsYjlCKxvdQf0wr
# jvhCZGIDn0mP+59ROx3mZkRU6kUo/qGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTM5NDNaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDD8D8Smf1+h1Setq/QiEGW4tU8BDwEBCSyk
# EEPY8QXezMmubLI95xTgw3Q5mhqQIegwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAZdd1yIe83bh33hoDli3nqQkLhIZL8XJ0z5KWeBE3VJQuHFzv
# rwd5TtUDNvgDccdbXZbinNCN2aph21EPhx/fn8lVc5ocWiNXzDbsutp0twsY/FeE
# 2YjOzbEXCJyv3ppD2cgxZEjnVer2pItLqmFR/JqwnRmXamdoXRvaDTDgQ9OJRknx
# q3wP7b1t3Ovepk8aIdj69Qf7scYWKh3nsPsV9yACBF2JyamYSEP/dVTqTv05P+n3
# AbM2PgNKUWqTm0BMAjP+x4aSJ+Elq/iq1gneztqappk3MOjKiMhldhsHgxg3/Mn8
# MPgfUFIrI1jSCoAFvxNrGsstiAZ3ESnwHkrmXt2R+9XORg36Vzg29IcspBMB+Cn4
# IuzQCqJuKaU66dWxPPhkq8i+//szZ4mM7OT/dF4nuKrIxafFokWVCaofHAqrka0Z
# 5xwyKQKLUndkKQrQT9SM+QAgsXfpfHNNFhZvTrvJeHBT7fTL64CJNWw6Q+9k6OXb
# UIXnah/9L4VogATJwWPwffN/l2s7hVhQrK70R0Nl3NI7fOAqlexFPZk+r7MY17Az
# K1AtQjOAoHxjp4UqVZx51UhC+LtikzLzJekG6Z9HJtoehs4AIClKwMlEZFahae56
# v4o2mww7/we86WCG1mzuWFJxJKLfHDf7/vQ38qNODwg3w5jnEJ+a3LmJFoQ=
# SIG # End signature block