Public/PrivilegedAccess/Update-EntraOpsPrivilegedUnprotectedAdministrativeUnit.ps1

<#
.SYNOPSIS
    Add privileged users without any protection (by role-assignable groups, assigned directory role or existing RMAU membership) to an Restricted Management Administrative Units to avoid delegated management by lower privileged user.

.DESCRIPTION
    Add privileged users without any protection (by role-assignable groups, assigned directory role or existing RMAU membership) to an Restricted Management Administrative Units to avoid delegated management by lower privileged user.

.PARAMETER ApplyToAccessTierLevel
    Array of Access Tier Levels to be processed. Default is ControlPlane, ManagementPlane.

.PARAMETER FilterObjectType
    Array of object types to be processed. Default is User, Group. Service Principal and application objects are not supported to be protected by RMAU.

.PARAMETER RbacSystems
    Array of RBAC systems to be processed. Default is Azure, AzureBilling, EntraID, IdentityGovernance, DeviceManagement, ResourceApps.

.PARAMETER IncludeUnprotectedDevices
    Also sync devices owned by/associated to privileged users (OwnedDevices, AssociatedPawDevice) into the same tier's RMAU, unless already protected by another RMAU.

.PARAMETER RemovalSafetyThreshold
    Fraction of current members that may be removed in a single run. Principal and device removals are planned together; an oversized plan applies no removals to that RMAU. Default is 0.5 (50%).

.PARAMETER ForceRemovalBeyondSafetyThreshold
    Apply a reviewed removal plan even when it exceeds RemovalSafetyThreshold.

.PARAMETER IncludeObjectDetails
    Include object display names and detailed API errors in console output. Defaults to
    ConsoleOutput.IncludeObjectDetails from EntraOpsConfig.json. Object IDs are always shown.

.EXAMPLE
    Assign privileged users without any protection but privileges in RBAC Systems "IdentityGovernance" to Restricted Management Administrative Units
    Update-EntraOpsPrivilegedUnprotectedAdministrativeUnit -RbacSystems ("IdentityGovernance")
#>


