modules/Azure/Discovery/Private/InvokeCIEMIdentityHierarchyBuild.ps1
|
function InvokeCIEMIdentityHierarchyBuild { <# .SYNOPSIS Builds a flat ordered node list representing the identity-centric hierarchy tree. Returns [PSCustomObject[]] with NodeId, NodeType, Depth, ParentNodeId, Relationship, Label properties. .NOTES Private helper for Get-CIEMAzureIdentityHierarchy. The identity hierarchy is a fixed 5-level tree: Tenant -> IdentityType -> Identity -> Role -> Scope. #> param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Assignments, [Parameter(Mandatory)] [ValidateSet('Effective', 'Direct')] [string]$Mode, [Parameter()] [hashtable]$ScopeLabelLookup = @{}, [Parameter()] [hashtable]$GroupNameLookup = @{} ) $ErrorActionPreference = 'Stop' $nodes = [System.Collections.Generic.List[PSObject]]::new() # Root tenant node $tenantNodeId = 'tenant:identity' $nodes.Add([PSCustomObject]@{ NodeId = $tenantNodeId NodeType = 'Tenant' Depth = 0 ParentNodeId = $null Relationship = $null Label = 'Tenant' }) # Canonical principal type ordering and mapping $principalTypeOrder = @('User', 'ServicePrincipal', 'Group', 'ManagedIdentity') $principalTypeMap = @{ 'User' = 'User' 'ServicePrincipal' = 'ServicePrincipal' 'Group' = 'Group' 'ManagedIdentity' = 'ManagedIdentity' 'ForeignGroup' = 'Group' 'Unknown' = 'Unknown' } # Normalize and group assignments by principal type $grouped = @{} foreach ($a in $Assignments) { $rawType = $a.PrincipalType $canonicalType = $principalTypeMap[$rawType] if (-not $canonicalType) { $canonicalType = $rawType } if (-not $grouped.ContainsKey($canonicalType)) { $grouped[$canonicalType] = [System.Collections.Generic.List[object]]::new() } $grouped[$canonicalType].Add($a) } # Level 1: IdentityType nodes (in canonical order, then any extras) $orderedTypes = @($principalTypeOrder | Where-Object { $grouped.ContainsKey($_) }) $extraTypes = @($grouped.Keys | Where-Object { $_ -notin $principalTypeOrder } | Sort-Object) $allTypes = $orderedTypes + $extraTypes foreach ($typeName in $allTypes) { $typeAssignments = $grouped[$typeName] $typeNodeId = "identitytype:$typeName" $typeCount = @($typeAssignments | Select-Object -Property PrincipalId -Unique).Count $nodes.Add([PSCustomObject]@{ NodeId = $typeNodeId NodeType = 'IdentityType' Depth = 1 ParentNodeId = $tenantNodeId Relationship = 'HAS_ACCESS' Label = "$typeName ($typeCount)" }) # Level 2: Identity nodes — group by PrincipalId within this type $byPrincipal = $typeAssignments | Group-Object -Property PrincipalId foreach ($principalGroup in $byPrincipal) { $principalId = $principalGroup.Name $displayName = ($principalGroup.Group[0]).PrincipalDisplayName if (-not $displayName) { $displayName = $principalId } $identityNodeId = "identity:$principalId" $nodes.Add([PSCustomObject]@{ NodeId = $identityNodeId NodeType = 'Identity' Depth = 2 ParentNodeId = $typeNodeId Relationship = 'HAS_ACCESS' Label = $displayName }) # Level 3: Role nodes — group by RoleName within this identity $byRole = $principalGroup.Group | Group-Object -Property RoleName foreach ($roleGroup in $byRole) { $roleName = $roleGroup.Name if (-not $roleName) { $roleName = 'Unknown Role' } $roleNodeId = "role:$principalId|$roleName" $nodes.Add([PSCustomObject]@{ NodeId = $roleNodeId NodeType = 'Role' Depth = 3 ParentNodeId = $identityNodeId Relationship = 'HAS_ACCESS' Label = $roleName }) # Level 4: Scope nodes — one per unique scope $byScope = $roleGroup.Group | Group-Object -Property Scope foreach ($scopeGroup in $byScope) { $scopeValue = $scopeGroup.Name $assignment = $scopeGroup.Group[0] # Resolve friendly scope label $scopeLabel = ResolveCIEMScopeLabel -Scope $scopeValue -SubscriptionNameLookup $ScopeLabelLookup # In Effective mode, annotate inherited assignments if ($Mode -eq 'Effective' -and $assignment.OriginalPrincipalId -and $assignment.PrincipalId -ne $assignment.OriginalPrincipalId) { $groupName = $GroupNameLookup[$assignment.OriginalPrincipalId] if ($groupName) { $scopeLabel = "$scopeLabel (via $groupName)" } else { $shortId = if ($assignment.OriginalPrincipalId.Length -gt 8) { $assignment.OriginalPrincipalId.Substring(0, 8) } else { $assignment.OriginalPrincipalId } $scopeLabel = "$scopeLabel (via $shortId)" } } $scopeNodeId = "scope:$principalId|$roleName|$scopeValue" $nodes.Add([PSCustomObject]@{ NodeId = $scopeNodeId NodeType = 'Scope' Depth = 4 ParentNodeId = $roleNodeId Relationship = 'HAS_ACCESS' Label = $scopeLabel }) } } } } $nodes } |