Public/PrivilegedAccess/Get-EntraOpsClassificationControlPlaneObjects.ps1
|
<# .SYNOPSIS Returns a deduplicated list of Control Plane objects identified from one or more classification sources. .DESCRIPTION Queries multiple data sources to identify Entra ID objects that hold Control Plane-level permissions and returns them as a unified, deduplicated list with classification metadata (source and reason). Supported sources are: - EntraOps : objects already classified as ControlPlane in the EntraOps EAM JSON files (object-level tier or Control Plane role assignments). - PrivilegedRolesFromAzGraph : permanent Azure RBAC role assignments for high-privileged roles queried via Azure Resource Graph. - PrivilegedEdgesFromExposureManagement : objects with attack-path edges to critical assets in Microsoft Security Exposure Management. - PrivilegedObjectIds : a manually supplied list of Entra object IDs. - All (default) : runs all four sources and merges the results. Each returned object includes ObjectId, ObjectType, ObjectSubType, ObjectDisplayName, ObjectSignInName, management restriction flags, assigned administrative units, and a Classification property that lists every source and reason the object was identified. .PARAMETER PrivilegedObjectClassificationSource One or more sources used to identify Control Plane objects. Valid values: "All", "EntraOps", "PrivilegedObjectIds", "PrivilegedRolesFromAzGraph", "PrivilegedEdgesFromExposureManagement". Defaults to "All". .PARAMETER EntraIdClassificationParameterFile Path to the Entra ID classification parameter template file. Defaults to ./Classification/Templates/Classification_AadResources.Param.json. .PARAMETER EntraIdCustomizedClassificationFile Path to the tenant-specific Entra ID classification file. Defaults to ./Classification/<TenantName>/Classification_AadResources.json, resolved from the active EntraOps tenant context. .PARAMETER EntraOpsEamFolder Path to the root folder containing the EntraOps EAM JSON files (one subfolder per scope). Defaults to the EntraOps default classified EAM folder. .PARAMETER EntraOpsScopes One or more EntraOps RBAC scopes to include when reading EAM data. Valid values: "Azure", "AzureBilling", "EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps", "Defender". Defaults to all available scopes. .PARAMETER AzureHighPrivilegedRoles Azure RBAC role names that are considered high-privileged when using the PrivilegedRolesFromAzGraph source. Defaults to: "Owner", "Role Based Access Control Administrator", "User Access Administrator". .PARAMETER AzureHighPrivilegedScopes Azure resource scopes (management group or subscription paths) to restrict the Azure Resource Graph query. Each configured value is matched exactly against the role assignment's scope; child scopes are not included. Use "*" (default) to include all scopes, including management groups. .PARAMETER ExposureCriticalityLevel KQL comparison expression applied to the criticalityLevel field in Exposure Management when using PrivilegedEdgesFromExposureManagement. Defaults to "<1" (Tier-0 / critical assets). .PARAMETER PrivilegedObjectIds Array of Entra object IDs to classify as Control Plane when using the PrivilegedObjectIds source. .EXAMPLE Return Control Plane objects identified by EntraOps EAM data across all available RBAC scopes. Get-EntraOpsClassificationControlPlaneObjects -PrivilegedObjectClassificationSource "EntraOps" .EXAMPLE Return Control Plane objects identified by EntraOps EAM data for a specific subset of RBAC scopes. Get-EntraOpsClassificationControlPlaneObjects -PrivilegedObjectClassificationSource "EntraOps" -EntraOpsScopes ("Azure", "EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps") .EXAMPLE Return objects with attack-path edges to critical assets (criticalityLevel < 1) in Microsoft Security Exposure Management. Get-EntraOpsClassificationControlPlaneObjects -PrivilegedObjectClassificationSource "PrivilegedEdgesFromExposureManagement" -ExposureCriticalityLevel "<1" .EXAMPLE Return objects holding high-privileged Azure RBAC roles on specific management group or subscription scopes queried via Azure Resource Graph. Get-EntraOpsClassificationControlPlaneObjects -PrivilegedObjectClassificationSource "PrivilegedRolesFromAzGraph" -AzureHighPrivilegedRoles ("Owner", "Role Based Access Control Administrator", "User Access Administrator") -AzureHighPrivilegedScopes ("/", "/providers/microsoft.management/managementgroups/8693dc7e-63c1-47ab-a7ee-acfe488bf52a") .EXAMPLE Return a merged and deduplicated list of Control Plane objects from all supported classification sources. Get-EntraOpsClassificationControlPlaneObjects -PrivilegedObjectClassificationSource "All" .EXAMPLE Return Control Plane objects for a manually compiled list of privileged users and groups. $PrivilegedUsers = Get-AzAdUser -Filter "startswith(DisplayName,'adm')" $PrivilegedGroups = Get-AzAdGroup -Filter "startswith(DisplayName,'prg')" $PrivilegedObjectIds = ($PrivilegedUsers + $PrivilegedGroups).Id Get-EntraOpsClassificationControlPlaneObjects -PrivilegedObjectClassificationSource "PrivilegedObjectIds" -PrivilegedObjectIds $PrivilegedObjectIds #> function Get-EntraOpsClassificationControlPlaneObjects { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet("All", "EntraOps", "PrivilegedObjectIds", "PrivilegedRolesFromAzGraph", "PrivilegedEdgesFromExposureManagement")] [object]$PrivilegedObjectClassificationSource = "All" , [Parameter(Mandatory = $false)] [System.String]$EntraIdClassificationParameterFile = "$DefaultFolderClassification/Templates/Classification_AadResources.Param.json" , [Parameter(Mandatory = $false)] [System.String]$EntraIdCustomizedClassificationFile = "$DefaultFolderClassification/$($TenantNameContext)/Classification_AadResources.json" , [Parameter(Mandatory = $false)] [string]$EntraOpsEamFolder = "$DefaultFolderClassifiedEam" , [Parameter(Mandatory = $false)] [ValidateSet("Azure", "AzureBilling", "EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps", "Defender")] [object]$EntraOpsScopes = ("Azure", "AzureBilling", "EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps", "Defender") , [Parameter(Mandatory = $false)] [object]$AzureHighPrivilegedRoles = ("Owner", "Role Based Access Control Administrator", "User Access Administrator") , [Parameter(Mandatory = $false)] [object]$AzureHighPrivilegedScopes = "*" , [Parameter(Mandatory = $false)] [string]$ExposureCriticalityLevel = "<1" , [Parameter(Mandatory = $false)] [object]$PrivilegedObjectIds , [Parameter(Mandatory = $false)] [System.String]$TenantId = (Get-EntraOpsAzContextValue -Property TenantId) , [Parameter(Mandatory = $false)] [System.Boolean]$EnableParallelProcessing = $true , [Parameter(Mandatory = $false)] [System.Int32]$ParallelThrottleLimit = 10 ) $PrivilegedObjects = @() # Check if classification file custom and/or template file exists, choose custom template for tenant if available if (!(Test-Path -Path "$($DefaultFolderClassification)/$($TenantNameContext)")) { try { New-Item -Path "$($DefaultFolderClassification)/$($TenantNameContext)" -ItemType Directory -Force | Out-Null } catch { Write-Error "Failed to create folder $($EntraIdCustomizedClassificationFile)! $_.Exception.Message" } } #region Get list of all privileged objects in Entra with classification on Control Plane by Custom Security Attribute or EntraOps Classification if ($PrivilegedObjectClassificationSource -eq "All" -or $PrivilegedObjectClassificationSource -contains "EntraOps") { Write-Host "Get privileged objects from EntraOps..." $EntraOpsAllPrivilegedObjects = foreach ($EntraOpsScope in $EntraOpsScopes) { try { Get-Content -Path $EntraOpsEamFolder\$($EntraOpsScope)\$($EntraOpsScope).json -ErrorAction Stop | ConvertFrom-Json -Depth 10 } catch { Write-Warning "No privileged objects found for $($EntraOpsScope) in EntraOps! $_.Exception.Message" } } if ($null -eq $EntraOpsAllPrivilegedObjects) { Write-Warning "No privileged objects found in EntraOps!" } else { $EntraOpsObjectClassification = $EntraOpsAllPrivilegedObjects | Where-Object { $_.ObjectAdminTierLevelName -eq "ControlPlane" } ` | Select-Object -Unique ObjectId, ObjectType, ObjectSubType, ObjectDisplayName, ObjectUserPrincipalName, ObjectTenantId, AssignedAdministrativeUnits, RestrictedManagementByRAG, RestrictedManagementByAadRole, RestrictedManagementByRMAU, OwnedDevices, AssociatedPawDevice ` | ForEach-Object { $PrivilegedObject = $_ $ClassificationReason = @("ObjectAdminTierLevelName") $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ObjectSignInName -Value ($PrivilegedObject.ObjectUserPrincipalName) -Force | Out-Null $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationReason -Value $ClassificationReason -Force | Out-Null $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationSource -Value "EntraOps" -Force | Out-Null return $PrivilegedObject } $PrivilegedObjects += $EntraOpsObjectClassification $EntraOpsRoleClassification = $EntraOpsAllPrivilegedObjects | Where-Object { $_.Classification.AdminTierLevelName -contains "ControlPlane" } ` | ForEach-Object { $PrivilegedObject = $_ | Select-Object ObjectId, ObjectType, ObjectSubType, ObjectDisplayName, ObjectUserPrincipalName, ObjectTenantId, AssignedAdministrativeUnits, RestrictedManagementByRAG, RestrictedManagementByAadRole, RestrictedManagementByRMAU, OwnedDevices, AssociatedPawDevice, RoleSystem $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ObjectSignInName -Value ($PrivilegedObject.ObjectUserPrincipalName) -Force | Out-Null $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationReason -Value ($PrivilegedObject | Select-Object -Unique RoleSystem) -Force | Out-Null $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationSource -Value "EntraOps" -Force | Out-Null return $PrivilegedObject } $PrivilegedObjects += $EntraOpsRoleClassification } } #endregion #region Get list of all privileged objects by Azure Resource Graph if ($PrivilegedObjectClassificationSource -eq "All" -or $PrivilegedObjectClassificationSource -contains "PrivilegedRolesFromAzGraph") { Write-Host "Get privileged objects from Azure Resource Graph..." # Query template and update them with parameter value of high privileged Azure roles and scopes $Query = 'AuthorizationResources | where type =~ "microsoft.authorization/roleassignments" | extend PrincipalType = tolower(tostring(properties["principalType"])) | extend PrincipalId = tostring(properties["principalId"]) | extend RoleDefinitionId = tolower(tostring(properties["roleDefinitionId"])) | extend RoleScope = tolower(tostring(properties["scope"])) | where isnotempty(RoleScope) | join kind=inner ( AuthorizationResources | where type =~ "microsoft.authorization/roledefinitions" | extend RoleDefinitionId = tolower(id) | extend RoleName = (properties.roleName) | where RoleName in (%AzureHighPrivilegedRoles%) ) on RoleDefinitionId | project PrincipalId, PrincipalType, RoleScope, RoleName' $Query = $Query.Replace("%AzureHighPrivilegedRoles%", "'$($AzureHighPrivilegedRoles -join "', '")'") if ($null -ne $AzureHighPrivilegedScopes -and $AzureHighPrivilegedScopes -ne "*") { $Scopes = "'$($AzureHighPrivilegedScopes -join "', '")'" $Query = $Query.Replace("isnotempty(RoleScope)", "RoleScope in ($($Scopes))") } # Get details of high privileged objects # Fail rather than truncate: a partial result here drops privileged principals from the # Control Plane object list, which is indistinguishable from them not being privileged. $HighPrivilegedObjectIdsFromAzGraph = (Invoke-EntraOpsAzGraphQuery -KqlQuery $Query -ThrowOnFailure) $UniqueHighPrivilegedObjects = @($HighPrivilegedObjectIdsFromAzGraph | Select-Object -Unique PrincipalId, PrincipalType | Where-Object { -not [string]::IsNullOrEmpty($_.PrincipalId) } | ForEach-Object { [PSCustomObject]@{ ObjectId = $_.PrincipalId; ObjectType = $_.PrincipalType } }) $AzGraphObjectDetailsCache = @{} if ($UniqueHighPrivilegedObjects.Count -gt 0) { $AzGraphObjectDetailsCache = Invoke-EntraOpsParallelObjectResolution -UniqueObjects $UniqueHighPrivilegedObjects -TenantId $TenantId -EnableParallelProcessing $EnableParallelProcessing -ParallelThrottleLimit $ParallelThrottleLimit } $PrivilegedObjects += $UniqueHighPrivilegedObjects | ForEach-Object { $HighPrivilegedObjectId = $_ $ResolvedObject = $AzGraphObjectDetailsCache[$HighPrivilegedObjectId.ObjectId] if ($null -ne $ResolvedObject -and $ResolvedObject.ObjectType -ne "unknown") { # Azure Resource Graph gives no row-order guarantee for joins, so RoleScope/RoleName pairs can # come back in a different order on every run - sort deterministically to avoid a spurious diff # in ScopeReasoning_ControlPlane.json each time the classification is regenerated with unchanged data. $HighPrivilegedRoles = $HighPrivilegedObjectIdsFromAzGraph | Where-Object { $_.PrincipalId -eq $HighPrivilegedObjectId.ObjectId -and $_.PrincipalType -eq $HighPrivilegedObjectId.ObjectType } | Sort-Object RoleScope, RoleName | Select-Object -Unique RoleScope, RoleName $PrivilegedObject = $ResolvedObject | Select-Object ObjectId, @{Name = 'ObjectType'; Expression = { $_.'ObjectType'.tolower() } }, ObjectSubType, ObjectDisplayName, ObjectSignInName, ObjectTenantId, AssignedAdministrativeUnits, RestrictedManagementByRAG, RestrictedManagementByAadRole, RestrictedManagementByRMAU, OwnedDevices, AssociatedPawDevice $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationReason -Value $HighPrivilegedRoles -Force | Out-Null $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationSource -Value "Azure Resource Graph" -Force | Out-Null $PrivilegedObject } else { Write-Warning "High privileged object with id $($HighPrivilegedObjectId.ObjectId) not found!" } } } #endregion #region Get list of all privileged objects by Microsoft Exposure Management if ($PrivilegedObjectClassificationSource -eq "All" -or $PrivilegedObjectClassificationSource -contains "PrivilegedEdgesFromExposureManagement") { Write-Host "Get privileged objects from exposure graph edges and nodes in Exposure Management..." $Timespan = "P1D" # Performance: single-pass Tier0Assets scan (was 3 full ExposureGraphNodes scans unioned) and, # most importantly, the node join side is pre-filtered to just the filtered edges' source nodes # BEFORE the expensive mv-expand over EntityIds - previously the mv-expand ran over the ENTIRE # ExposureGraphNodes table (every resource/device/identity node), which dominated query runtime. $Query = ' let Tier0Assets = ExposureGraphNodes | where isnotnull(NodeProperties.rawData.criticalityLevel) and (NodeProperties.rawData.criticalityLevel.criticalityLevel %CriticalLevel%) | where (NodeLabel != "device" and parse_json(Categories) !has "identities") or (NodeProperties.rawData.primaryProvider == "AzureActiveDirectory") or (NodeLabel == "device" and NodeProperties.rawData.isAzureADJoined == true) | project NodeId; let SensitiveRelation = dynamic(["has permissions to","can authenticate as","has role on","has credentials of","affecting", "can authenticate as", "Member of", "frequently logged in by"]); // Devices are not supported yet, no AadObject Id available in ExposureGraphNodes, DeviceInfo shows only AadDeviceId let FilteredNodes = dynamic(["user","group","serviceprincipal","managedidentity","device"]); let SensitiveEdges = ExposureGraphEdges | where EdgeLabel in (SensitiveRelation) and SourceNodeLabel in (FilteredNodes) | where TargetNodeId in (Tier0Assets) or SourceNodeId in (Tier0Assets); let SensitiveSourceNodeIds = SensitiveEdges | distinct SourceNodeId; SensitiveEdges | join kind=leftouter ( ExposureGraphNodes | where NodeId in (SensitiveSourceNodeIds) | mv-expand parse_json(EntityIds) | where parse_json(EntityIds).type == "AadObjectId" | extend AadObjectId = tostring(parse_json(EntityIds).id) | extend TenantId = extract("tenantid=([\\w-]+)", 1, AadObjectId) | extend ObjectId = extract("objectid=([\\w-]+)", 1, AadObjectId) | project ObjectDisplayName = NodeName, ObjectType = NodeLabel, ObjectId, NodeId) on $left.SourceNodeId == $right.NodeId | where isnotempty(ObjectId) | extend ClassificationReason = bag_pack_columns(EdgeLabel, TargetNodeName) | summarize by ObjectDisplayName, SourceNodeName, tolower(ObjectType), ObjectId, NodeId, tostring(ClassificationReason)' $Query = $Query.Replace("%CriticalLevel%", $ExposureCriticalityLevel) $Body = @{ "Query" = $Query; "Timespan" = $Timespan; } | ConvertTo-Json $PrivilegedObjectsGraphEdges = (Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/beta/security/runHuntingQuery" -Body $Body).results if ($null -ne $PrivilegedObjectsGraphEdges) { $UniqueGraphEdgeObjects = @($PrivilegedObjectsGraphEdges | Select-Object -Unique ObjectDisplayName, ObjectId, ObjectType | Where-Object { -not [string]::IsNullOrEmpty($_.ObjectId) }) $XspmObjectDetailsCache = @{} if ($UniqueGraphEdgeObjects.Count -gt 0) { $XspmResolutionObjects = @($UniqueGraphEdgeObjects | ForEach-Object { [PSCustomObject]@{ ObjectId = $_.ObjectId; ObjectType = $_.ObjectType } }) $XspmObjectDetailsCache = Invoke-EntraOpsParallelObjectResolution -UniqueObjects $XspmResolutionObjects -TenantId $TenantId -EnableParallelProcessing $EnableParallelProcessing -ParallelThrottleLimit $ParallelThrottleLimit } $PrivilegedObjects += $UniqueGraphEdgeObjects | ForEach-Object { $GraphEdge = $_ # Normalize ObjectType casing (Get-EntraOpsPrivilegedEntraObject can return mixed-case values, # e.g. from the Graph @odata.type) so the same object isn't later treated as two distinct # entries by the ObjectId+ObjectType uniqueness check below. $ResolvedObject = $XspmObjectDetailsCache[$GraphEdge.ObjectId] if ($null -ne $ResolvedObject -and $ResolvedObject.ObjectType -ne "unknown") { $PrivilegedObject = $ResolvedObject | Select-Object ObjectId, @{Name = 'ObjectType'; Expression = { $_.'ObjectType'.tolower() } }, ObjectSubType, ObjectDisplayName, ObjectSignInName, ObjectTenantId, AssignedAdministrativeUnits, RestrictedManagementByRAG, RestrictedManagementByAadRole, RestrictedManagementByRMAU, OwnedDevices, AssociatedPawDevice $ClassificationReason = @() $ClassificationReason += ($PrivilegedObjectsGraphEdges | Where-Object { $_.ObjectId -eq $GraphEdge.ObjectId -and $_.ObjectType -eq $GraphEdge.ObjectType }).ClassificationReason | ConvertFrom-Json # Kusto's "summarize by" gives no ordering guarantee, so EdgeLabel/TargetNodeName pairs can come # back in a different order on every run - sort deterministically to avoid a spurious diff in # ScopeReasoning_ControlPlane.json each time the classification is regenerated with unchanged data. $ClassificationReason = @($ClassificationReason | Sort-Object EdgeLabel, TargetNodeName) $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationReason -Value $ClassificationReason -Force | Out-Null $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationSource -Value "XSPM" -Force | Out-Null $PrivilegedObject } } } } #endregion #region Get list of all privileged objects by manual list of ObjectIds if ($PrivilegedObjectClassificationSource -contains "PrivilegedObjectIds" -and $null -ne $PrivilegedObjectIds) { Write-Host "Get privileged objects from manual list of object ids..." $PrivilegedObjects += $PrivilegedObjectIds | ForEach-Object { $PrivilegedObject = Get-EntraOpsPrivilegedEntraObject -AadObjectId $_ # Normalize ObjectType casing for the same reason as the XSPM source above. $PrivilegedObject.ObjectType = $PrivilegedObject.ObjectType.tolower() $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationReason -Value @("Manual") -Force | Out-Null $PrivilegedObject | Add-Member -MemberType NoteProperty -Name ClassificationSource -Value "Manual" -Force | Out-Null return $PrivilegedObject } } #endregion #region Display summary of identified Control Plane objects Write-Host "`n=== Control Plane Classification Summary ===" -ForegroundColor Cyan $UniqueTotalCount = ($PrivilegedObjects | Select-Object -Unique ObjectId).Count Write-Host "Total unique privileged objects identified: $UniqueTotalCount`n" -ForegroundColor White $PrivilegedObjects | Group-Object ClassificationSource | ForEach-Object { $SourceGroup = $_ $UniqueSourceObjects = $SourceGroup.Group | Select-Object -Unique ObjectId, ObjectType, ObjectDisplayName Write-Host " [Source: $($SourceGroup.Name)] - $($UniqueSourceObjects.Count) unique object(s)" -ForegroundColor Yellow switch ($SourceGroup.Name) { "EntraOps" { Write-Host " Analyzed EntraOps scopes: $($EntraOpsScopes -join ', ')" -ForegroundColor Gray $ObjectLevelClassified = $SourceGroup.Group | Where-Object { $_.ClassificationReason -contains "ObjectAdminTierLevelName" } | Select-Object -Unique ObjectId $RoleLevelClassified = $SourceGroup.Group | Where-Object { $_.ClassificationReason -notcontains "ObjectAdminTierLevelName" } | Select-Object -Unique ObjectId if ($ObjectLevelClassified.Count -gt 0) { Write-Host " Classified by object-level Control Plane tier (ObjectAdminTierLevelName): $($ObjectLevelClassified.Count) object(s)" -ForegroundColor Gray } if ($RoleLevelClassified.Count -gt 0) { $RoleSystems = $SourceGroup.Group | Where-Object { $_.ClassificationReason -notcontains "ObjectAdminTierLevelName" } ` | ForEach-Object { $_.ClassificationReason } | Where-Object { $null -ne $_.RoleSystem } ` | Select-Object -ExpandProperty RoleSystem -Unique | Sort-Object Write-Host " Classified by Control Plane role assignments: $($RoleLevelClassified.Count) object(s)" -ForegroundColor Gray if ($RoleSystems) { Write-Host " RBAC systems with Control Plane assignments: $($RoleSystems -join ', ')" -ForegroundColor Gray } } } "Azure Resource Graph" { Write-Host " High privileged roles analyzed: $($AzureHighPrivilegedRoles -join ', ')" -ForegroundColor Gray if ($AzureHighPrivilegedScopes -eq "*") { Write-Host " Azure scopes: All scopes (including management groups)" -ForegroundColor Gray } else { Write-Host " Azure scopes: $($AzureHighPrivilegedScopes -join ', ')" -ForegroundColor Gray } $AssignedRoles = $SourceGroup.Group | ForEach-Object { $_.ClassificationReason } ` | Where-Object { $null -ne $_.RoleName } | Select-Object -ExpandProperty RoleName -Unique | Sort-Object if ($AssignedRoles) { Write-Host " Roles found assigned: $($AssignedRoles -join ', ')" -ForegroundColor Gray } } "XSPM" { Write-Host " Exposure criticality level filter: $ExposureCriticalityLevel" -ForegroundColor Gray $EdgeLabels = $SourceGroup.Group | ForEach-Object { $_.ClassificationReason } ` | Where-Object { $null -ne $_.EdgeLabel } | Select-Object -ExpandProperty EdgeLabel -Unique | Sort-Object if ($EdgeLabels) { Write-Host " Exposure edge relations identified: $($EdgeLabels -join ', ')" -ForegroundColor Gray } } "Manual" { Write-Host " Classification based on manually specified privileged object IDs" -ForegroundColor Gray } } $UniqueSourceObjects | Group-Object ObjectType | Sort-Object Name | ForEach-Object { Write-Host " Object types: $($_.Count) $($_.Name)(s)" -ForegroundColor Gray } Write-Host "" } Write-Host "==========================================`n" -ForegroundColor Cyan #endregion #region Summarize and return list of privileged objects # Sort deterministically (ObjectId as final tiebreaker) so the output order is stable across runs - # otherwise objects sharing the same ObjectType/ObjectDisplayName (e.g. same-named managed identities) # can swap positions between runs purely due to upstream API pagination/enumeration order, producing # spurious diff noise with no actual data change. $PrivilegedObjects = $PrivilegedObjects | Sort-Object ObjectType, ObjectDisplayName, ObjectId $PrivilegedObjects | Select-Object -Unique ObjectId, ObjectType, ObjectSubType, ObjectDisplayName, ObjectSignInName, ObjectTenantId, RestrictedManagementByAadRole, RestrictedManagementByRAG, RestrictedManagementByRMAU, OwnedDevices, AssociatedPawDevice, AssignedAdministrativeUnits | ForEach-Object { $PrivilegedObject = $_ $Classifications = $PrivilegedObjects | Where-Object { $_.ObjectId -eq $PrivilegedObject.ObjectId -and $_.ObjectType -eq $PrivilegedObject.ObjectType } | select-object ClassificationReason, ClassificationSource # An object can carry multiple classification-reason entries (one per originating source: EntraOps # per-RBAC-scope hits, Azure Resource Graph, XSPM edges, ...), collected here via a plain filter with no # inherent order. Since ClassificationReason/ClassificationSource mix scalars and differently-shaped # objects (RoleSystem vs EdgeLabel/TargetNodeName), sort by their compact JSON representation - this is a # function of content only, so the order is stable across runs regardless of upstream arrival order # (notably the XSPM Kusto query, which gives no row-order guarantee). $Classifications = @($Classifications | Sort-Object { $_ | ConvertTo-Json -Compress -Depth 5 }) $PrivilegedObject | Add-Member -MemberType NoteProperty -Name Classification -Value $Classifications -Force | Out-Null return $PrivilegedObject } #endregion } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBTqOF5zzOVu8tJ # KgQWL7XiSKKSY8+gDTI5fZBKrfSgZKCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # 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 # ARUwLwYJKoZIhvcNAQkEMSIEIAIWLWUhtJSmHZx+atNRYicYRCPQsKehuKGgmzc9 # vCMGMA0GCSqGSIb3DQEBAQUABIIBgFaPMoalFFZi+TcSqkIo/f52sSALoFAf1cv6 # J14ZE6OXC3vjOpmbmx0umkl4UEiyuRfUfD0TpNBDse3u/GBZnUL29pOrJB07ZlV8 # +GM8Y/pKz6QgPI3/zHKNipIaKEucWxVu7SeoPLm8BhWw75lXIeZTdwwzjo/17821 # 8H7bRwuO92yt4imIS7ZxNmX1EaHxLXv7aA7fGXTb10E5OuQkbHNR4jK8rgZJ0tHn # +xjJUIvHO2xp+Cslu1xnoTrWUVNumOtx4MJNwpMuoGI7TkuZs4M3gaoTw9QR04Gs # /YKMRIkqgwzQRQ+hLEBC8mqhKLBchpWhE1i2IifLolnK8/B9+fVVquIRWF9viD64 # 6qkVfe8mq5QhV1St9vLItuPscv4uBfK2JmScXAfOQtlZRhi47o9d03Ipgj0imJ40 # K8mHeWyn40MYHFLh1hFEdYNOW5Y9p/7bEu2K+mSlJon+enTVs+I2zVXmvh9IHJfF # pzaSjUI+Ayzs9jI/DgAjjZHsdbw0WaGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxMDdaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDDhn8EI20hPojPKBHOtmkK0nW9CQ/wzJA6h # 8ML3qv0Kix/+KZHXqPSOmR0MB3GDrk4wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIASwV2ENa62TG/zS544JZ6aMx1u0NWrr0YJiHsfKct5Oc/Mzb5 # vIHvptkz6DLhsc9VbKObrWZrHS7arOIsXkTc+Lnm2nqdTBAsWMj6lQC3vXEUS4Dy # GbZwaYDoMvaETpbsqHpK+/b5NJKG1bhCIaqXF0rnIG/a75z8JkIhwbAuY8ezMpUB # EH6PC09JiLCAhJlDpReiPV4qgJjXhbnNE80I1I3TQUxpYmpcWlw3nG2i1P2F34QH # xX7ct8tm3gbgO8aIFqhPftI7S1IREfsUpTaBzYaC50cfdeU1YSPfz4oTHc0yi+0p # imJ29gnIrhdXimH1Ff1FiPD7Q9v2kadtbqzyOSFbsDq4lN6BWmHUnY6NUr0J/ooS # H2fhScyU/whijjm6gK6xjchjpbBeLlcxGkNiR1/0jkU0W6KicSgayvf+DaeHDa9A # K3aK/cfJSg65tB+5cKjATLgihNOc3MDMsH/Pwfw1ti8XmP/nwd7DsBXy8aizRWu6 # c03K5s6X14GOM4pAlcRz1suoQ+FNvSmd6Fpvj3t0RQrE8l6FaiPVv1OM6Y44gIFv # TTBKsw/YcY4bh3WPWyJ2ey74aJpr5okvQaZ1lD+87be7MkZ3MtSqs4i0whd+QUwb # GfTotg8RdKEi3UlCTLS1j/bMpS9wYenFJHr7ELCmYU3aPThyQ7amFFZkxSA= # SIG # End signature block |