Analyzing User Privileges in Azure based on activity

Role-Based Access Control (RBAC) in Azure has developed significantly over the years, becoming more granulated and thus providing a robust framework for managing access to resources. Ensuring that users are assigned the least privileged role is a crucial aspect of improving security and should be a continuous process.

I took some inspiration from this blog from Timur. The solution involves identifying users with high privileged roles, also known as directory roles, in Azure. We will analyze the activity patterns of these users to determine if they could be assigned different roles. The result is a list of users, their directory roles, and their activity patterns. Users with no activity in the last 90 days will be flagged for evaluation and potential removal, with exceptions for break glass users and a limited number of Global Administrators.

Detailed Steps

  1. Get all the directory roles
  2. Get all users that have assigned the directory role on them directly
  3. Get all users that are members of an Entra ID group that have the directory role
  4. Query Log analytics for the activity logs for that user with that role
  5. Create an overview

Below is the code categorized into the different steps. You can also find the entire script here.

Get all the directory roles

Using the Graph API to get all the directory roles

function Get-DirectoryRoles {

$directoryRoles = Invoke-AzRestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryRoles"

if ($directoryRoles.StatusCode -eq 200) {
$directoryRoles = $directoryRoles.Content | ConvertFrom-Json | Select-Object -ExpandProperty value
return $directoryRoles
}
elseif ($directoryRoles.Content) {
$Content = $directoryRoles.Content | ConvertFrom-Json
Write-Error $Content.error.message
}
}

Get all users that have assigned the directory role on them directly

First I get all the resources that have the directory role assigned to them. This can be service principals, groups and users.

function Get-DirectoryRoleMembers {

param(
[Parameter(Mandatory = $true)]
[string]$DirectoryRoleID
)
$directoryRoleMembers = Invoke-AzRestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryRoles/$DirectoryRoleID/members"

if ($directoryRoleMembers.StatusCode -eq 200) {
$directoryRoleMembers = $directoryRoleMembers.Content | ConvertFrom-Json | Select-Object -ExpandProperty value
return $directoryRoleMembers
}
elseif ($directoryRoleMembers.Content) {
$Content = $directoryRoleMembers.Content | ConvertFrom-Json
Write-Error $Content.error.message
}

}

Then we can filter out the users from the list

function Get-AllUsersWithDirectDirectoryRoles {

$directoryRoles = Get-DirectoryRoles
$usersWithPrivilegedRoles = @()
$directoryRoles | ForEach-Object {
$directoryRole = $_
$users = Get-DirectoryRoleMembers -DirectoryRoleID $_.id
# Check if there are any users that have the role and that the object is a user and not a group
if ($null -ne $users) {
$users | ForEach-Object {
if ($_.'@odata.type' -eq '#microsoft.graph.user') {
$usersWithPrivilegedRoles += [PSCustomObject]@{
users = $users
directoryRole = $directoryRole
}
}
}
}
}
return $usersWithPrivilegedRoles
}

Get all users that are members of an Entra ID group that have the directory role

function Get-AllEntraIdGroupMembersWithDirectoryRoles {

$directoryRoles = Get-DirectoryRoles
$usersWithPrivilegedRoles = @()

$directoryRoles | ForEach-Object {
$directoryRole = $_
$directoryRoleMembers = Get-DirectoryRoleMembers -DirectoryRoleID $_.id
if ($null -ne $directoryRoleMembers -and $directoryRoleMembers.'@odata.type' -eq '#microsoft.graph.group') {
$directoryRoleMembers | ForEach-Object {
$groupMembers = Invoke-AzRestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/groups/$($_.id)/members"
if ($groupMembers.StatusCode -eq 200) {
$groupMembers = $groupMembers.Content | ConvertFrom-Json | Select-Object -ExpandProperty value
if ($null -ne $groupMembers) {
$groupMembers | ForEach-Object {
if ($_.'@odata.type' -eq '#microsoft.graph.user') {
$usersWithPrivilegedRoles += [PSCustomObject]@{
users = $groupMembers
directoryRole = $directoryRole
}
}
}
}
elseif ($groupMembers.Content) {
$Content = $groupMembers.Content | ConvertFrom-Json
Write-Error $Content.error.message
}
}
}
}
}
return $usersWithPrivilegedRoles
}