function Update-EntraOpsPrivilegedUnprotectedAdministrativeUnit {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $False)]
        [ValidateSet("ControlPlane", "ManagementPlane")]
        [Array]$ApplyToAccessTierLevel = ("ControlPlane", "ManagementPlane")
        ,

        [Parameter(Mandatory = $False)]
        [ValidateSet("User", "Group")]
        [Array]$FilterObjectType = ("User", "Group")
        ,
        [Parameter(Mandatory = $False)]
        [ValidateSet("Azure", "EntraID", "IdentityGovernance", "ResourceApps", "DeviceManagement", "Defender")]
        [Array]$RbacSystems = ("Azure", "EntraID", "IdentityGovernance", "DeviceManagement", "Defender")
        ,
        # Maximum fraction of current RMAU members removable per synchronization.
        [Parameter(Mandatory = $False)]
        [ValidateRange(0, 1)]
        [double]$RemovalSafetyThreshold = 0.5
        ,
        # Permits removals that exceed RemovalSafetyThreshold.
        [Parameter(Mandatory = $False)]
        [switch]$ForceRemovalBeyondSafetyThreshold
        ,
        [Parameter(Mandatory = $False)]
        [switch]$IncludeUnprotectedDevices
        ,
        [Parameter(Mandatory = $False)]
        [boolean]$IncludeObjectDetails = [bool]$Global:EntraOpsIncludeObjectDetails
        ,
        [Parameter(Mandatory = $False)]
        [boolean]$ApplyRmauAssignmentsForUnprotectedObjects = $false
    )

    # Get all privileged EAM objects
    $PrivilegedEamObjects = foreach ($RbacSystem in $RbacSystems) {
        Get-Content "$DefaultFolderClassifiedEam/$RbacSystem/$($RbacSystem).json" | ConvertFrom-Json
    }

    # Get all privileged EAM objects without any restricted management and their tier levels
    $UnprotectedPrivilegedUser = $PrivilegedEamObjects | Where-Object {
        $_.RestrictedManagementByRAG -ne $True -and `
            $_.RestrictedManagementByAadRole -ne $True -and `
            $_.RestrictedManagementByRMAU -ne $True -and `
            $_.ObjectType -in $FilterObjectType }

    # Get all unique AdminTierLevels which needs to be iterated for assigning objects to Conditional Access Target Groups
    $PrivilegedEamTierLevels = Get-ChildItem -Path "$($DefaultFolderClassification)/Templates" -File -Recurse -Exclude *.Param.json | foreach-object { Get-Content $_.FullName -Filter "*.json" | ConvertFrom-Json }
    $SelectedPrivilegedEamTierLevels = $PrivilegedEamTierLevels | where-object { $_.EAMTierLevelName -in $ApplyToAccessTierLevel } | select-object -unique @{Name = 'AdminTierLevel'; Expression = 'EAMTierLevelTagValue' }, @{Name = 'AdminTierLevelName'; Expression = 'EAMTierLevelName' }
    # Always process the most privileged tier first, independent of how $ApplyToAccessTierLevel is ordered
    # in the config, so a device claimed by Tier 0 reaches its AU before a lower tier evaluates it.
    $SelectedPrivilegedEamTierLevels = @($SelectedPrivilegedEamTierLevels | Sort-Object { [int]$_.AdminTierLevel })
    #endregion

    # Summary tracking
    $SyncSummary = [System.Collections.Generic.List[psobject]]::new()
    $TotalAdded = 0
    $TotalRemoved = 0
    $TotalKept = 0
    $WarningMessages = New-Object -TypeName "System.Collections.Generic.List[psobject]"
    $TenantId = (Get-AzContext).Tenant.Id

    # Devices carry no EAM classification of their own - they inherit their tier from the privileged users
    # that own or use them. A user classified at more than one tier would otherwise make the same device
    # "desired" in every tier's AU, leaving the winner dependent on tier iteration order. Resolve each
    # device to its most privileged (lowest) tier once, up front, so the outcome is deterministic.
    $DeviceTierAssignment = @{}
    # AU names this run manages, so a sibling tier's AU is not mistaken for unrelated RMAU protection.
    $ManagedAdminUnitNames = @($SelectedPrivilegedEamTierLevels | ForEach-Object { "Tier" + $_.AdminTierLevel + "-" + $_.AdminTierLevelName + ".UnprotectedObjects" })
    if ($IncludeUnprotectedDevices) {
        foreach ($Tier in $SelectedPrivilegedEamTierLevels) {
            $TierDeviceOwners = $PrivilegedEamObjects | Where-Object {
                $_.ObjectType -eq "user" -and `
                    $_.Classification.AdminTierLevel -contains $Tier.AdminTierLevel -and `
                    $_.Classification.AdminTierLevelName -contains $Tier.AdminTierLevelName -and `
                    ([string]::IsNullOrEmpty($_.ObjectTenantId) -or $_.ObjectTenantId -eq $TenantId)
            }
            foreach ($OwnedDeviceId in @($TierDeviceOwners | ForEach-Object { @($_.OwnedDevices) + @($_.AssociatedPawDevice) } | Where-Object { $_ })) {
                if (-not $DeviceTierAssignment.ContainsKey($OwnedDeviceId) -or [int]$Tier.AdminTierLevel -lt [int]$DeviceTierAssignment[$OwnedDeviceId]) {
                    $DeviceTierAssignment[$OwnedDeviceId] = $Tier.AdminTierLevel
                }
            }
        }
    }

    Write-Host ""
    Write-Host "=========================================================" -ForegroundColor Cyan
    Write-Host " EntraOps - Unprotected Objects RMAU Sync" -ForegroundColor Cyan
    Write-Host " RBAC Systems : $($RbacSystems -join ', ')" -ForegroundColor Cyan
    Write-Host " Access Tiers : $($ApplyToAccessTierLevel -join ', ')" -ForegroundColor Cyan
    Write-Host " Object Types : $($FilterObjectType -join ', ')" -ForegroundColor Cyan
    Write-Host " Unprotected : $(@($UnprotectedPrivilegedUser).Count) objects identified across all tiers" -ForegroundColor Cyan
    Write-Host "=========================================================" -ForegroundColor Cyan
    Write-Host ""

    #region Assign all unprotected principals in Privileged EAM to restricted AUs or remove from RMAU if no longer privileged
    foreach ($TierLevel in $SelectedPrivilegedEamTierLevels) {

        $AdminUnitId = $null
        $AdminUnitName = "Tier" + $TierLevel.AdminTierLevel + "-" + $TierLevel.AdminTierLevelName + ".UnprotectedObjects"
        $AuAdded = 0
        $AuRemoved = 0
        $AuKept = 0
        $AuFailed = 0
        $AuStatus = "OK"

        Write-Host "---------------------------------------------------------" -ForegroundColor DarkCyan
        Write-Host " AU: $AdminUnitName" -ForegroundColor DarkCyan
        Write-Host "---------------------------------------------------------" -ForegroundColor DarkCyan

        $AdministrativeUnits = @(Invoke-EntraOpsMsGraphQuery -Method "GET" -Uri "/beta/administrativeUnits?`$filter=DisplayName eq '$(ConvertTo-EntraOpsODataStringLiteral -Value $AdminUnitName)'" -DisableCache -OutputType PSObject)
        # -AllowNotFound: a missing AU is handled by the SKIP branch below so the remaining
        # tiers still reconcile; duplicates still throw (F-10 safety).
        $AdminUnitId = (Select-EntraOpsUniqueGraphObject -InputObject $AdministrativeUnits -ObjectDescription "administrative unit '$AdminUnitName'" -AllowNotFound).id
        if ($null -eq $AdminUnitId) {
            Write-Warning " [SKIP] Could not find AU: $AdminUnitName"
            $WarningMessages.Add([PSCustomObject]@{ Type = "AuNotFound"; Message = "Could not find AU: $AdminUnitName" })
            $SyncSummary.Add([PSCustomObject]@{
                    AdminUnit     = $AdminUnitName
                    MembersBefore = "N/A"
                    Added         = 0
                    Removed       = 0
                    Kept          = 0
                    Failed        = 0
                    Status        = "NOT FOUND"
                })
            continue
        }

        # Get privileged objects for this tier level
        $UnprotectedPrivilegedUserOnTierLevel = @($UnprotectedPrivilegedUser | Where-Object { $_.Classification.AdminTierLevel -contains $TierLevel.AdminTierLevel -and $_.Classification.AdminTierLevelName -contains $TierLevel.AdminTierLevelName })
        $CurrentAdminUnitMembers = @()
        $CurrentAdminUnitMembers = (Invoke-EntraOpsMsGraphQuery -Method "GET" -Uri "/beta/administrativeUnits/$($AdminUnitId)/members" -OutputType PSObject -DisableCache)

        $CurrentMemberCount = @($CurrentAdminUnitMembers.Id).Count
        $DesiredMemberCount = $UnprotectedPrivilegedUserOnTierLevel.Count
        Write-Host " Current members : $CurrentMemberCount | Unprotected objects for tier: $DesiredMemberCount" -ForegroundColor Gray

        $DesiredPrincipalIds = @($UnprotectedPrivilegedUserOnTierLevel.ObjectId | Where-Object { $_ } | Select-Object -Unique)
        $CurrentPrincipalMembers = @($CurrentAdminUnitMembers | Where-Object { $_.'@odata.type' -in @('#microsoft.graph.user', '#microsoft.graph.group') })
        $CurrentPrincipalIds = @($CurrentPrincipalMembers.Id | Select-Object -Unique)
        $PrincipalIdsToAdd = @($DesiredPrincipalIds | Where-Object { $_ -notin $CurrentPrincipalIds })
        $PrincipalRemovalPlan = [System.Collections.Generic.List[psobject]]::new()

        foreach ($CurrentMember in @($CurrentPrincipalMembers | Where-Object { $_.id -notin $DesiredPrincipalIds })) {
            $EamEntry = $PrivilegedEamObjects | Where-Object { $_.ObjectId -eq $CurrentMember.id } | Select-Object -First 1
            $RemoveReason = $null

            if ($null -eq $EamEntry) {
                $RemoveReason = "no longer privileged"
            } elseif ($EamEntry.RestrictedManagementByAadRole -eq $True -or $EamEntry.RestrictedManagementByRAG -eq $True) {
                $RemoveReason = "protected by AadRole or RAG"
            } else {
                $MemberType = $CurrentMember.'@odata.type'.Replace('#microsoft.graph.', '')
                $MemberAUs = Invoke-EntraOpsMsGraphQuery -Uri "/beta/$($MemberType)s/$($CurrentMember.id)/memberOf/Microsoft.Graph.AdministrativeUnit" -OutputType PSObject -DisableCache
                $OtherRMAUs = $MemberAUs | Where-Object { $_.isMemberManagementRestricted -eq $True -and $_.id -ne $AdminUnitId }
                if ($null -ne $OtherRMAUs) {
                    $RemoveReason = "protected by another RMAU"
                }
            }

            if ($null -ne $RemoveReason) {
                $PrincipalRemovalPlan.Add([PSCustomObject]@{
                        Id     = $CurrentMember.id
                        Type   = $CurrentMember.'@odata.type'.Replace('#microsoft.graph.', '')
                        Name   = $CurrentMember.displayName
                        Reason = $RemoveReason
                    })
            } else {
                if ($IncludeObjectDetails) {
                    Write-Host " [~] KEEP [$($CurrentMember.'@odata.type'.Replace('#microsoft.graph.', ''))] $($CurrentMember.displayName) (this AU is the only RMAU)" -ForegroundColor DarkYellow
                } else {
                    Write-Host " [~] KEEP [$($CurrentMember.'@odata.type'.Replace('#microsoft.graph.', ''))] $($CurrentMember.id) (this AU is the only RMAU)" -ForegroundColor DarkYellow
                }
                $AuKept++
            }
        }

        foreach ($UnsupportedMember in @($CurrentAdminUnitMembers | Where-Object { $_.'@odata.type' -notin @('#microsoft.graph.user', '#microsoft.graph.group', '#microsoft.graph.device') })) {
            Write-Warning " [!] Unsupported object type $($UnsupportedMember.'@odata.type') - skipping"
            $WarningMessages.Add([PSCustomObject]@{ Type = "UnsupportedObjectType"; Message = "Unsupported object type $($UnsupportedMember.'@odata.type') in $AdminUnitName - skipped" })
            $AuKept++
        }

        $DesiredDeviceIds = @()
        $CurrentDeviceMemberIds = @()
        $DeviceIdsToAdd = [System.Collections.Generic.List[string]]::new()
        $DeviceRemovalPlan = [System.Collections.Generic.List[psobject]]::new()
        if ($IncludeUnprotectedDevices) {
            $PrivilegedUsersAtTier = $PrivilegedEamObjects | Where-Object {
                $_.ObjectType -eq "user" -and `
                    $_.Classification.AdminTierLevel -contains $TierLevel.AdminTierLevel -and `
                    $_.Classification.AdminTierLevelName -contains $TierLevel.AdminTierLevelName
            }
            $ForeignTenantDeviceOwners = @($PrivilegedUsersAtTier | Where-Object {
                    -not [string]::IsNullOrEmpty($_.ObjectTenantId) -and $_.ObjectTenantId -ne $TenantId
                })
            if ($ForeignTenantDeviceOwners.Count -gt 0) {
                Write-Warning " [SKIP] Excluded devices for $($ForeignTenantDeviceOwners.Count) user(s) not owned by tenant $TenantId"
                $WarningMessages.Add([PSCustomObject]@{ Type = "ForeignTenantDeviceOwner"; Message = "Skipped devices for $($ForeignTenantDeviceOwners.Count) user(s) not owned by tenant $TenantId for $AdminUnitName" })
            }
            $PrivilegedUsersAtTier = @($PrivilegedUsersAtTier | Where-Object {
                    [string]::IsNullOrEmpty($_.ObjectTenantId) -or $_.ObjectTenantId -eq $TenantId
                })
            # Restricted to devices pinned to this tier by $DeviceTierAssignment - a device owned by a user
            # classified at several tiers belongs to the most privileged one only.
            $DesiredDeviceIds = @($PrivilegedUsersAtTier | ForEach-Object { @($_.OwnedDevices) + @($_.AssociatedPawDevice) } | Where-Object { $_ } | Select-Object -Unique | Where-Object { $DeviceTierAssignment[$_] -eq $TierLevel.AdminTierLevel })
            $DeviceMembersUri = "/v1.0/directory/administrativeUnits/$AdminUnitId/members"
            $CurrentDeviceMemberIds = @((Invoke-EntraOpsMsGraphQuery -Method "GET" -Uri "$DeviceMembersUri/microsoft.graph.device" -OutputType PSObject -DisableCache) | Where-Object { $null -ne $_.id } | Select-Object -ExpandProperty id)

            if ($DesiredDeviceIds.Count -gt 0) {
                Write-Host " Checking $($DesiredDeviceIds.Count) device(s) owned by/associated to Tier $($TierLevel.AdminTierLevelName) users..." -ForegroundColor Gray
            }

            foreach ($DeviceId in $DesiredDeviceIds) {
                $Device = Invoke-EntraOpsMsGraphQuery -Method Get -Uri "/v1.0/devices/$DeviceId" -OutputType PSObject -DisableCache -SuppressNotFoundWarning
                if ($null -eq $Device -or [string]::IsNullOrEmpty($Device.id)) {
                    $DeviceNotFoundMessage = if ($IncludeObjectDetails) { "Device $DeviceId was not found in tenant $TenantId" } else { "Device $DeviceId was not found in the target tenant" }
                    Write-Warning " [SKIP] $DeviceNotFoundMessage"
                    $WarningMessages.Add([PSCustomObject]@{ Type = "DeviceNotFound"; Message = "$DeviceNotFoundMessage for $AdminUnitName" })
                    continue
                }
                $DeviceAUs = @(Invoke-EntraOpsMsGraphQuery -Method Get -Uri "/beta/devices/$DeviceId/memberOf/microsoft.graph.administrativeUnit" -OutputType PSObject -DisableCache -SuppressNotFoundWarning | Where-Object { $null -ne $_.id })
                # Treat memberships in sibling EntraOps tier AUs as managed rather than external protection.
                $HasOtherRMAU = ($DeviceAUs | Where-Object { $_.isMemberManagementRestricted -eq $True -and $_.id -ne $AdminUnitId -and $_.displayName -notin $ManagedAdminUnitNames }).Count -gt 0

                if ($HasOtherRMAU) {
                    if ($DeviceId -in $CurrentDeviceMemberIds) {
                        $DeviceRemovalPlan.Add([PSCustomObject]@{ Id = $DeviceId; Reason = "protected by another RMAU" })
                    }
                    continue
                }

                if ($DeviceId -notin $CurrentDeviceMemberIds) {
                    $DeviceIdsToAdd.Add($DeviceId)
                } else {
                    $AuKept++
                }
            }

            foreach ($StaleDeviceId in @($CurrentDeviceMemberIds | Where-Object { $_ -notin $DesiredDeviceIds })) {
                $DeviceRemovalPlan.Add([PSCustomObject]@{ Id = $StaleDeviceId; Reason = "no longer associated to a privileged Tier $($TierLevel.AdminTierLevelName) user" })
            }
        }

        $PlannedRemovalCount = $PrincipalRemovalPlan.Count + $DeviceRemovalPlan.Count
        $SafetyCheck = Test-EntraOpsRemovalSafetyThreshold -CurrentCount $CurrentMemberCount -RemovalCount $PlannedRemovalCount -RemovalSafetyThreshold $RemovalSafetyThreshold
        $SkipRemovals = $SafetyCheck.Exceeds -and -not $ForceRemovalBeyondSafetyThreshold

        Write-Host " Delta: +$($PrincipalIdsToAdd.Count + $DeviceIdsToAdd.Count) to add -$PlannedRemovalCount to remove" -ForegroundColor Yellow
        if ($SkipRemovals) {
            Write-Warning " [ABORT] $PlannedRemovalCount removals exceeds $($SafetyCheck.ThresholdPercent)% safety threshold ($($SafetyCheck.RemovalThreshold) of $CurrentMemberCount members). No removals were applied; additions still proceed. Review the plan, then re-run with -ForceRemovalBeyondSafetyThreshold to apply."
            $WarningMessages.Add([PSCustomObject]@{ Type = "SafetyAbort"; Message = "Aborted removals in $AdminUnitName`: $PlannedRemovalCount exceeds the $($SafetyCheck.ThresholdPercent)% safety threshold ($($SafetyCheck.RemovalThreshold) of $CurrentMemberCount members) - re-run with -ForceRemovalBeyondSafetyThreshold to apply" })
            $AuStatus = "ABORTED"
            $AuKept += $PlannedRemovalCount
        } elseif ($SafetyCheck.Exceeds) {
            Write-Warning " [FORCED] Applying $PlannedRemovalCount removals despite exceeding the $($SafetyCheck.ThresholdPercent)% safety threshold - requested via -ForceRemovalBeyondSafetyThreshold."
            $WarningMessages.Add([PSCustomObject]@{ Type = "SafetyOverride"; Message = "Forced $PlannedRemovalCount removals in $AdminUnitName beyond the $($SafetyCheck.ThresholdPercent)% safety threshold" })
            $AuStatus = "FORCED"
        }

        if (-not $SkipRemovals) {
            foreach ($Removal in $PrincipalRemovalPlan) {
                try {
                    $AdminUnitMember = Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/beta/directoryObjects/$($Removal.Id)" -OutputType PSObject -DisableCache
                    if ($IncludeObjectDetails) {
                        Write-Host " [-] REM [$($Removal.Type)] $($AdminUnitMember.displayName) ($($Removal.Reason))" -ForegroundColor Yellow
                    } else {
                        Write-Host " [-] REM [$($Removal.Type)] $($Removal.Id)" -ForegroundColor Yellow
                    }
                    Invoke-EntraOpsMsGraphQuery -Method DELETE -Uri "/beta/administrativeUnits/$($AdminUnitId)/members/$($Removal.Id)/`$ref" -OutputType PSObject -DisableCache -ThrowOnFailure
                    $AuRemoved++
                } catch {
                    $RemoveFailureMessage = if ($IncludeObjectDetails) { "FAIL REMOVE $($Removal.Id) from $AdminUnitName`: $_" } else { "Failed to remove $($Removal.Type) object $($Removal.Id) from $AdminUnitName" }
                    Write-Warning " [!] $RemoveFailureMessage"
                    $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = $RemoveFailureMessage })
                    $AuFailed++
                }
            }

            foreach ($Removal in $DeviceRemovalPlan) {
                try {
                    Invoke-EntraOpsMsGraphQuery -Method DELETE -Uri "$DeviceMembersUri/$($Removal.Id)/`$ref" -OutputType PSObject -ThrowOnFailure
                    if ($IncludeObjectDetails) {
                        Write-Host " [-] REM [device] $($Removal.Id) ($($Removal.Reason))" -ForegroundColor Yellow
                    } else {
                        Write-Host " [-] REM [device] $($Removal.Id)" -ForegroundColor Yellow
                    }
                    $AuRemoved++
                } catch {
                    $RemoveFailureMessage = if ($IncludeObjectDetails) { "FAIL REMOVE device $($Removal.Id) from $AdminUnitName`: $_" } else { "Failed to remove device $($Removal.Id) from $AdminUnitName" }
                    Write-Warning " [!] $RemoveFailureMessage"
                    $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = $RemoveFailureMessage })
                    $AuFailed++
                }
            }
        }

        foreach ($PrincipalId in $PrincipalIdsToAdd) {
            try {
                $AdminUnitMember = Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/beta/directoryObjects/$PrincipalId" -OutputType PSObject -DisableCache
                $MemberType = $AdminUnitMember.'@odata.type'.Replace('#microsoft.graph.', '')
                if ($IncludeObjectDetails) {
                    Write-Host " [+] ADD [$MemberType] $($AdminUnitMember.displayName)" -ForegroundColor Green
                } else {
                    Write-Host " [+] ADD [$MemberType] $PrincipalId" -ForegroundColor Green
                }
                $OdataBody = @{ '@odata.id' = "https://graph.microsoft.com/beta/directoryObjects/$PrincipalId" } | ConvertTo-Json
                Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/beta/administrativeUnits/$($AdminUnitId)/members/`$ref" -Body $OdataBody -OutputType PSObject -ThrowOnFailure
                $AuAdded++
            } catch {
                $AddFailureMessage = if ($IncludeObjectDetails) { "FAIL ADD $PrincipalId to $AdminUnitName`: $_" } else { "Failed to add principal object $PrincipalId to $AdminUnitName" }
                Write-Warning " [!] $AddFailureMessage"
                $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = $AddFailureMessage })
                $AuFailed++
            }
        }

        foreach ($DeviceId in $DeviceIdsToAdd) {
            try {
                $OdataBody = @{ '@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$DeviceId" } | ConvertTo-Json
                Invoke-EntraOpsMsGraphQuery -Method POST -Uri "$DeviceMembersUri/`$ref" -Body $OdataBody -OutputType PSObject -ThrowOnFailure
                if ($IncludeObjectDetails) {
                    Write-Host " [+] ADD [device] $DeviceId" -ForegroundColor Green
                } else {
                    Write-Host " [+] ADD [device] $DeviceId" -ForegroundColor Green
                }
                $AuAdded++
            } catch {
                $AddFailureMessage = if ($IncludeObjectDetails) { "FAIL ADD device $DeviceId to $AdminUnitName`: $_" } else { "Failed to add device $DeviceId to $AdminUnitName" }
                Write-Warning " [!] $AddFailureMessage"
                $WarningMessages.Add([PSCustomObject]@{ Type = "ApiError"; Message = $AddFailureMessage })
                $AuFailed++
            }
        }

        if ($AuFailed -gt 0) { $AuStatus = "ERRORS" }
        $TotalAdded += $AuAdded
        $TotalRemoved += $AuRemoved
        $TotalKept += $AuKept

        $SyncSummary.Add([PSCustomObject]@{
                AdminUnit     = $AdminUnitName
                MembersBefore = $CurrentMemberCount
                Added         = $AuAdded
                Removed       = $AuRemoved
                Kept          = $AuKept
                Failed        = $AuFailed
                Status        = $AuStatus
            })
    }

    # Final summary
    Write-Host ""
    Write-Host "=========================================================" -ForegroundColor Cyan
    Write-Host " Unprotected Objects RMAU Sync - Complete" -ForegroundColor Cyan
    Write-Host " Total added : $TotalAdded" -ForegroundColor Green
    Write-Host " Total removed: $TotalRemoved" -ForegroundColor Yellow
    if ($TotalKept -gt 0) {
        Write-Host " Kept (no other RMAU): $TotalKept" -ForegroundColor DarkYellow
    }
    if (($SyncSummary | Where-Object { $_.Failed -gt 0 }).Count -gt 0) {
        Write-Host " Failures : $(($SyncSummary | Measure-Object -Property Failed -Sum).Sum)" -ForegroundColor Red
    }
    Write-Host "=========================================================" -ForegroundColor Cyan
    Write-Host ""
    Show-EntraOpsWarningSummary -WarningMessages $WarningMessages -IncludeObjectDetails $IncludeObjectDetails
    $SyncSummary | Format-Table -AutoSize -Property AdminUnit, MembersBefore,
    @{Name = 'Added'; Expression = { $_.Added }; Align = 'Right' },
    @{Name = 'Removed'; Expression = { $_.Removed }; Align = 'Right' },
    @{Name = 'Kept'; Expression = { $_.Kept }; Align = 'Right' },
    @{Name = 'Failed'; Expression = { $_.Failed }; Align = 'Right' },
    Status

    $FailureCount = ($SyncSummary | Measure-Object -Property Failed -Sum).Sum
    $AbortedCount = @($SyncSummary | Where-Object { $_.Status -eq 'ABORTED' }).Count
    if ($FailureCount -gt 0 -or $AbortedCount -gt 0) {
        $Reasons = [System.Collections.Generic.List[string]]::new()
        if ($FailureCount -gt 0) { $Reasons.Add("$FailureCount membership operation(s) failed") | Out-Null }
        if ($AbortedCount -gt 0) { $Reasons.Add("$AbortedCount RMAU synchronization(s) aborted by the removal safety threshold") | Out-Null }
        throw "RMAU sync did not complete: $($Reasons -join '; '). Review the warning summary above."
    }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAOPgtrh9mOwcL+
