Public/PrivilegedAccess/Get-EntraOpsPrivilegedEAMIntune.ps1

<#
.SYNOPSIS
    Get a list in schema of EntraOps with all privileged principals in Microsoft Intune and assigned roles and classifications.

.DESCRIPTION
    Get a list in schema of EntraOps with all privileged principals in Microsoft Intune and assigned roles and classifications.

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

.PARAMETER FolderClassification
    Folder path to the classification definition files. Default is "./Classification".

.PARAMETER SampleMode
    Use sample data for testing or offline mode. Default is $False. Default sample data is stored in "./Samples"

.PARAMETER GlobalExclusion
    Use global exclusion list for classification. Default is $true. Global exclusion list is stored in "./Classification/Global.json".

.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 IncludeObjectDetails
    Include descriptive object details in warning output. Defaults to ConsoleOutput.IncludeObjectDetails from
    EntraOpsConfig.json. Object IDs are always shown.
#>


function Get-EntraOpsPrivilegedEAMIntune {
    [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]$IncludeObjectDetails = [bool]$Global:EntraOpsIncludeObjectDetails
    )

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

    # Check if classification file custom and/or template file exists, choose custom template for tenant if available
    $IntuneClassificationFilePath = Resolve-EntraOpsClassificationPath -ClassificationFileName "Classification_DeviceManagement.json" -FolderClassification $FolderClassification

    # Load group → device member mapping for BloodHound edge enrichment (created by Update-EntraOpsClassificationControlPlaneScope)
    $DeviceMembersFile = Join-Path (Split-Path $IntuneClassificationFilePath -Parent) "DeviceManagement_ScopeGroupDeviceMembers.json"
    $DeviceMembersData = if (Test-Path $DeviceMembersFile) {
        Get-Content $DeviceMembersFile -Raw | ConvertFrom-Json
    } else { $null }

    #region Get all role assignments and global exclusions
    #region Stage 1: Fetch Device Management Roles
    $Stage1Start = Get-Date
    Write-Host ""
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " Stage 1/4: Fetching Device Management Roles" -ForegroundColor Cyan
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "Retrieving Intune device management role assignments and definitions..." -ForegroundColor Gray
    Write-Progress -Activity "Stage 1/4: Fetching Device Management Roles" -Status "Loading role assignments and global exclusions..." -PercentComplete 10

    if ($SampleMode -ne $True) {
        $DeviceMgmtRbacAssignments = Get-EntraOpsPrivilegedDeviceRoles -TenantId $TenantId -WarningMessages $WarningMessages
    } else {
        $WarningMessages.Add([PSCustomObject]@{Type = "Stage1"; Message = "SampleMode currently not supported!" })
    }

    $GlobalExclusionList = Import-EntraOpsGlobalExclusions -Enabled $GlobalExclusion

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

    # Return early if no role assignments found to prevent null index errors
    if ($null -eq $DeviceMgmtRbacAssignments -or @($DeviceMgmtRbacAssignments).Count -eq 0) {
        Write-Warning "No Device Management role assignments found. Returning empty result."
        return @()
    }

    #region Get scope tages and assignments
    #region Stage 2: Fetch Scope Tags
    $Stage2Start = Get-Date
    Write-Host ""
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " Stage 2/4: Fetching Scope Tags" -ForegroundColor Cyan
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "Retrieving role scope tags and their assignments for Intune device management..." -ForegroundColor Gray
    Write-Progress -Activity "Stage 2/4: Fetching Scope Tags" -Status "Loading scope tags and assignments..." -PercentComplete 25

    $ScopeTags = (Invoke-EntraOpsMsGraphQuery -Method GET -Uri https://graph.microsoft.com/beta/deviceManagement/roleScopeTags -OutputType PSObject)
    # Build scope tag name lookup for display name resolution
    $ScopeTagNameLookup = @{}
    foreach ($ScopeTag in $ScopeTags) {
        $ScopeTagNameLookup["$($ScopeTag.Id)"] = $ScopeTag.DisplayName
    }

    # Resolve display names for directoryScopeIds from role assignments
    # Scope tags are only for visibility, not scope enforcement — directoryScopeIds per assignment is authoritative
    Write-Host "Resolving display names for role assignment directoryScopeIds..." -ForegroundColor Gray
    $ScopeGroupNameCache = @{}
    $AllDirectoryScopeIds = @($DeviceMgmtRbacAssignments | Where-Object { $null -ne $_.DirectoryScopeIds } | ForEach-Object { $_.DirectoryScopeIds } | Where-Object { -not [string]::IsNullOrEmpty($_) } | Select-Object -Unique)
    $AllGroupIdsToResolve = @($AllDirectoryScopeIds | Where-Object { -not [string]::IsNullOrEmpty($_) } | Select-Object -Unique)
    if ($AllGroupIdsToResolve.Count -gt 0) {
        try {
            $Body = @{ ids = @($AllGroupIdsToResolve) } | ConvertTo-Json -Depth 3
            $GroupObjects = Invoke-EntraOpsMsGraphQuery -Method POST -Uri "https://graph.microsoft.com/beta/directoryObjects/getByIds" -Body $Body -OutputType PSObject
            foreach ($GroupObj in $GroupObjects) {
                $ScopeGroupNameCache[$GroupObj.id] = $GroupObj.displayName
            }
        } catch {
            Write-Warning "Failed to batch-resolve directory object display names: $($_.Exception.Message)"
        }
        # Fallback for any IDs not resolved
        foreach ($GroupId in $AllGroupIdsToResolve) {
            if (-not $ScopeGroupNameCache.ContainsKey($GroupId)) {
                $ScopeGroupNameCache[$GroupId] = $GroupId
            }
        }
    }
    Write-Host "Resolved display names for $($ScopeGroupNameCache.Count) directory object(s)." -ForegroundColor Gray

    $Stage2Duration = ((Get-Date) - $Stage2Start).TotalSeconds
    Write-Host "✓ Stage 2 completed in $([Math]::Round($Stage2Duration, 2)) seconds ($($ScopeTags.Count) scope tags retrieved)" -ForegroundColor Green
    Write-Progress -Activity "Stage 2/4: Fetching Scope Tags" -Completed
    #endregion

    #region Check if RBAC role action and scope is defined in JSON classification
    #region Stage 3: Classify Role Actions
    $Stage3Start = Get-Date
    Write-Host ""
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " Stage 3/4: Classifying Role Actions" -ForegroundColor Cyan
    Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "Checking if RBAC role action and scope is defined in JSON classification..." -ForegroundColor Gray
    Write-Progress -Activity "Stage 3/4: Classifying Role Actions" -Status "Mapping JSON classifications..." -PercentComplete 50

    # Optimization: Pre-fetch all role definitions to avoid N+1 API calls
    $IntuneRoleDefinitionsCache = @{}
    if ($SampleMode -ne $True) {
        Write-Host "Pre-fetching all Intune role definitions..." -ForegroundColor Gray
        $AllIntuneRoles = Invoke-EntraOpsMsGraphQuery -Method GET -Uri "https://graph.microsoft.com/beta/roleManagement/deviceManagement/roleDefinitions" -OutputType PSObject
        foreach ($Role in $AllIntuneRoles) {
            if ($null -ne $Role.id) {
                # Ensure ID is string for consistent lookup
                $IntuneRoleDefinitionsCache["$($Role.id)"] = $Role
            }
        }
        Write-Host "Cached $($IntuneRoleDefinitionsCache.Count) role definitions." -ForegroundColor Gray
    }

    # Build lookup: (RoleDefinitionId, RoleAssignmentScopeId) -> directoryScopeIds for scope matching and TaggedBy enrichment
    # Scoped per role assignment to avoid cross-contamination between different role assignments sharing the same scope tag
    $DirectoryScopesByRoleAndScope = @{}
    foreach ($assignment in $DeviceMgmtRbacAssignments | Where-Object { $_.RoleAssignmentScopeId -ne "/" -and $null -ne $_.DirectoryScopeIds }) {
        $key = "$($assignment.RoleDefinitionId)|$($assignment.RoleAssignmentScopeId)"
        if (-not $DirectoryScopesByRoleAndScope.ContainsKey($key)) {
            $DirectoryScopesByRoleAndScope[$key] = [System.Collections.Generic.List[string]]::new()
        }
        foreach ($dsId in $assignment.DirectoryScopeIds) {
            if (-not [string]::IsNullOrEmpty($dsId) -and -not $DirectoryScopesByRoleAndScope[$key].Contains($dsId)) {
                $DirectoryScopesByRoleAndScope[$key].Add($dsId)
            }
        }
    }

    $IntuneResourcesByClassificationJSON = Expand-EntraOpsPrivilegedEAMJsonFile -FilePath $IntuneClassificationFilePath | select-object EAMTierLevelName, EAMTierLevelTagValue, Category, Service, RoleAssignmentScopeName, ExcludedRoleAssignmentScopeName, RoleDefinitionActions, ExcludedRoleDefinitionActions

    # Load classification overwrites (down-/upgrade of entire role definitions) from tenant-specific folder or Templates fallback.
    # Role action overwrites are already baked into the classification file by Update-EntraOpsClassificationControlPlaneScope.
    $ClassificationOverwrites = Import-EntraOpsClassificationOverwrites -RbacSystem "DeviceManagement" -FolderClassification $FolderClassification
    $DeviceMgmtRbacClassificationsByJSON = @()
    $DeviceMgmtRbacClassificationsByJSON += foreach ($DeviceMgmtRbacAssignment in $DeviceMgmtRbacAssignments | Select-Object -Unique RoleDefinitionId, RoleAssignmentScopeId) {
        if ($DeviceMgmtRbacAssignment.RoleAssignmentScopeId -ne "/") {
            $DeviceMgmtRbacAssignment.RoleAssignmentScopeId = "$($DeviceMgmtRbacAssignment.RoleAssignmentScopeId)"
        }
        # Role actions are defined for scope and role definition contains an action of the role, otherwise all role actions within role assignment scope will be applied
        if ($SampleMode -eq $True) {
            $WarningMessages.Add([PSCustomObject]@{Type = "Stage3"; Message = "SampleMode currently not supported!" })
        } else {
            $IntuneRoleActions = $IntuneRoleDefinitionsCache["$($DeviceMgmtRbacAssignment.RoleDefinitionId)"]
        }

        # Scope tags are for visibility only — use directoryScopeIds of this specific assignment for scope matching
        $AppScopeId = "$($DeviceMgmtRbacAssignment.RoleAssignmentScopeId)"

        # Get directoryScopeIds specific to this role definition + scope combination
        $IsScoped = ($AppScopeId -ne "/")
        $RoleAndScopeKey = "$($DeviceMgmtRbacAssignment.RoleDefinitionId)|$AppScopeId"
        $AssignmentDirectoryScopes = @()
        if ($IsScoped -and $DirectoryScopesByRoleAndScope.ContainsKey($RoleAndScopeKey)) {
            $AssignmentDirectoryScopes = @($DirectoryScopesByRoleAndScope[$RoleAndScopeKey])
        }

        $MatchedClassificationByScope = @()
        # Check if RBAC scope is listed in JSON classification.
        # For scoped assignments, the assignment's own directoryScopeIds are matched against
        # RoleAssignmentScopeName (which contains group GUIDs from classification parameters).
        # "/*" matches any scoped (non-root) assignment.
        # "/" matches tenant-wide assignments.
        # Specific GUIDs match against the assignment's directoryScopeIds.
        $MatchedClassificationByScope += foreach ($ClassEntry in $IntuneResourcesByClassificationJSON) {
            $ScopeName = $ClassEntry.RoleAssignmentScopeName

            # Determine scope match
            # Note: Intune uses numeric scope tag IDs (e.g. "2") not path-like scopes,
            # so -like with "/*" pattern does not work here (unlike Defender/EntraID).
            $ScopeMatch = $false
            if ($AppScopeId -eq "/" -and $ScopeName -eq "/") {
                $ScopeMatch = $true
            } elseif ($AppScopeId -ne "/" -and $ScopeName -eq "/*") {
                $ScopeMatch = $true
            } elseif ($AppScopeId -ne "/" -and $ScopeName -ne "/" -and $ScopeName -ne "/*" -and $AssignmentDirectoryScopes.Count -gt 0) {
                # Group GUID match: check if classification scope name is among this assignment's directoryScopeIds
                if ($ScopeName -in $AssignmentDirectoryScopes) {
                    $ScopeMatch = $true
                }
            }

            if ($ScopeMatch) {
                # Exclusion check against this assignment's directoryScopeIds (not all scope tag groups)
                $IsExcluded = $false
                if ($AppScopeId -eq "/") {
                    $IsExcluded = ($ClassEntry.ExcludedRoleAssignmentScopeName -contains "/")
                } elseif ($ScopeName -eq "/*" -and $IsScoped -and $AssignmentDirectoryScopes.Count -gt 0) {
                    # Wildcard match: only exclude if ALL of this assignment's directoryScopeIds are excluded
                    $NonExcludedGroups = @($AssignmentDirectoryScopes | Where-Object { $_ -notin $ClassEntry.ExcludedRoleAssignmentScopeName })
                    $IsExcluded = ($NonExcludedGroups.Count -eq 0)
                } elseif ($ScopeName -ne "/*" -and $ScopeName -ne "/") {
                    # GUID match: check if the specific matched group is excluded
                    $IsExcluded = ($ScopeName -in $ClassEntry.ExcludedRoleAssignmentScopeName)
                }

                if (-not $IsExcluded) {
                    $ClassEntry
                }
            }
        }

        # Guard: does any allowed role action match an in-scope classification? Matchers are
        # precomputed once (loop-invariant) and the probe short-circuits on the first hit instead
        # of materializing the full match set only to test .Count -gt 0.
        $GuardMatchers = @(foreach ($ClassEntry in $MatchedClassificationByScope) {
                Build-EntraOpsClassificationActionMatcher -RoleDefinitionActions $ClassEntry.RoleDefinitionActions -ExcludedRoleDefinitionActions $ClassEntry.ExcludedRoleDefinitionActions
            })
        $HasClassifiedRoleAction = $false
        foreach ($Action in $IntuneRoleActions.rolePermissions.allowedResourceActions) {
            if ([string]::IsNullOrEmpty($Action)) { continue }
            foreach ($Matcher in $GuardMatchers) {
                $AllowedMatch = $Matcher.AllowedExact.Contains($Action)
                if (-not $AllowedMatch) {
                    foreach ($Pattern in $Matcher.AllowedWildcards) { if ($Action -like $Pattern) { $AllowedMatch = $true; break } }
                }
                if (-not $AllowedMatch) { continue }
                $ExcludedMatch = $Matcher.ExcludedExact.Contains($Action)
                if (-not $ExcludedMatch) {
                    foreach ($Pattern in $Matcher.ExcludedWildcards) { if ($Action -like $Pattern) { $ExcludedMatch = $true; break } }
                }
                if (-not $ExcludedMatch) { $HasClassifiedRoleAction = $true; break }
            }
            if ($HasClassifiedRoleAction) { break }
        }

        if ($HasClassifiedRoleAction) {
            # Track which scope name (group GUID or wildcard) triggered each classification match,
            # and which specific groups from this assignment are relevant (non-excluded)
            # Matchers precomputed once per classification entry (loop-invariant across the action
            # loop). This site deliberately checks allowed actions only, like the helper call it
            # replaces - exclusions here are scope-based, handled below per match type.
            $IntuneClassificationMatchers = @(foreach ($ClassEntry in $IntuneResourcesByClassificationJSON) {
                    [pscustomobject]@{
                        Entry   = $ClassEntry
                        Matcher = Build-EntraOpsClassificationActionMatcher -RoleDefinitionActions $ClassEntry.RoleDefinitionActions
                    }
                })
            $ClassifiedWithMatchedScope = @()
            foreach ($IntuneRoleAction in $IntuneRoleActions.rolePermissions.allowedResourceActions) {
                if ([string]::IsNullOrEmpty($IntuneRoleAction)) { continue }
                $ClassifiedWithMatchedScope += foreach ($MatcherEntry in $IntuneClassificationMatchers) {
                    $ClassEntry = $MatcherEntry.Entry
                    $ScopeName = $ClassEntry.RoleAssignmentScopeName

                    # Same scope matching logic as above
                    $ScopeMatch = $false
                    $MatchType = $null
                    if ($AppScopeId -eq "/" -and $ScopeName -eq "/") {
                        $ScopeMatch = $true
                        $MatchType = "root"
                    } elseif ($AppScopeId -ne "/" -and $ScopeName -eq "/*") {
                        $ScopeMatch = $true
                        $MatchType = "wildcard"
                    } elseif ($AppScopeId -ne "/" -and $ScopeName -ne "/" -and $ScopeName -ne "/*" -and $AssignmentDirectoryScopes.Count -gt 0 -and $ScopeName -in $AssignmentDirectoryScopes) {
                        $ScopeMatch = $true
                        $MatchType = "guid"
                    }

                    $ActionMatch = $false
                    if ($ScopeMatch) {
                        $ActionMatch = $MatcherEntry.Matcher.AllowedExact.Contains($IntuneRoleAction)
                        if (-not $ActionMatch) {
                            foreach ($Pattern in $MatcherEntry.Matcher.AllowedWildcards) { if ($IntuneRoleAction -like $Pattern) { $ActionMatch = $true; break } }
                        }
                    }
                    if ($ScopeMatch -and $ActionMatch) {
                        $IsExcluded = $false
                        $EntryMatchedGroups = @()

                        if ($AppScopeId -eq "/") {
                            $IsExcluded = ($ClassEntry.ExcludedRoleAssignmentScopeName -contains "/")
                        } elseif ($MatchType -eq "wildcard" -and $IsScoped -and $AssignmentDirectoryScopes.Count -gt 0) {
                            # Wildcard: filter to only the non-excluded directoryScopeIds from this assignment
                            $EntryMatchedGroups = @($AssignmentDirectoryScopes | Where-Object { $_ -notin $ClassEntry.ExcludedRoleAssignmentScopeName })
                            $IsExcluded = ($EntryMatchedGroups.Count -eq 0)
                        } elseif ($MatchType -eq "guid") {
                            # GUID match: check if the specific matched group is excluded
                            $IsExcluded = ($ScopeName -in $ClassEntry.ExcludedRoleAssignmentScopeName)
                            if (-not $IsExcluded -and $ScopeName -in $AssignmentDirectoryScopes) {
                                $EntryMatchedGroups = @($ScopeName)
                            }
                        }

                        if (-not $IsExcluded) {
                            [PSCustomObject]@{
                                EAMTierLevelName     = $ClassEntry.EAMTierLevelName
                                EAMTierLevelTagValue = $ClassEntry.EAMTierLevelTagValue
                                Service              = $ClassEntry.Service
                                MatchedScopeName     = $ScopeName
                                MatchType            = $MatchType
                                MatchedGroupIds      = $EntryMatchedGroups
                                MatchedAction        = $IntuneRoleAction
                            }
                        }
                    }
                }
            }

            # Group by unique classification and compute per-classification TaggedBy
            $UniqueClassifications = $ClassifiedWithMatchedScope | Select-Object -Unique EAMTierLevelName, EAMTierLevelTagValue, Service
            $Classification = foreach ($UniqueClass in $UniqueClassifications) {
                # Aggregate matched entries for this classification
                $MatchedEntries = @($ClassifiedWithMatchedScope | Where-Object {
                    $_.EAMTierLevelName -eq $UniqueClass.EAMTierLevelName -and
                    $_.EAMTierLevelTagValue -eq $UniqueClass.EAMTierLevelTagValue -and
                    $_.Service -eq $UniqueClass.Service
                })

                # Aggregate matched actions across all entries
                [array]$MatchedActions = @($MatchedEntries.MatchedAction | Where-Object { -not [string]::IsNullOrEmpty($_) } | Select-Object -Unique)

                if ($IsScoped) {
                    [array]$TaggedByIds = @($MatchedEntries | ForEach-Object { $_.MatchedGroupIds } | Where-Object { -not [string]::IsNullOrEmpty($_) } | Select-Object -Unique)

                    if ($TaggedByIds.Count -gt 0) {
                        [array]$TaggedByNames = @($TaggedByIds | ForEach-Object { $ScopeGroupNameCache[$_] })
                        $TaggedBySystem = "ScopeTagAssignedGroup"
                    } else {
                        $TaggedByIds = $null
                        $TaggedByNames = $null
                        $TaggedBySystem = $null
                    }
                } else {
                    $TaggedByIds = $null
                    $TaggedByNames = $null
                    $TaggedBySystem = $null
                }

                # Resolve scoped devices from group → device member mapping
                $ScopedObjects = @()
                if ($null -ne $DeviceMembersData) {
                    if ($TaggedByIds.Count -gt 0) {
                        # Scoped assignment: devices from matched groups
                        $ScopedObjects = @($TaggedByIds | ForEach-Object {
                            $grpData = $DeviceMembersData.groupDeviceMembers.$_
                            if ($grpData) { $grpData.deviceMembers }
                        } | Where-Object { $null -ne $_ })
                    } elseif (-not $IsScoped) {
                        # Tenant-wide assignment: all classified devices
                        $ScopedObjects = @($DeviceMembersData.allClassifiedDevices | Where-Object { $null -ne $_ })
                    }
                }

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

            [PSCustomObject]@{
                'RoleDefinitionId'      = $DeviceMgmtRbacAssignment.RoleDefinitionId
                'RoleAssignmentScopeId' = $DeviceMgmtRbacAssignment.RoleAssignmentScopeId
                'Classification'        = $Classification
            }
        } else {
            $ClassifiedWithMatchedScope = @()
        }
    }
    #endregion

    #region Classify all assigned privileged users and groups in Device Management
    $DeviceMgmtRbacClassifications = foreach ($DeviceMgmtRbacAssignment in $DeviceMgmtRbacAssignments) {
        $DeviceMgmtRbacAssignment = $DeviceMgmtRbacAssignment | Select-Object -ExcludeProperty Classification, DirectoryScopeIds
        $Classification = @()
        $Classification += ($DeviceMgmtRbacClassificationsByJSON | Where-Object { $_.RoleAssignmentScopeId -eq $DeviceMgmtRbacAssignment.RoleAssignmentScopeId -and $_.RoleDefinitionId -eq $DeviceMgmtRbacAssignment.RoleDefinitionId }).Classification
        $Classification = $Classification | select-object -Unique AdminTierLevel, AdminTierLevelName, Service, TaggedBy, TaggedByObjectIds, TaggedByObjectDisplayNames, TaggedByRoleSystem, MatchedActions, ScopedObjects, Justification | Sort-Object AdminTierLevel, AdminTierLevelName, Service, TaggedBy
        $DeviceMgmtRbacAssignment | Add-Member -NotePropertyName "Classification" -NotePropertyValue $Classification -Force
        $DeviceMgmtRbacAssignment
    }

    # Apply role definition classification overwrites (down-/upgrade by RoleDefinitionId or RoleDefinitionName)
    if ($ClassificationOverwrites.RoleDefinitionOverwrites.Count -gt 0) {
        Write-Host "Applying $($ClassificationOverwrites.RoleDefinitionOverwrites.Count) role definition classification overwrite(s) from Classification_RoleDefinitionOverwrites.json..." -ForegroundColor Yellow
        $DeviceMgmtRbacClassifications = Invoke-EntraOpsClassificationRoleOverwrite -RbacClassifications $DeviceMgmtRbacClassifications -RoleDefinitionOverwrites $ClassificationOverwrites.RoleDefinitionOverwrites -RoleSystem "DeviceManagement"
    }

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

    #region Apply classification to all assigned privileged users and groups in Device Management
    #region Stage 4: Resolve and Finalize Objects
    $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

    # Group assignments by ObjectId for efficient lookup
    $DeviceMgmtRbacByObject = $DeviceMgmtRbacClassifications | Group-Object ObjectId -AsHashTable -AsString

    # Collect unique objects and resolve details
    # Case-insensitive dedup (mirrors Get-EntraOpsPrivilegedEAMAzure.ps1): Select-Object -Unique compares
    # ObjectId case-sensitively and would emit the same principal twice when ids differ only in casing.
    $UniqueObjects = @(
        $DeviceMgmtRbacAssignments |
            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

    # Aggregate classifications and build output objects
    $DeviceMgmtRbacClassifiedObjects = Invoke-EntraOpsEAMClassificationAggregation `
        -UniqueObjects $UniqueObjects `
        -ObjectDetailsCache $ObjectDetailsCache `
        -RbacClassificationsByObject $DeviceMgmtRbacByObject `
        -RoleSystem "DeviceManagement" `
        -EnableParallelProcessing $EnableParallelProcessing `
        -ParallelThrottleLimit $ParallelThrottleLimit `
        -WarningMessages $WarningMessages
    #endregion

    Write-Host "Applying global exclusions and finalizing results..."
    $FilteredIntuneObjects = $DeviceMgmtRbacClassifiedObjects | Where-Object { $GlobalExclusionList -notcontains $_.ObjectId }

    Write-Host "Completed processing $($FilteredIntuneObjects.Count) privileged objects."

    Show-EntraOpsWarningSummary -WarningMessages $WarningMessages -IncludeObjectDetails $IncludeObjectDetails

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

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBszoZATYJ10ioA
# SkTCboY6BOk9VW7kIEunlTVMs8/AW6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIFiLD4Vds8RXZJ4U+sFS0C60X3iFzTBIAp6ET8B1
# wDLqMA0GCSqGSIb3DQEBAQUABIIBgAVR5BLEYy+9cUt+kghZOCFGS5B6yfWI5g2u
# U4fUmwnxtkhBXC+LqbbW8wCNTeWwWH57lDZg3yi4I+CHwXLzcypX+aa1Dkh0Xa03
# NYOZ1zo/T/RvNInzLzhsXLdE5mZwiyphwusiPrk4zATq15mdpo1m0YeGS7agGzdL
# hO4nE5i4c1gw2dU4+V3Yc9tUKNiRHj4V/uxpokmtVP9l2KRX8BGtHqXIXNDrXv8g
# ziyJuKeXCPW961Zh87+4DymF9XM6AwHWgYggHXpyUZbIwD5x/7X/i8ntAcPfbqSN
# u4Yc7G/dY2CNlNcgDvW7MBafdVTNmd2bYBxIBRmp+3stXj6ej9fJSR25q0pKKoqL
# VPWwRVkNkeGobqCosketmzV5UqSe+jz9gLeXPmOu6BcxjJXg3ztJ2yu9NS1ASPS3
# DFDEsjcY5exZFukcquBRPeXwPxq1YAfPzPGiZ7TD5ODPrrZUocHHKwpP/jMAmMtc
# yS1p6xvHd6UT6TK5cddTN2yTFoBCfaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxMjFaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDDOuxCnmmYVDuusT7MBbULC+xXsuQIhI2Di
# pKeviB775C6OYk1hpUPq8MB4gv4K3mMwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAKBCKuORyIPFcmkbG4PQl/J3C3SrGZLHb6KINxkNmlul339Mu
# AgEL4K0vOdB/oaQkIh/9gm8PLgMKKMziIMU6Jq5hhNTkqCqeCOmXrcdlzfRQ2+2H
# EjOZm7EjQcqg1tiEbmFxEA5uK8UEn/vzfwG04r+FDyEWaU1WC6rGE28MhYWHyWVT
# 5A9CGItMPZkXibKzLEyWoFWvybcKWg/X84BaS6TjRZSG1OzeWxiL2lTMBL2pOex2
# R7CoatP/ZjhnhnaUj7K8VQjOQhxe04bjDifIGDiIWB5dQa0r/dP8HhVMAVE8DCpz
# XN6FVBGdT7aykndrbX6EYYzbW5TvqH71c2LBQ1Q6ZLBuyTnNX4GH+eGgLxLnJyR6
# xDQKOKbha0R3toTbf4bg8ZAB7AS3XVq8n8RrszkLFTjt66CgSh2pK5mTUYB4zfss
# qHZVboknxTFW1Nc6J/hsgrXZNZE329P/RhUzIJdFx7yq2hluyQPcVovMnj9R0ziZ
# T+7bzqsN9JzP6QLdAq00HMRm8oImQ3XsGthZQ2hG+34xOl2Q78AiDmCRwOaj4FFx
# +qRjzoSJE78DY/wR2rFKIeDf1g40fDSeR+RhUhSr88Yd9KLFOcLhePnwRRT73lsH
# sMmqh6vaHyoflo4/UadqeUXifWj7bgLMobRe0alOUuM9Btk6QQgkbKvKoxA=
# SIG # End signature block