Управление доступом в Azure может быстро превратиться в хаос: десятки групп, сотни пользователей, тысячи прав. Простая, интерактивная визуализация в виде дерева мгновенно показывает структуру, пробелы и потенциальные риски.
Если вы можете извлечь данные — вы можете их визуализировать.
🧹 Представление членства в группах
Первая визуализация показывает, какие пользователи входят в какие группы:
🛡️ Представление прав доступа
Вторая визуализация показывает, кто какие права имеет в разных ресурсах:
🛠️ Как собираются данные
Данные создаются с помощью двух PowerShell-скриптов:
- один извлекает членство в группах
- другой строит связи назначений ролей между пользователями, группами и скоупами.
Скрипт для получения информации о членстве в группах
#Requires -Modules Az, ipmgmt
# prepare data for detailed permissions visualization
$subscriptionDetails = @{}
$currentContext = Get-AzContext
$users = Get-AzADUser
$groups = Get-AzADGroup
$servicePrincipals = Get-AzADServicePrincipal
$applications = Get-AzADApplication
Get-AzSubscription | % {
Select-AzSubscription -SubscriptionObject $_ | out-null
$subscriptionDetails[$_.Name] = @{
users = $users
groups = $groups
assignments = Get-AzRoleAssignment
servicePrincipals = $servicePrincipals
applications = $applications
}
}
Set-AzContext -Context $currentContext | out-null
# process data for group membership visualization
$userIndex = @{}
$users | % {
$userIndex[$_.UserPrincipalName] = $_
}
$rootName = 'groupsRoot'
$rootChildren = $groups | % {
$g = $_
$members = Get-AzADGroupMember -GroupObjectId $g.Id -WarningAction Ignore
$children = @($members | % { [pscustomobject]@{name = $userIndex[$_.UserPrincipalName].DisplayName; value = 1 } })
[pscustomobject]@{
name = $g.DisplayName
children = $children
value = $children.Count
}
}
[pscustomobject]@{
name = $rootName
children = $rootChildren
value = 1
} | ConvertTo-Json -Depth 10 | Out-File 'src/data/userGroupData.json' -Force
Скрипт для визуализации назначения ролей
#Requires -Modules Az, ipmgmt
# data processing functions
function generateName($o) {
if ($o -is [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphUser]) {
return "user`:$($o.UserPrincipalName)"
}
if ($o -is [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphGroup]) {
return "group`:$($o.DisplayName)"
}
if ($o -is [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphServicePrincipal]) {
return "srvPrincipal`:$($o.DisplayName)"
}
if ($o -is [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphApplication]) {
return "application`:$($o.DisplayName)"
}
}
function Get-PermissionDetails {
param(
$InputObject, # $groups + $users + $servicePrincipals + $applications
$SubscriptionName
)
$users = $InputObject[$SubscriptionName].users
$groups = $InputObject[$SubscriptionName].groups
$assignments = $InputObject[$SubscriptionName].assignments
$servicePrincipals = $InputObject[$SubscriptionName].servicePrincipals
$applications = $InputObject[$SubscriptionName].applications
$rootChildren = @(($users + $groups + $servicePrincipals + $applications) | % {
$id = $_.id
$name = generateName $_
$permissions = $assignments | ? ObjectId -eq $id | Group-Object -Property Scope
$children = $permissions | % {
$rdNames = $_.Group.RoleDefinitionName | % { @([pscustomobject]@{ name = $_; value = 1; children = @() }) }
[pscustomobject]@{
name = "scope`:$(($_.Name -split "/")[-1])"
children = @($rdNames)
value = @($rdNames).Count
} }
$ret = [pscustomobject]@{
name = $name
value = @($children).Count
children = @($children) #$children.Count -gt 1 ? $children : "[ $children ]"
}
$ret
})
[pscustomobject]@{
name = $SubscriptionName
children = @($rootChildren) | ? { $_.value -gt 0 }
value = @($rootChildren).Count
}
}
$subscriptionDetails = @{}
$currentContext = Get-AzContext
$users = Get-AzADUser
$groups = Get-AzADGroup
$servicePrincipals = Get-AzADServicePrincipal
$applications = Get-AzADApplication
$assignments = Get-AzRoleAssignment
Get-AzSubscription | % {
Select-AzSubscription -SubscriptionObject $_ | out-null
$subscriptionDetails[$_.Name] = @{
users = $users
groups = $groups
assignments = Get-AzRoleAssignment
servicePrincipals = $servicePrincipals
applications = $applications
}
}
Set-AzContext -Context $currentContext | out-null
# process data
Get-PermissionDetails -InputObject $subscriptionDetails -SubscriptionName 'some-subscription-name' | ConvertTo-Json -Depth 100 | Out-File "src/data/prodScopesAndPermissions.json"
JavaScript-код для визуализации дерева, на основе d3js
import * as d3 from "npm:d3";
export function displayGroupTree(data, width = 2500, height = 900) {
console.log(data); // Display data for debugging
const root = d3.hierarchy(data);
const dx = 10;
const dy = width / (root.height + 1);
const tree = d3.tree().nodeSize([dx, dy]);
root.sort((a, b) => d3.ascending(a.data.name, b.data.name));
tree(root);
let x0 = Infinity;
let x1 = -x0;
root.each(d => {
if (d.x > x1) x1 = d.x;
if (d.x < x0) x0 = d.x;
});
const margin = 20; // save some space for the last line
height = x1 - x0 + dx * 2 + margin;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-dy / 3, x0 - dx - margin / 2, width, height])
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");
// Append a group element to manipulate (for zooming purposes)
const g = svg.append("g")
.attr("transform", `translate(${dy / 3},${dx})`);
const link = g.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5)
.selectAll("path")
.data(root.links())
.join("path")
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
const node = g.append("g")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", d => `translate(${d.y},${d.x})`);
node.append("circle")
.attr("fill", d => d.children ? "#555" : "#999")
.attr("r", 2.5);
node.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.children ? -6 : 6)
.attr("text-anchor", d => d.children ? "end" : "start")
.text(d => d.data.name)
.attr("stroke", "white")
.attr("paint-order", "stroke");
// Add zooming functionality
svg.call(d3.zoom().on("zoom", (event) => {
g.attr("transform", event.transform);
}));
return svg.node();
}
🚀 Результаты
Имея всего два JSON-файла и немного магии Observable, вы можете мгновенно визуализировать сложные модели доступа в Azure — и наконец понять, как связаны ваши пользователи, группы, сервисные принципы и назначения ролей.
📋 Что дальше?
Вы можете расширить этот подход для:
- Выявления аккаунтов с избыточными правами
- Поиска «осиротевших» групп и сервисных принципов
- Отслеживания изменений прав во времени