# 7I19ER7q8QYUQKi3k7YLb/JPM5j3KaCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIN2QnjuQUx5OX3rfkDpNrz9G0EIxKLqLYsPhYEL4
# uCqdMA0GCSqGSIb3DQEBAQUABIIBgGl0uBJu+vEvPYoaRpCZ1qDHCjF/AB7T0wyh
# YX+SS5JRUqB/E21KSOK9rAtc26X/jM8dqUPiz5YpFxZmymt+xB95QX0qpwslypjO
# /oR9nFm4uylIqihuIRDpyHlPQQpfFk0PiPTVXNZBZEVnbRaOBeGR4Od933mxSoXN
# 2EWQ4OFOEPBvGxxopu4n/w0iJBoRW4TRcY9ohbl3znHBgziuwXAGHiYKE2gPBWBA
# r8J/gqNzRaYWi8LlzcwfofeTKsdzUlDEBLXyklw/AJVJuzCJjx7nUMLlSV0c8gsw
# E+mbhDXz8M0OQ+/5SaINWp/7hFjH/Iy4GkZg++CV2Oy2Ger7U8r6BviRtliKmXOz
# mIQtDCIFuh2qwfNvflWqwsb7dfvel5ZlIGQkyPcAjQb0tvgKtpgxgRGHykRxjaOa
# T9/iCUkLcdqrmAla25o+c8gO83QwF7OUayDBFy/TuuGwTDJ8YOLDQzPr9lrTFYFT
# wYonGs9WXdNBIanDwkGAF5ju8/bcqaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxNDRaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDBUVgFnLD/+u+pBdM7as2H+j2xFj3qgaET9
# xSIDwfzf8wfUx+d7NjrV11Kane5YzxEwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAlkeA43fCf5Tb66i/D6+XfE9J44TOw5bOS0lyC1OUZeNPNLm8
# Ay7G/HkYgkyugCgXoYSoeMBcdMRQvAH98JjgX4G6H3T29DFsIiNtHVv5CnDwoR8W
# a0dL5YYCsKyJ7Nyz79ELDbPaqe92QhagwkJoV5Dq4mIKH/ZnezeQGR84YnWfPQaf
# z2yVdDow7ktJ22oEHwsEeg1VzmhZUfcBDYQed5E1PU6f+GMOMIzFqE2KMbMHAoGk
# 2yE4ECLz3DRHOGNXJxvEFy67w58ZHXTz8M6EtOWkz1zTE5uJ8i9f6YcRDI6KK9Vm
# 9B5gWWmN6FXgHNrK7Lku82w10PylF2Rt8vhxpnpsgB/AcZZyHidtF7vQi7KQGGjJ
# CTC/9SGw5ex1whkktfztsttT5zX78TE+1tTaTKAno359QkDESuWoTqVY2UAA5z9R
# 8xRGI8uzmuf6mzjtD9bpYKzm299li4IjrA+uBCGeeGNflrxC86eBSz6MyGOyTojM
# eMa2pmC5MCevK5Pyf2EcAQfuTbSKZPwpySzBVkZhTXItGJWptHNHRqJN6CgfHqrm
# Z7kiJI4kqNDCQAb3WBirSjb0KKtaJ4y1lEGdZJ7yTYf22HDXK9/FXoUceM7KozdE
# a/JKhblMNZkEjWyGJEYiWzom1yOQMwQsuRCvJkehGsHVO0TGQDAFU346rRk=
# SIG # End signature block