Public/PrivilegedAccess/Export-EntraOpsPrivilegedEAMBloodHound.ps1
|
<# .SYNOPSIS Export EntraOps Privileged EAM data as a BloodHound OpenGraph JSON file. .DESCRIPTION Reads the existing EAM JSON export files and converts them into a BloodHound OpenGraph-compatible JSON payload (graph.nodes + graph.edges) that can be uploaded directly to BloodHound CE or Enterprise via the standard UI upload or the BloodHound API. The payload is intended to enrich an existing AzureHound-ingested tenant graph. Node kinds emitted: AzureHound-native principals: AZUser, AZGroup, AZServicePrincipal, AZDevice AzureHound-native Entra ID role nodes: AZRole EntraOps role nodes: EO_DefenderRole, EO_IntuneRole, EO_IdGovRole, EO_AppRole EntraOps role-assignment nodes: EO_EntraRoleAssignment, EO_DefenderRoleAssignment, EO_IntuneRoleAssignment, EO_IdGovRoleAssignment, EO_AppRoleAssignment EntraOps other nodes: EO_AdministrativeUnit, EO_Base Edge kinds emitted: EO_HasEntraRole - principal holds an active Entra ID directory role EO_HasDefenderRole - principal holds an active Defender RBAC role EO_HasIntuneRole - principal holds an active Intune / Device Management role EO_HasIdGovRole - principal holds an active Identity Governance role EO_HasAppRole - principal holds an active Resource App API permission role EO_EligibleForEntraRole - principal is PIM-eligible for an Entra ID directory role EO_EligibleForDefenderRole - principal is PIM-eligible for a Defender RBAC role EO_EligibleForIntuneRole - principal is PIM-eligible for an Intune role EO_EligibleForIdGovRole - principal is PIM-eligible for an Identity Governance role EO_EligibleForAppRole - principal is PIM-eligible for a Resource App role EO_Has*RoleAssignment - principal has a concrete EntraOps role-assignment node EO_*RoleAssigned - role definition is linked to its role-assignment node EO_ClassifiedViaObject - principal or role assignment was tier-classified because of a tagged object EO_ScopedTo - role assignment is scoped to an administrative unit or tenant EO_AssignedToAdministrativeUnit - principal is a member of an administrative unit EO_HasWorkAccount - privileged account is linked to a standard work account EO_UsesPAW - privileged account is assigned a PAW/SAW device EO_PAWFor - PAW/SAW device is assigned to a privileged account EO_OwnsDevice - principal owns / has registered a device EO_DeviceOwner - registered device is owned by a principal EO_OwnerOf - principal owns another privileged object (group, application/service principal, ...) EO_OwnedBy - privileged object is owned by another principal (inverse of EO_OwnerOf) EO_IsSponsoredBy - guest or external identity is sponsored by another principal EO_HasIdentityParent - identity was derived from / linked to a parent identity EO_IntuneRolePermission - Intune role assignment/principal has matched actions scoped to a device Entra ID role definitions use the AzureHound-native AZRole kind so EntraOps enriches the canonical BloodHound node instead of trying to change its kind. EntraOps-owned EO_HasEntraRole, EO_EligibleForEntraRole, EO_EntraRoleAssignment, EO_HasEntraRoleAssignment, and EO_EntraRoleAssigned preserve the additional assignment and classification context. Top-level EntraOps tier/name properties are stored on nodes; per-assignment classification details travel as edge properties. .PARAMETER ImportPath Root folder that holds the per-RBAC-system export sub-folders produced by Save-EntraOpsPrivilegedEAMJson. Defaults to $DefaultFolderClassifiedEam. .PARAMETER RbacSystems Which RBAC systems to include. Defaults to all five supported systems. .PARAMETER OutputPath Where to write the resulting JSON file. Defaults to <ImportPath>/BloodHound/EntraOps_OpenGraph.json. .PARAMETER IncludeClassifiedViaObjectEdges When $true (default), emit EO_ClassifiedViaObject edges from principals to the objects referenced in TaggedByObjectIds. Set to $false to reduce graph size when you only need the role-assignment edges. .PARAMETER IncludeAdministrativeUnitEdges When $true (default), emit EO_ScopedTo edges for administrative unit and tenant-wide role assignment scopes, and EO_AdministrativeUnit nodes for AU scopes. Set to $false to omit scope edges and AU scope nodes. .PARAMETER IncludeAdministrativeUnitMembershipEdges When $true (default), emit EO_AssignedToAdministrativeUnit edges from each principal to the administrative units it is a direct member of (from AssignedAdministrativeUnits). Set to $false to omit these edges. .PARAMETER IncludeWorkAccountEdges When $true (default), emit EO_HasWorkAccount edges from a privileged account to its associated standard work account (AssociatedWorkAccount) and EO_UsesPAW edges to assigned PAW/SAW devices (AssociatedPawDevice). Both are populated from custom security attributes. Set to $false to omit these edges. .PARAMETER IncludeDeviceOwnershipEdges When $true (default), emit EO_OwnsDevice edges from a principal to each device it has registered or owns (OwnedDevices), plus inverse EO_DeviceOwner edges from the device to the principal. Set to $false to omit these edges. .PARAMETER IncludeObjectOwnershipEdges When $true (default), emit EO_OwnerOf edges from a principal to each privileged object it owns (OwnedObjects - typically groups, applications/service principals it manages) and from each owner listed on a privileged object (Owners - e.g. a role-assignable group's owners), plus inverse EO_OwnedBy edges. Set to $false to omit these edges. .PARAMETER IncludeSponsorEdges When $true (default), emit EO_IsSponsoredBy edges from a guest or external identity to each of its sponsors (Sponsors). Set to $false to omit these edges. .PARAMETER IncludeIdentityParentEdges When $true (default), emit EO_HasIdentityParent edges from an identity to its parent identity (IdentityParent), e.g. an agent service principal linked back to the originating application. Set to $false to omit these edges. .PARAMETER IncludeDeviceActionEdges When $true (default), emit EO_IntuneRolePermission edges for DeviceManagement assignments where classification matched Intune actions to scoped devices. Edges are emitted from both the role-assignment node and the principal to the target device. Set to $false to omit these edges. .EXAMPLE Export-EntraOpsPrivilegedEAMBloodHound # Writes EntraOps_OpenGraph.json to the default BloodHound sub-folder. .EXAMPLE Export-EntraOpsPrivilegedEAMBloodHound -RbacSystems @("EntraID","ResourceApps") -OutputPath "C:\Temp\entraops.json" .EXAMPLE Export-EntraOpsPrivilegedEAMBloodHound -IncludeClassifiedViaObjectEdges $false #> function Export-EntraOpsPrivilegedEAMBloodHound { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String]$TenantId , [Parameter(Mandatory = $false)] [System.String]$ImportPath = $DefaultFolderClassifiedEam , [Parameter(Mandatory = $false)] [ValidateSet("EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps", "Defender")] [Array]$RbacSystems = @("EntraID", "IdentityGovernance", "DeviceManagement", "ResourceApps", "Defender") , [Parameter(Mandatory = $false)] [System.String]$OutputPath = "" , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeClassifiedViaObjectEdges = $true , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeAdministrativeUnitEdges = $true , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeAdministrativeUnitMembershipEdges = $true , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeWorkAccountEdges = $true , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeDeviceOwnershipEdges = $true , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeObjectOwnershipEdges = $true , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeSponsorEdges = $true , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeIdentityParentEdges = $true , [Parameter(Mandatory = $false)] [System.Boolean]$IncludeDeviceActionEdges = $true ) # ── Default output path ──────────────────────────────────────────────────── if ([string]::IsNullOrEmpty($OutputPath)) { $OutputDir = Join-Path $ImportPath "BloodHound" if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } $OutputPath = Join-Path $OutputDir "EntraOps_OpenGraph.json" } # ── Lookup tables ────────────────────────────────────────────────────────── $HasRoleEdgeKindMap = @{ 'EntraID' = 'EO_HasEntraRole' 'Defender' = 'EO_HasDefenderRole' 'DeviceManagement' = 'EO_HasIntuneRole' 'IdentityGovernance' = 'EO_HasIdGovRole' 'ResourceApps' = 'EO_HasAppRole' } $HasRoleAssignmentEdgeKindMap = @{ 'EntraID' = 'EO_HasEntraRoleAssignment' 'Defender' = 'EO_HasDefenderRoleAssignment' 'DeviceManagement' = 'EO_HasIntuneRoleAssignment' 'IdentityGovernance' = 'EO_HasIdGovRoleAssignment' 'ResourceApps' = 'EO_HasAppRoleAssignment' } $RoleAssignedEdgeKindMap = @{ 'EntraID' = 'EO_EntraRoleAssigned' 'Defender' = 'EO_DefenderRoleAssigned' 'DeviceManagement' = 'EO_IntuneRoleAssigned' 'IdentityGovernance' = 'EO_IdGovRoleAssigned' 'ResourceApps' = 'EO_AppRoleAssigned' } # Eligible edge kinds (PIM-eligible assignments) $EligibleEdgeKindMap = @{ 'EntraID' = 'EO_EligibleForEntraRole' 'Defender' = 'EO_EligibleForDefenderRole' 'DeviceManagement' = 'EO_EligibleForIntuneRole' 'IdentityGovernance' = 'EO_EligibleForIdGovRole' 'ResourceApps' = 'EO_EligibleForAppRole' } $RoleNodeKindMap = @{ 'EntraID' = @('AZRole') 'Defender' = @('EO_DefenderRole') 'DeviceManagement' = @('EO_IntuneRole') 'IdentityGovernance' = @('EO_IdGovRole') 'ResourceApps' = @('EO_AppRole') } $RoleAssignmentNodeKindMap = @{ 'EntraID' = @('EO_EntraRoleAssignment') 'Defender' = @('EO_DefenderRoleAssignment') 'DeviceManagement' = @('EO_IntuneRoleAssignment') 'IdentityGovernance' = @('EO_IdGovRoleAssignment') 'ResourceApps' = @('EO_AppRoleAssignment') } # Node kinds $NodeKind = @{ Base = 'EO_Base' AdministrativeUnit = 'EO_AdministrativeUnit' } # Edge kinds $EdgeKind = @{ MemberOf = 'AZMemberOf' ScopedTo = 'EO_ScopedTo' AssignedToAU = 'EO_AssignedToAdministrativeUnit' ClassifiedViaObject = 'EO_ClassifiedViaObject' HasWorkAccount = 'EO_HasWorkAccount' UsesPAW = 'EO_UsesPAW' PAWFor = 'EO_PAWFor' OwnsDevice = 'EO_OwnsDevice' DeviceOwner = 'EO_DeviceOwner' OwnerOf = 'EO_OwnerOf' OwnedBy = 'EO_OwnedBy' IsSponsoredBy = 'EO_IsSponsoredBy' HasIdentityParent = 'EO_HasIdentityParent' IntuneRolePermission = 'EO_IntuneRolePermission' } # ObjectType → primary node kind $PrincipalKindMap = @{ 'user' = @('AZUser') 'group' = @('AZGroup') 'serviceprincipal' = @('AZServicePrincipal') 'device' = @('AZDevice') } # ── Graph data structures ────────────────────────────────────────────────── # $NodesIndex : id (string) → node hashtable (deduplicated) # $EdgesIndex : "startId|endId|kind" → $true (deduplicated) # $EdgesList : ordered list of edge objects $NodesIndex = @{} $EdgesIndex = @{} $EdgesList = [System.Collections.Generic.List[object]]::new() # ── Precomputed edge kind unions for Cypher queries in node properties ────── $AllActiveRoleEdgeKinds = ($HasRoleEdgeKindMap.Values) -join '|' $AllEligibleRoleEdgeKinds = ($EligibleEdgeKindMap.Values) -join '|' $AllRoleAssignmentEdges = ($HasRoleAssignmentEdgeKindMap.Values) -join '|' $AllRoleAssignedEdges = ($RoleAssignedEdgeKindMap.Values) -join '|' # ── Helper: upsert a node ───────────────────────────────────────────────── # Keeps the first-seen properties; later calls can update only if the existing # node lacks admintierlevel (i.e. it was a stub created for a referenced object). function Upsert-Node { param( [string]$Id, [string[]]$Kinds, [hashtable]$Properties ) $Id = $Id.ToUpper() if (-not $NodesIndex.ContainsKey($Id)) { $NodesIndex[$Id] = @{ id = $Id kinds = $Kinds properties = $Properties } } else { $existing = $NodesIndex[$Id] # Upgrade a stub node when: # - existing is EO_Base only and new data has a more specific kind, OR # - existing lacks admintierlevel and new data provides it $existingIsBaseOnly = ($existing.kinds.Count -eq 1 -and $existing.kinds[0] -eq $NodeKind.Base) $newIsMoreSpecific = ($Kinds.Count -gt 0 -and $Kinds[0] -ne $NodeKind.Base) $upgradeByTier = ($null -eq $existing.properties['admintierlevel']) -and ($null -ne $Properties['admintierlevel']) if (($existingIsBaseOnly -and $newIsMoreSpecific) -or $upgradeByTier) { $NodesIndex[$Id] = @{ id = $Id kinds = $Kinds properties = $Properties } } } } function Ensure-AZDeviceNode { param([string]$Id) if ([string]::IsNullOrEmpty($Id)) { return } Upsert-Node -Id $Id -Kinds @('AZDevice') -Properties @{} } # ── Helper: append a deduplicated edge ───────────────────────────────────── function Add-Edge { param( [string]$StartId, [string]$EndId, [string]$Kind, [hashtable]$Properties = $null ) if ([string]::IsNullOrEmpty($StartId) -or [string]::IsNullOrEmpty($EndId)) { return } $StartId = $StartId.ToUpper() $EndId = $EndId.ToUpper() $dedupeKey = "${StartId}|${EndId}|${Kind}" if (-not $EdgesIndex.ContainsKey($dedupeKey)) { $EdgesIndex[$dedupeKey] = $true $edge = @{ kind = $Kind start = @{ match_by = "id"; value = $StartId } end = @{ match_by = "id"; value = $EndId } } if ($null -ne $Properties -and $Properties.Count -gt 0) { $edge['properties'] = $Properties } $EdgesList.Add($edge) | Out-Null } } function Get-EndpointDedupeKey { # Build a deterministic key from an endpoint dictionary (sorted by key name) # to avoid duplicate edges caused by non-deterministic hashtable enumeration order in ConvertTo-Json param([System.Collections.IDictionary]$Endpoint) $pairs = foreach ($key in ($Endpoint.Keys | Sort-Object)) { $value = $Endpoint[$key] if ($value -is [System.Collections.IDictionary] -or ($value -is [System.Collections.IEnumerable] -and $value -isnot [string])) { $value = $value | ConvertTo-Json -Compress -Depth 5 } "$key=$value" } return ($pairs -join ';') } function Add-EdgeByEndpoint { param( [System.Collections.IDictionary]$StartEndpoint, [System.Collections.IDictionary]$EndEndpoint, [string]$Kind, [hashtable]$Properties = $null ) if ($null -eq $StartEndpoint -or $null -eq $EndEndpoint) { return } $startKey = Get-EndpointDedupeKey -Endpoint $StartEndpoint $endKey = Get-EndpointDedupeKey -Endpoint $EndEndpoint $dedupeKey = "${startKey}|${endKey}|${Kind}" if (-not $EdgesIndex.ContainsKey($dedupeKey)) { $EdgesIndex[$dedupeKey] = $true $edge = @{ kind = $Kind start = $StartEndpoint end = $EndEndpoint } if ($null -ne $Properties -and $Properties.Count -gt 0) { $edge['properties'] = $Properties } $EdgesList.Add($edge) | Out-Null } } function ConvertTo-PimMemberOfEdgeProperties { param( [object]$RoleAssignment, [string]$RbacSystem ) return @{ rbacsystem = [string]$RbacSystem roleassignmenttype = [string]($RoleAssignment.RoleAssignmentType ?? '') roleassignmentsubtype = [string]($RoleAssignment.RoleAssignmentSubType ?? '') pimassignmenttype = [string]($RoleAssignment.PIMAssignmentType ?? '') roleassignmentid = [string]($RoleAssignment.RoleAssignmentId ?? '') roleassignmentinstanceid = [string]($RoleAssignment.RoleAssignmentInstanceId ?? '') roleassignmentscopeid = [string]($RoleAssignment.RoleAssignmentScopeId ?? '') roleassignmentscopename = [string]($RoleAssignment.RoleAssignmentScopeName ?? '') } } function Get-PrimitiveStringArray { param([object]$Value) if ($null -eq $Value) { return @() } return @( $Value | Where-Object { $null -ne $_ -and -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { [string]$_ } ) } function Add-PimEligibleMemberOfEdges { param( [string]$PrincipalId, [object]$RoleAssignment, [string]$RbacSystem ) if ([string]::IsNullOrEmpty($PrincipalId) -or $null -eq $RoleAssignment) { return } if ([string]($RoleAssignment.RoleAssignmentType ?? '') -ne 'Transitive') { return } $roleAssignmentSubType = [string]($RoleAssignment.RoleAssignmentSubType ?? '') $edgeProps = ConvertTo-PimMemberOfEdgeProperties -RoleAssignment $RoleAssignment -RbacSystem $RbacSystem if ($roleAssignmentSubType -eq 'Eligible member') { $transitiveByObjectId = [string]($RoleAssignment.TransitiveByObjectId ?? '') if (-not [string]::IsNullOrWhiteSpace($transitiveByObjectId)) { Add-Edge -StartId $PrincipalId -EndId $transitiveByObjectId -Kind $EdgeKind.MemberOf -Properties $edgeProps } return } if ($roleAssignmentSubType -notin @('Nested Eligible member', 'Nested Eligible group member')) { return } $nestingIds = Get-PrimitiveStringArray -Value $RoleAssignment.TransitiveByNestingObjectIds if ($nestingIds.Count -lt 2) { return } for ($ni = 1; $ni -lt $nestingIds.Count; $ni++) { Add-Edge -StartId $nestingIds[$ni] -EndId $nestingIds[$ni - 1] -Kind $EdgeKind.MemberOf -Properties $edgeProps } } # ── Helper: pick the best (lowest tier) classification item ─────────────── function Get-BestClassification { param([array]$Classifications) if ($null -eq $Classifications -or $Classifications.Count -eq 0) { return $null } return $Classifications | Where-Object { $null -ne $_.AdminTierLevel } | Sort-Object AdminTierLevel | Select-Object -First 1 } # ── Helper: build flat edge properties from a classification item ────────── # OpenGraph requires flat primitives only – no nested objects. function ConvertTo-EdgeProperties { param( [object]$Classification, [object]$RoleAssignment, [string]$RbacSystem ) $props = @{} if ($null -ne $Classification) { $props['admintierlevel'] = [string]($Classification.AdminTierLevel ?? '') $props['admintierlevelname'] = [string]($Classification.AdminTierLevelName ?? '') $props['service'] = [string]($Classification.Service ?? '') $props['taggedby'] = [string]($Classification.TaggedBy ?? '') $props['taggedbyrolesystem'] = [string]($Classification.TaggedByRoleSystem ?? '') } if ($null -ne $RoleAssignment) { $props['roleassignmenttype'] = [string]($RoleAssignment.RoleAssignmentType ?? '') $props['roleassignmentsubtype'] = [string]($RoleAssignment.RoleAssignmentSubType ?? '') $props['pimassignmenttype'] = [string]($RoleAssignment.PIMAssignmentType ?? '') $props['pimmanagedrole'] = [bool] ($RoleAssignment.PIMManagedRole -eq $true) $props['roleassignmentscopeid'] = [string]($RoleAssignment.RoleAssignmentScopeId ?? '') $props['roleassignmentscopename'] = [string]($RoleAssignment.RoleAssignmentScopeName ?? '') $props['rbacsystem'] = [string]$RbacSystem } return $props } # Keep EO_IntuneRolePermission edges focused on direct device-compromise # evidence. Source EAM MatchedActions remains unfiltered. function Get-PrunedIntunePermissionActions { param([object[]]$Actions) $directCompromiseActions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($directCompromiseAction in @( 'Microsoft.Intune/DeviceConfigurations/Assign', 'Microsoft.Intune/DeviceConfigurations/Create', 'Microsoft.Intune/DeviceConfigurations/Update', 'Microsoft.Intune/MobileApps/Assign', 'Microsoft.Intune/MobileApps/Create', 'Microsoft.Intune/MobileApps/Relate', 'Microsoft.Intune/MobileApps/Update', 'Microsoft.Intune/RemoteTasks/OnDemandProactiveRemediation', 'Microsoft.Intune/RemoteTasks/RequestRemoteAssistance' )) { [void] $directCompromiseActions.Add($directCompromiseAction) } $prunedActions = [System.Collections.Generic.List[string]]::new() $seenActions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($action in @($Actions)) { if ($null -eq $action) { continue } $actionStr = ([string]$action).Trim() if ([string]::IsNullOrEmpty($actionStr)) { continue } if (-not $directCompromiseActions.Contains($actionStr)) { continue } if ($seenActions.Add($actionStr)) { $prunedActions.Add($actionStr) } } return @($prunedActions) } # ── Main loop ───────────────────────────────────────────────────────────── foreach ($RbacSystem in $RbacSystems) { $JsonFile = Join-Path $ImportPath "$RbacSystem/$RbacSystem.json" if (-not (Test-Path $JsonFile)) { Write-Warning "No export file found for $RbacSystem at $JsonFile – skipping." continue } Write-Host "Processing $RbacSystem..." -ForegroundColor Cyan try { $Privileges = Get-Content -Path $JsonFile -Raw -ErrorAction Stop | ConvertFrom-Json -Depth 10 } catch { Write-Warning "Failed to read $($JsonFile): $_" continue } $RoleEdgeKind = $HasRoleEdgeKindMap[$RbacSystem] $RoleEligibleEdgeKind = $EligibleEdgeKindMap[$RbacSystem] $HasRoleAssignmentEdgeKind = $HasRoleAssignmentEdgeKindMap[$RbacSystem] $RoleAssignedEdgeKind = $RoleAssignedEdgeKindMap[$RbacSystem] $RoleNodeKinds = $RoleNodeKindMap[$RbacSystem] $RoleAssignmentNodeKinds = $RoleAssignmentNodeKindMap[$RbacSystem] foreach ($Privilege in $Privileges) { # ── 1. Principal node ────────────────────────────────────────────── $principalId = [string]$Privilege.ObjectId if ([string]::IsNullOrEmpty($principalId)) { continue } # Top-level Classification[] is an array of objects – flatten to primitive arrays $tierLevels = @( $Privilege.Classification | Where-Object { $null -ne $_.AdminTierLevel } | Select-Object -ExpandProperty AdminTierLevel -Unique | ForEach-Object { [string]$_ } ) $tierNames = @( $Privilege.Classification | Where-Object { $null -ne $_.AdminTierLevelName } | Select-Object -ExpandProperty AdminTierLevelName -Unique | ForEach-Object { [string]$_ } ) $services = @( $Privilege.Classification | Where-Object { $null -ne $_.Service } | Select-Object -ExpandProperty Service -Unique | ForEach-Object { [string]$_ } ) $principalProps = @{ entraopsadmintierlevel = [string]($Privilege.ObjectAdminTierLevel ?? '') entraopsadmintierlevelname = [string]($Privilege.ObjectAdminTierLevelName ?? '') restrictedmanagementbyrag = [bool] ($Privilege.RestrictedManagementByRAG -eq $true) restrictedmanagementbyaadrole = [bool] ($Privilege.RestrictedManagementByAadRole -eq $true) restrictedmanagementbyrmau = [bool] ($Privilege.RestrictedManagementByRMAU -eq $true) } # For service principals ObjectSignInName holds the appId – surface it explicitly # so BloodHound can cross-match against AZApp / AZServicePrincipal nodes from AzureHound if (($Privilege.ObjectType ?? '').ToLower() -eq 'serviceprincipal') { $spAppId = [string]($Privilege.ObjectUserPrincipalName ?? '') if (-not [string]::IsNullOrEmpty($spAppId)) { $principalProps['appid'] = $spAppId } } if ($tierLevels.Count -gt 0) { $principalProps['classification_tierlevels'] = $tierLevels } if ($tierNames.Count -gt 0) { $principalProps['classification_tiernames'] = $tierNames } if ($services.Count -gt 0) { $principalProps['classification_services'] = $services } $primaryKind = $PrincipalKindMap[($Privilege.ObjectType ?? '').ToLower()] ?? $NodeKind.Base Upsert-Node -Id $principalId -Kinds $primaryKind -Properties $principalProps # ── 1a. AssignedToAdministrativeUnit edges (AU membership of the object) ── if ($IncludeAdministrativeUnitMembershipEdges) { $assignedAUs = @($Privilege.AssignedAdministrativeUnits | Where-Object { $null -ne $_ }) foreach ($AU in $assignedAUs) { $auId = [string]($AU.id ?? '') if ([string]::IsNullOrEmpty($auId)) { continue } $auNodeId = "au-$auId" $auName = [string]($AU.displayName ?? $auId) Upsert-Node -Id $auNodeId -Kinds $NodeKind.AdministrativeUnit -Properties @{ name = $auName displayname = $auName } Add-Edge -StartId $principalId -EndId $auNodeId -Kind $EdgeKind.AssignedToAU } } # ── 1b. Work account & PAW device edges ─────────────────────────── if ($IncludeWorkAccountEdges) { # HasWorkAccount: admin account → associated standard work account $workAccountIds = @($Privilege.AssociatedWorkAccount | Where-Object { $null -ne $_ -and $_ -ne '' } | ForEach-Object { [string]$_ }) foreach ($waId in $workAccountIds) { Add-Edge -StartId $principalId -EndId $waId -Kind $EdgeKind.HasWorkAccount -Properties @{ rbacsystem = [string]$RbacSystem } } # UsesPAW: admin account -> assigned PAW/SAW device; PAWFor is the inverse. $pawDeviceIds = @($Privilege.AssociatedPawDevice | Where-Object { $null -ne $_ -and $_ -ne '' } | ForEach-Object { [string]$_ }) foreach ($pawId in $pawDeviceIds) { $pawEdgeProps = @{ rbacsystem = [string]$RbacSystem } $pawDeviceEndpoint = [ordered]@{ match_by = 'property' kind = 'AZDevice' property_matchers = @( [ordered]@{ key = 'deviceid' operator = 'equals' value = ([string]$pawId).ToLowerInvariant() } ) } $principalEndpoint = [ordered]@{ match_by = 'property' kind = @($primaryKind)[0] property_matchers = @( [ordered]@{ key = 'objectid' operator = 'equals' value = ([string]$principalId).ToUpperInvariant() } ) } Add-EdgeByEndpoint -StartEndpoint $principalEndpoint -EndEndpoint $pawDeviceEndpoint -Kind $EdgeKind.UsesPAW -Properties $pawEdgeProps Add-EdgeByEndpoint -StartEndpoint $pawDeviceEndpoint -EndEndpoint $principalEndpoint -Kind $EdgeKind.PAWFor -Properties $pawEdgeProps } } # ── 1c. Owned device edges ──────────────────────────────────────── if ($IncludeDeviceOwnershipEdges) { $ownedDeviceIds = @($Privilege.OwnedDevices | Where-Object { $null -ne $_ -and $_ -ne '' } | ForEach-Object { [string]$_ }) foreach ($devId in $ownedDeviceIds) { $ownedDeviceEdgeProps = @{ rbacsystem = [string]$RbacSystem } Ensure-AZDeviceNode -Id $devId Add-Edge -StartId $principalId -EndId $devId -Kind $EdgeKind.OwnsDevice -Properties $ownedDeviceEdgeProps Add-Edge -StartId $devId -EndId $principalId -Kind $EdgeKind.DeviceOwner -Properties $ownedDeviceEdgeProps } } # ── 1c2. Owned object edges (groups/apps/service principals - not devices) ── # OwnedObjects lists what this principal owns (e.g. a user/service principal # that owns groups or app registrations); Owners lists who owns this object # (e.g. a role-assignable group's owners). Both are emitted as EO_OwnerOf / # EO_OwnedBy so either direction of collection produces the same edge - Add-Edge # dedupes by (start, end, kind), so overlap between the two is harmless. Targets # that aren't privileged objects in this export are resolved to stub nodes by # New-EntraOpsAccessPathMapData (same pattern as other cross-object edges here). if ($IncludeObjectOwnershipEdges) { $ownedObjectIds = @($Privilege.OwnedObjects | Where-Object { $null -ne $_ -and $_ -ne '' } | ForEach-Object { [string]$_ }) foreach ($ownedId in $ownedObjectIds) { $ownershipEdgeProps = @{ rbacsystem = [string]$RbacSystem } Add-Edge -StartId $principalId -EndId $ownedId -Kind $EdgeKind.OwnerOf -Properties $ownershipEdgeProps Add-Edge -StartId $ownedId -EndId $principalId -Kind $EdgeKind.OwnedBy -Properties $ownershipEdgeProps } $ownerIds = @($Privilege.Owners | Where-Object { $null -ne $_ -and $_ -ne '' } | ForEach-Object { [string]$_ }) foreach ($ownerId in $ownerIds) { $ownershipEdgeProps = @{ rbacsystem = [string]$RbacSystem } Add-Edge -StartId $ownerId -EndId $principalId -Kind $EdgeKind.OwnerOf -Properties $ownershipEdgeProps Add-Edge -StartId $principalId -EndId $ownerId -Kind $EdgeKind.OwnedBy -Properties $ownershipEdgeProps } } # ── 1d. Sponsor edges ───────────────────────────────────────────── if ($IncludeSponsorEdges) { $sponsorIds = @($Privilege.Sponsors | Where-Object { $null -ne $_ -and $_ -ne '' } | ForEach-Object { [string]$_ }) foreach ($sponsorId in $sponsorIds) { Add-Edge -StartId $principalId -EndId $sponsorId -Kind $EdgeKind.IsSponsoredBy -Properties @{ rbacsystem = [string]$RbacSystem } } } # ── 1e. Identity parent edges ───────────────────────────────────── if ($IncludeIdentityParentEdges) { $identityParentId = [string]($Privilege.IdentityParent ?? '') if (-not [string]::IsNullOrEmpty($identityParentId)) { Add-Edge -StartId $principalId -EndId $identityParentId -Kind $EdgeKind.HasIdentityParent -Properties @{ rbacsystem = [string]$RbacSystem } } } # ── 2. The principal's assignments, create Role and RoleAssignment nodes/edges ─────────────────────────────────────── foreach ($RoleAssignment in $Privilege.RoleAssignments) { # Start Role Assignment node creation ###################################### $roleAssignmentId = [string]($RoleAssignment.RoleAssignmentId ?? '') if ([string]::IsNullOrEmpty($roleAssignmentId)) { continue } $roleAssignmentInstanceId = [string]($RoleAssignment.RoleAssignmentInstanceId ?? '') if ([string]::IsNullOrEmpty($roleAssignmentInstanceId)) { $roleAssignmentInstanceId = Get-EntraOpsRoleAssignmentInstanceId -RoleSystem $RbacSystem -RoleAssignment $RoleAssignment } #$roleAssignmentName = [string]($RoleAssignment.RoleDefinitionName ?? $roleAssignmentId) $roleAssignmentName = $roleAssignmentId $roleAssignmentProps = @{ name = $roleAssignmentName displayname = $roleAssignmentName roleassignmentid = $roleAssignmentId roleassignmentinstanceid = $roleAssignmentInstanceId rbacsystem = [string]$RbacSystem roleassignmentscopeid = [string]($RoleAssignment.RoleAssignmentScopeId ?? '') roleassignmentscopename = [string]($RoleAssignment.RoleAssignmentScopeName ?? '') roleassignmenttype = [string]($RoleAssignment.RoleAssignmentType ?? '') roleassignmentsubtype = [string]($RoleAssignment.RoleAssignmentSubType ?? '') } $matchedRoleAssignmentActions = @( $RoleAssignment.Classification | Where-Object { $null -ne $_.MatchedActions } | ForEach-Object { $_.MatchedActions } | Where-Object { -not [string]::IsNullOrEmpty($_) } | Select-Object -Unique | ForEach-Object { [string]$_ } ) if ($matchedRoleAssignmentActions.Count -gt 0) { $roleAssignmentProps['matchedactions'] = $matchedRoleAssignmentActions } # Role assignment node (stub – first occurrence wins full properties) Upsert-Node -Id $roleAssignmentInstanceId -Kinds $RoleAssignmentNodeKinds -Properties $roleAssignmentProps # Best classification for this specific assignment $bestClass = Get-BestClassification -Classifications $RoleAssignment.Classification $edgeProps = ConvertTo-EdgeProperties -Classification $bestClass -RoleAssignment $RoleAssignment -RbacSystem $RbacSystem Add-PimEligibleMemberOfEdges -PrincipalId $principalId -RoleAssignment $RoleAssignment -RbacSystem $RbacSystem # Direct or transitive assignment → role edge from principal Add-Edge -StartId $principalId -EndId $roleAssignmentInstanceId -Kind $HasRoleAssignmentEdgeKind -Properties $edgeProps # Start Role node creation ############################### $roleDefId = [string]($RoleAssignment.RoleDefinitionId ?? '') if ([string]::IsNullOrEmpty($roleDefId)) { continue } $roleNodeId = $roleDefId + "@" + $TenantId $roleDefName = [string]($RoleAssignment.RoleDefinitionName ?? $roleNodeId) # Role definition node (stub – first occurrence wins full properties) $roleNodeProps = @{ rbacsystem = [string]$RbacSystem } if ($RbacSystem -ne 'EntraID') { $roleNodeProps['name'] = $roleDefName $roleNodeProps['displayname'] = $roleDefName } Upsert-Node -Id $roleNodeId -Kinds $RoleNodeKinds -Properties $roleNodeProps # Best classification for this specific assignment $bestClass = Get-BestClassification -Classifications $RoleAssignment.Classification $edgeProps = ConvertTo-EdgeProperties -Classification $bestClass -RoleAssignment $RoleAssignment -RbacSystem $RbacSystem # Select active vs eligible edge kind based on PIM assignment type $effectiveEdgeKind = if ($RoleAssignment.PIMAssignmentType -eq 'Eligible') { $RoleEligibleEdgeKind } else { $RoleEdgeKind } # Direct or transitive assignment → role edge from principal Add-Edge -StartId $principalId -EndId $roleNodeId -Kind $effectiveEdgeKind -Properties $edgeProps # Link Role to RoleAssignment Add-Edge -StartId $roleNodeId -EndId $roleAssignmentInstanceId -Kind $RoleAssignedEdgeKind -Properties $edgeProps # ── Role assignment scope edge ──────────────────────────────── if ($IncludeAdministrativeUnitEdges) { $scopeId = [string]($RoleAssignment.RoleAssignmentScopeId ?? '') $scopeId = $scopeId.Trim() $scopeName = [string]($RoleAssignment.RoleAssignmentScopeName ?? $scopeId) $roleAssignmentSubType = [string]($RoleAssignment.RoleAssignmentSubType ?? '') $auScopeId = '' if ($scopeId -match '^/?administrativeUnits?/([^/]+)$') { $auScopeId = $Matches[1] } elseif ($roleAssignmentSubType -match 'AdministrativeUnit' -and -not [string]::IsNullOrEmpty($scopeId)) { $auScopeId = $scopeId } if (-not [string]::IsNullOrEmpty($auScopeId)) { $auNodeId = "au-$auScopeId" $auName = $scopeName Upsert-Node -Id $auNodeId -Kinds $NodeKind.AdministrativeUnit -Properties @{ name = $auName displayname = $auName rbacsystem = [string]$RbacSystem } Add-Edge -StartId $roleAssignmentInstanceId -EndId $auNodeId -Kind $EdgeKind.ScopedTo -Properties @{ rbacsystem = [string]$RbacSystem roleassignmenttype = [string]($RoleAssignment.RoleAssignmentType ?? '') roleassignmentsubtype = $roleAssignmentSubType roleassignmentscopeid = $scopeId roleassignmentscopename = $auName pimassignmenttype = [string]($RoleAssignment.PIMAssignmentType ?? '') roledefinitionid = $roleNodeId roledefinitionname = $roleDefName } } elseif ([string]::IsNullOrWhiteSpace($scopeId) -or $scopeId -eq '/') { Add-Edge -StartId $roleAssignmentInstanceId -EndId $TenantId -Kind $EdgeKind.ScopedTo -Properties @{ rbacsystem = [string]$RbacSystem roleassignmenttype = [string]($RoleAssignment.RoleAssignmentType ?? '') roleassignmentsubtype = $roleAssignmentSubType roleassignmentscopeid = $scopeId roleassignmentscopename = $scopeName pimassignmenttype = [string]($RoleAssignment.PIMAssignmentType ?? '') roledefinitionid = $roleNodeId roledefinitionname = $roleDefName } } } # ── 4. ClassifiedViaObject edges ────────────────────────────── if ($IncludeClassifiedViaObjectEdges) { foreach ($ClassItem in $RoleAssignment.Classification) { # Only emit when there are actual object IDs to point to $taggedBy = [string]($ClassItem.TaggedBy ?? '') if ($taggedBy -in @('ControlPlaneWithoutRoleActions', 'RoleDefinitionOverwrites', 'RoleActionOverwrites', '')) { continue } $taggedIds = @($ClassItem.TaggedByObjectIds | Where-Object { $null -ne $_ } | ForEach-Object { [string]$_ }) if ($taggedIds.Count -eq 0) { continue } $taggedRoleSystem = [string]($ClassItem.TaggedByRoleSystem ?? '') for ($ti = 0; $ti -lt $taggedIds.Count; $ti++) { $taggedId = $taggedIds[$ti] if ([string]::IsNullOrEmpty($taggedId)) { continue } # For OAuthApplication the tagged id is an app role GUID – prefix to avoid # collisions with role definition nodes that use the same GUID space $resolvedTaggedId = if ($taggedBy -like '*OAuthApplication*') { "app-role-$taggedId" } else { $taggedId } # Principal -[ClassifiedViaObject] -> (object) Add-Edge -StartId $principalId -EndId $resolvedTaggedId -Kind $EdgeKind.ClassifiedViaObject -Properties @{ taggedby = $taggedBy taggedbyrolesystem = $taggedRoleSystem admintierlevel = [string]($ClassItem.AdminTierLevel ?? '') admintierlevelname = [string]($ClassItem.AdminTierLevelName ?? '') service = [string]($ClassItem.Service ?? '') rbacsystem = [string]$RbacSystem } # RoleAssignment -[ClassifiedViaObject] -> (object) Add-Edge -StartId $roleAssignmentInstanceId -EndId $resolvedTaggedId -Kind $EdgeKind.ClassifiedViaObject -Properties @{ taggedby = $taggedBy taggedbyrolesystem = $taggedRoleSystem admintierlevel = [string]($ClassItem.AdminTierLevel ?? '') admintierlevelname = [string]($ClassItem.AdminTierLevelName ?? '') service = [string]($ClassItem.Service ?? '') rbacsystem = [string]$RbacSystem } } } } # ── 5. IntuneRolePermission edges: RoleAssignment → Device ──────── if ($IncludeDeviceActionEdges -and $RbacSystem -eq 'DeviceManagement') { # Pre-aggregate: collect all actions and devices across classifications # so we create one edge per roleAssignment→device with ALL actions combined $deviceActionsMap = @{} # deviceId → @{ actions = [List]; tierLevel; tierName } foreach ($ClassItem in $RoleAssignment.Classification) { $scopedDevices = @($ClassItem.ScopedObjects | Where-Object { $null -ne $_ }) $matchedActions = Get-PrunedIntunePermissionActions -Actions @($ClassItem.MatchedActions | Where-Object { $null -ne $_ }) if ($scopedDevices.Count -eq 0 -or $matchedActions.Count -eq 0) { continue } foreach ($scopedDevice in $scopedDevices) { $deviceId = [string]($scopedDevice.id ?? '') if ([string]::IsNullOrEmpty($deviceId)) { continue } if (-not $deviceActionsMap.ContainsKey($deviceId)) { $deviceActionsMap[$deviceId] = @{ actions = [System.Collections.Generic.List[string]]::new() tierLevel = [string]($ClassItem.AdminTierLevel ?? '') tierName = [string]($ClassItem.AdminTierLevelName ?? '') } } foreach ($action in $matchedActions) { $actionStr = [string]$action if (-not $deviceActionsMap[$deviceId].actions.Contains($actionStr)) { $deviceActionsMap[$deviceId].actions.Add($actionStr) } } } } # Emit one edge per device with all aggregated actions foreach ($entry in $deviceActionsMap.GetEnumerator()) { $deviceId = $entry.Key $edgeActions = @($entry.Value.actions) if ($edgeActions.Count -eq 0) { continue } Ensure-AZDeviceNode -Id $deviceId # RoleAssignment → Device Add-Edge -StartId $roleAssignmentInstanceId -EndId $deviceId -Kind $EdgeKind.IntuneRolePermission -Properties @{ actions = $edgeActions admintierlevel = $entry.Value.tierLevel admintierlevelname = $entry.Value.tierName rbacsystem = [string]$RbacSystem } # Principal → Device $queryDeviceId = ([string]$deviceId).ToUpperInvariant() $queryPrincipalId = ([string]$principalId).ToUpperInvariant() # $principalDeviceComposition = @( # "MATCH p1 = (principal)-[:AZMemberOf*1..]->(group)-[:EO_HasIntuneRoleAssignment]->(ra)-[:EO_IntuneRolePermission]->(device)" # "WHERE device.objectid = '$($queryDeviceId)'" # " AND principal.objectid = '$($queryDeviceId)' # "OPTIONAL MATCH p2 = (ra)<-[:EO_IntuneRoleAssigned]-(role)" # "RETURN p1,p2" # ) -join [Environment]::NewLine $principalDeviceComposition = @( "MATCH p1=(principal)-[:EO_HasIntuneRoleAssignment]->(ra:EO_IntuneRoleAssignment)-[:EO_IntuneRolePermission]->(device)" "WHERE device.objectid = '$($queryDeviceId)' AND principal.objectid = '$($queryPrincipalId)'" "OPTIONAL MATCH p2=(ra)<-[:EO_IntuneRoleAssigned]-(:EO_IntuneRole)" "OPTIONAL MATCH p3 = (group:AZGroup)-[r:EO_HasIntuneRoleAssignment]->(ra) WHERE NOT r.roleassignmentsubtype IN ['Nested Eligible group member', 'Eligible member']" "OPTIONAL MATCH p4 = (principal)-[:AZMemberOf*1..]->(group)" "RETURN p1,p2,p3,p4" ) -join [Environment]::NewLine Add-Edge -StartId $principalId -EndId $deviceId -Kind $EdgeKind.IntuneRolePermission -Properties @{ actions = $edgeActions admintierlevel = $entry.Value.tierLevel admintierlevelname = $entry.Value.tierName rbacsystem = [string]$RbacSystem Composition = $principalDeviceComposition } } } } } Write-Host " ✓ $($RbacSystem): $($Privileges.Count) principals loaded" -ForegroundColor Green } # ── Add Cypher query properties to nodes ──────────────────────────────────── # Sets of kind names for kind-based dispatch $RoleNodeKindSet = [System.Collections.Generic.HashSet[string]]::new( [string[]]@($RoleNodeKindMap.Values | ForEach-Object { $_ }) ) $RoleAssignmentKindSet = [System.Collections.Generic.HashSet[string]]::new( [string[]]@($RoleAssignmentNodeKindMap.Values | ForEach-Object { $_ }) ) # All-system edge kind strings for node query shortcuts. $AllActiveEdgeKindsQ = $AllActiveRoleEdgeKinds $AllEligibleEdgeKindsQ = $AllEligibleRoleEdgeKinds $AllRaEdgesQ = $AllRoleAssignmentEdges $AllRaAssignedQ = $AllRoleAssignedEdges $MemberOfEdgeQ = $EdgeKind.MemberOf $UsesPAWEdgeQ = $EdgeKind.UsesPAW $PAWForEdgeQ = $EdgeKind.PAWFor $OwnsDeviceEdgeQ = $EdgeKind.OwnsDevice $DeviceOwnerEdgeQ = $EdgeKind.DeviceOwner $IntunePermissionEdgeQ = $EdgeKind.IntuneRolePermission $AssignedToAUEdgeQ = $EdgeKind.AssignedToAU $ScopedToEdgeQ = $EdgeKind.ScopedTo foreach ($nodeId in @($NodesIndex.Keys)) { $node = $NodesIndex[$nodeId] $props = $node.properties $nodeKinds = $node.kinds $rbacSystem = [string]($props['rbacsystem'] ?? '') if (-not $rbacSystem) { foreach ($candidateRbacSystem in $RoleAssignmentNodeKindMap.Keys) { $candidateKinds = @($RoleAssignmentNodeKindMap[$candidateRbacSystem]) if ($nodeKinds | Where-Object { $candidateKinds -contains $_ }) { $rbacSystem = $candidateRbacSystem break } } } $queryNodeId = ([string]$nodeId).ToUpperInvariant() # Universal: every node gets inbound + outbound traversal queries $props['InboundRelationships'] = "MATCH p=(n)<-[r]-(m) WHERE n.objectid = '$queryNodeId' RETURN p" $props['OutboundRelationships'] = "MATCH p=(n)-[r]->(m) WHERE n.objectid = '$queryNodeId' RETURN p" # Resolve per-system or all-system edge kind strings if ($rbacSystem -and $HasRoleEdgeKindMap.ContainsKey($rbacSystem)) { $activeEdgeQ = $HasRoleEdgeKindMap[$rbacSystem] $eligibleEdgeQ = $EligibleEdgeKindMap[$rbacSystem] $raEdgeQ = $HasRoleAssignmentEdgeKindMap[$rbacSystem] $raAssignedQ = $RoleAssignedEdgeKindMap[$rbacSystem] $roleNodeQ = @($RoleNodeKindMap[$rbacSystem])[0] $raNodeQ = @($RoleAssignmentNodeKindMap[$rbacSystem])[0] } else { $activeEdgeQ = $AllActiveEdgeKindsQ $eligibleEdgeQ = $AllEligibleEdgeKindsQ $raEdgeQ = $AllRaEdgesQ $raAssignedQ = $AllRaAssignedQ $roleNodeQ = ($RoleNodeKindMap.Values | ForEach-Object { $_ }) -join '|' $raNodeQ = ($RoleAssignmentNodeKindMap.Values | ForEach-Object { $_ }) -join '|' } # Kind-specific queries # Role definition nodes (AZRole, EO_DefenderRole, EO_IntuneRole, etc.) if ($nodeKinds | Where-Object { $RoleNodeKindSet.Contains($_) }) { $props['ActiveAssignments'] = "MATCH p=()-[:$activeEdgeQ|$MemberOfEdgeQ*1..]->(role) WHERE role.objectid = '$queryNodeId' RETURN p" $props['EligibleAssignments'] = "MATCH p=()-[:$eligibleEdgeQ|$MemberOfEdgeQ*1..]->(role) WHERE role.objectid = '$queryNodeId' RETURN p" } # Role assignment nodes (EO_EntraRoleAssignment, EO_DefenderRoleAssignment, etc.) elseif ($nodeKinds | Where-Object { $RoleAssignmentKindSet.Contains($_) }) { $props['RoleAndAssignments'] = "MATCH p1 = (role:$roleNodeQ)-[:$raAssignedQ]->(ra:$raNodeQ) WHERE ra.objectid = '$queryNodeId' OPTIONAL MATCH p2 = (group:AZGroup)-[r:$raEdgeQ]->(ra) WHERE NOT r.roleassignmentsubtype IN ['Nested Eligible group member', 'Eligible member'] OPTIONAL MATCH p3 = (principal)-[:$MemberOfEdgeQ*1..]->(group) RETURN p1,p2,p3" $props['AssignmentScope'] = "MATCH p=(ra)-[:$ScopedToEdgeQ]->(scope) WHERE ra.objectid = '$queryNodeId' RETURN p" } # User nodes elseif ($nodeKinds -contains 'AZUser') { $props['Roles'] = "MATCH p=(principal)-[:$MemberOfEdgeQ*1..]->(:AZGroup)-[:$activeEdgeQ|$eligibleEdgeQ]->(role) WHERE principal.objectid = '$queryNodeId' RETURN p" $props['PAWDevices'] = "MATCH p=(principal)-[:$UsesPAWEdgeQ|$OwnsDeviceEdgeQ]->(device) WHERE principal.objectid = '$queryNodeId' RETURN p" } # Group nodes elseif ($nodeKinds -contains 'AZGroup') { $props['Roles'] = "MATCH p=(principal)-[:$activeEdgeQ|$eligibleEdgeQ]->(role) WHERE principal.objectid = '$queryNodeId' RETURN p" } # Service principal nodes elseif ($nodeKinds -contains 'AZServicePrincipal') { $props['Roles'] = "MATCH p=(principal)-[:$MemberOfEdgeQ*1..]->(:AZGroup)-[:$activeEdgeQ|$eligibleEdgeQ]->(role) WHERE principal.objectid = '$queryNodeId' RETURN p" } # Device nodes elseif ($nodeKinds -contains 'AZDevice') { $props['AssociatedPrincipals'] = "MATCH p=(device)-[:$PAWForEdgeQ|$DeviceOwnerEdgeQ]->(principal) WHERE device.objectid = '$queryNodeId' RETURN p" $props['InboundIntunePermissions'] = "MATCH p=(principal)-[:$IntunePermissionEdgeQ]->(device) WHERE device.objectid = '$queryNodeId' RETURN p" } # Administrative unit nodes elseif ($nodeKinds -contains 'EO_AdministrativeUnit') { $props['Members'] = "MATCH p=(n)-[:$AssignedToAUEdgeQ]->(au) WHERE au.objectid = '$queryNodeId' RETURN p" $props['ScopedByRoleAssignments'] = "MATCH p=(ra)-[:$ScopedToEdgeQ]->(au) WHERE au.objectid = '$queryNodeId' RETURN p" } # EO_Base stub nodes: no type-specific queries beyond the universal pair } # ── Assemble OpenGraph payload ───────────────────────────────────────────── $Payload = [ordered]@{ graph = [ordered]@{ nodes = @($NodesIndex.Values) edges = @($EdgesList) } } # ── Write output ────────────────────────────────────────────────────────── Write-Host "" Write-Host "Writing OpenGraph payload..." -ForegroundColor Cyan $Json = $Payload | ConvertTo-Json -Depth 10 -Compress:$false $Json | Out-File -FilePath $OutputPath -Encoding utf8 -Force $NodeCount = $NodesIndex.Count $EdgeCount = $EdgesList.Count $FileSizeKB = [Math]::Round((Get-Item $OutputPath).Length / 1KB, 1) Write-Host "" Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Green Write-Host " ✓ OpenGraph export complete" -ForegroundColor Green Write-Host "═══════════════════════════════════════════════════════════════════════════════" -ForegroundColor Green Write-Host " Nodes : $NodeCount" -ForegroundColor Gray Write-Host " Edges : $EdgeCount" -ForegroundColor Gray Write-Host " File : $OutputPath ($FileSizeKB KB)" -ForegroundColor Gray Write-Host "" return [PSCustomObject]@{ OutputPath = $OutputPath NodeCount = $NodeCount EdgeCount = $EdgeCount FileSizeKB = $FileSizeKB } } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCY+8Tlu4Ux4szL # ZUBoFWdQXBBUgFcv+YQIdAwtQKteuKCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # 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 # ARUwLwYJKoZIhvcNAQkEMSIEIGrPPHI10KNGEkujUnPic2xrdQSODeWnlYBtlCmX # 0lgrMA0GCSqGSIb3DQEBAQUABIIBgEMunuz6xgDRcJYcW+U0l17t/DBWOUrzYQsN # l2w29tReZp/zxU+s0biToGz3Mf+5gcLHtbe2qHkB2+mXwg86610B/9camgtbC6P0 # +92gpCEd4i894p3Q6RPf43wpND1Q3IWBrdlRrCuH4PSJBtKLo5gM2tIlOOWDAxLT # CwMltj9ImbAtpPD2mPycxU8XPxwIIx6Ya8bPn9PSBe5fRNjOzLTiQz0cOmR+jjcJ # p5l8tnNgEoKoh+/5olvEYfUDY9vXZ56nCiPT2zO0DTmzu5vgiGDYNOsQt6tj89G9 # Wi0c/NDfIBRhe1Ld9QgBvU8NJMAMGD6WyrWApAEyVtflK7t+ygiaPvTYA0jq3WBk # yx8N4vOoNWDrmg4OSyAEU2KeF9RZFi6qwdd54/lDroZO8k9vLlq6i0nNOlQJIeWU # hmPgpV4nFi8zxAaaOYjAvPH60Gqx1b5LMB83WEgWVCCiPF8QwDe5m1QNkQKDaoz2 # zz5vZitKJfzQDdqnVwKwzGiERtfaMqGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQxMDZaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDC6aoHvCgZkq2sUG5A2hxUDt/fDmLdXCuvZ # 7gz9U9XM+Vw/3UGiVvuaFYE1uFKyJ/8wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAbJv+RSp4mz6PtewA4t7Ps/HvMk5XHYEAlD6lVkXrLcGO1S8u # XdqM95wuN47adL0TiOCZdIXTdXhPhJGmAHo09fsxgr4xO5/+aHK6BYqqopC4yHAz # VuwbQ2dCYukuPzaJ9bWS4gRl1pIfP/lEpHWNZsQdjsxzmP3PymU4nwK2lEE5AYD4 # B+rzJ2nVLRNZrg0WR998ED5FVi5mSrGw3NwG9reOCI8bHH+8EJrmRQqobGr7Ot7t # SKVzQ/qeW4bbJW8fzYfImfJbr7vfqjAYoyIZkQRFxX4hTsJt+/n8drfsOiQaQu0C # U33iK8DIbBIX9stp1bdj739Ww/ACYr3FEGIgp7AGLdAj6zv0yWhH3/W0U99O4zVR # 3UVp0Jbm3pndcABWJnY4ywGv6OReEeVlm1gynJaldmtky7WBYPlsfBhHkiRt1oUh # FILP6Y0Y/s106TRod3Wg5tYC5w9cEfYx93fqOr88cQ0ZKgfJwzH7MQJm25v+V0Zr # f3kxbEm+ti5gIRUQZaKKWMVNK1OMszE8E98tMOReJ5TwKybG4WfWJzPGWZ9PBiJm # KOAwMXKSpBRE9ocPPhxaYtkPau2p+0taWHSrocUtqVerl/MwKvBb5OMz+boWKNom # jd78lqqL+3hIBk1Vwe+WptSOe8I7IH4Sryq5bmltVTlQZ0yYqHDPti2gda0= # SIG # End signature block |