Query Log analytics for the activity logs for that user with that role

function Get-QueryResultFromLogAnalyticsWorkspace {

param(
[Parameter(Mandatory = $true)]
[string]$WorkspaceId,
[Parameter(Mandatory = $true)]
[string]$LogAnalyticsWorkspaceSubscriptionID,
[Parameter(Mandatory = $true)]
[string]$Query
)
$null = Select-AzSubscription -SubscriptionId $LogAnalyticsWorkspaceSubscriptionID -WarningAction SilentlyContinue
try {
$queryResults = Invoke-AzOperationalInsightsQuery -Query $Query -WorkspaceId $WorkspaceId
$results = $queryResults.Results
}
catch {
Write-Error "Error occured while getting the query results from Log Analytics Workspace. Error message: $($_)"
}
return $results
}

function Get-Query {
param(
[Parameter(Mandatory = $true)]
[string]$User,
[Parameter(Mandatory = $true)]
[string]$DirectoryRoleID
)
$query = @"
datatable(UserPrincipalName:string, Roles:dynamic) [
'$($User)', dynamic(['$($DirectoryRoleID)'])
]
| join kind=inner (AuditLogs
| where TimeGenerated > ago(90d)
| extend ActorName = iif(
isnotempty(tostring(InitiatedBy["user"])),
tostring(InitiatedBy["user"]["userPrincipalName"]),
tostring(InitiatedBy["app"]["displayName"])
)
| extend ActorID = iif(
isnotempty(tostring(InitiatedBy["user"])),
tostring(InitiatedBy["user"]["id"]),
tostring(InitiatedBy["app"]["id"])
)
| where isnotempty(ActorName)
) on `$left.UserPrincipalName == `$right.ActorName
| summarize Operations = make_set(OperationName) by ActorName, ActorID, tostring(Roles)
| extend OperationsCount = array_length(Operations)
| project ActorName, Operations, OperationsCount, Roles, ActorID
| sort by OperationsCount desc
"@
return $query
}

Create an overview

function Invoke-EntraIdPrivilegedRoleReport {

param(
[Parameter(Mandatory = $true)]
[string]$WorkspaceId,
[Parameter(Mandatory = $true)]
[string]$LogAnalyticsWorkspaceSubscriptionID,
[Parameter(Mandatory = $true)]
[bool]$IncludeGroups,
[Parameter(Mandatory = $true)]
[bool]$IncludePIM
)

$info = @()
$usersWithDirectDirectoryRoles = Get-AllUsersWithDirectDirectoryRoles

if ($IncludeGroups) {
$usersWithEntraIdGroupMembersWithDirectoryRoles = Get-AllEntraIdGroupMembersWithDirectoryRoles
$usersWithPrivilegedRoles = $usersWithDirectDirectoryRoles + $usersWithEntraIdGroupMembersWithDirectoryRoles
}
else {
$usersWithPrivilegedRoles = $usersWithDirectDirectoryRoles
}

$usersWithPrivilegedRoles | ForEach-Object {
$directoryRole = $_.directoryRole
$_.users | ForEach-Object {
$query = Get-Query -User $_.userPrincipalName -DirectoryRoleID $directoryRole.id
$queryResults = Get-QueryResultFromLogAnalyticsWorkspace -WorkspaceId $WorkspaceId -LogAnalyticsWorkspaceSubscriptionID $LogAnalyticsWorkspaceSubscriptionID -Query $query

# If the user has not done any actions that last 90 days
if ($null -eq $queryResults) {
$userActivity = $null
}
else {
$userActivity = $queryResults
}
$info += [PSCustomObject]@{
User = $_.userPrincipalName
DirectoryRole = $directoryRole
UserActivity = $userActivity
}
}
}
$infoHashTable = $info | Group-Object -Property User -AsHashTable
    $infoHashTable
}

Further development

This is only the beginning, I’m planning to keep developing this tool. Mostly to learn and dig deeper into a problem. Here is the plan for further development.

Summary

Maintaining control over users with directory roles is a challenge in itself. However, a more informed approach involves examining their usage based on activity logs. This provides a comprehensive view of how these roles are being utilized by the respective users. Executing this script offers an overview of the users and their activities over the past 90 days, thereby enabling effective role management.

Legg igjen en kommentar

Blogg på WordPress.com.