Why Reviewing Azure PIM Role Assignments Matters
Managing privileged access in Entra ID is critical for maintaining security and compliance in cloud environments. Privileged Identity Management (PIM) allows organizations to grant just-in-time access to sensitive roles, reducing the risk of over-privileged accounts.
However, over time, role assignments accumulate, leading to unnecessary or outdated privileges. Regularly auditing PIM role activation’s ensures that only necessary users retain access, minimizing security risks.
In this article, we’ll go over a PowerShell script that simplifies the process of reviewing PIM role assignments and identifying unused access.
What the Script Does
- Retrieves a list of PIM role assignments
- Retrieves Audit Logs related to PIM activations
- Matches users to their last PIM activation
- Exports findings to a CSV file for easy review
- Helps organizations remove unnecessary access
By running this script, administrators gain a clear picture of who has access, when roles were last activated, and whether access is still needed.
$useWorkspace = $false # When set to false the script queries the relevant Activity Logs directly, when true it queries a Log Analytics Workspace
$workspaceId = '########-#######-########' # Enter the Log Analytics Workspace to use
$exportFilePath = "C:\temp\PIMReport.csv"
$historyToQueryInDays = 90 # The number of days to query, this will be limited by Azure Log retention
# Get users that are PIM eligible
Connect-MgGraph -Scopes "AuditLog.Read.All"
Write-Host "Retrieving Activity Logs..."
if($useWorkspace) {
# Retrieve Audit Logs from workspace
$startTime = (Get-Date).AddDays(-$historyToQueryInDays)
$endTime = Get-Date
$startTime = $startTime.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$endTime = $endTime.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
# Query to return only the data required for only user
$query = @"
AuditLogs
| where OperationName == 'Add member to role completed (PIM activation)'
and Result == 'success'
and Category == 'RoleManagement'
and TimeGenerated between (datetime($startTime) .. datetime($endTime))
| project TimeGenerated, Identity, UPN = TargetResources[2].userPrincipalName, id = TargetResources[2].id, TargetRole = TargetResources[0].displayName, TargetRoleId = TargetResources[0].id, Result
"@
$activityLogs = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $query -ErrorAction Stop | Select-Object -ExpandProperty Results
} else {
if($historyToQueryInDays -gt 30) { $historyToQueryInDays = 30 }
$startTime = (Get-Date).AddDays(-$historyToQueryInDays).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")
# Retrieve AuditLogs directly
$logs = Get-MgAuditLogDirectoryAudit -Filter "activityDisplayName eq 'Add member to role completed (PIM activation)' and Result eq 'success' and activityDateTime gt $startTime" -Property ActivityDateTime, TargetResources -All
$activityLogs = $logs | ForEach-Object {
$userData = $_.TargetResources | Where-Object {$_.Type -eq "User"}
$roleData = $_.TargetResources | Where-Object {$_.Type -eq "Role"}
[PSCustomObject]@{
TimeGenerated = $_.ActivityDateTime
Identity = $userData.DisplayName
UPN = $userData.UserPrincipalName
id = $userData.Id
TargetRole = $roleData.DisplayName
TargetRoleId = $roleData.Id
}
}
}
# Get all eligible role assignments
$eligibleRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All -ExpandProperty RoleDefinition -Property PrincipalId, RoleDefinition, RoleDefinitionId, DirectoryScopeId
# Get accounts to reference
$users = Get-MgUser -Property DisplayName, Id, UserPrincipalName -All
$servicePrincipals = Get-MgServicePrincipal -Property DisplayName, Id, AppId -All
$groups = Get-MgGroup -ExpandProperty Members -Filter "IsAssignableToRole eq true" -Property DisplayName, Id, Description, Members -All
$processedEligibleRoles = @()
foreach ($role in $eligibleRoles) {
# Work out the account type and get details
$accountType = "User"
$accountDetails = $users | Where-Object {$_.Id -eq $role.PrincipalId}
if($null -eq $accountDetails) {
$accountDetails = $servicePrincipals | Where-Object {$_.Id -eq $role.PrincipalId}
$accountType = "Service Principal"
}
if($null -eq $accountDetails) {
$accountDetails = $groups | Where-Object {$_.Id -eq $role.PrincipalId}
$accountType = "Group"
}
# Save the details dependent on whether the assignment is a user or group
if($accountType -eq "User" -or $accountType -eq "Service Principal") {
# Get any related activity logs for the given user and role
$relatedActivityLogs = $activityLogs | Where-Object {$_.id -eq $accountDetails.Id -and $_.TargetRoleId -eq $role.RoleDefinitionId}
$processedEligibleRoles += [pscustomobject]@{
DisplayName = $accountDetails.DisplayName
PrincipalName = $accountDetails.UserPrincipalName || $accountDetails.AppId
InUse = $relatedActivityLogs.Length -gt 0
LastUsed = if($relatedActivityLogs.Length -gt 0) {$relatedActivityLogs | Measure-Object -Property TimeGenerated -Maximum | Select-Object -ExpandProperty Maximum} else {"Never"}
GroupName = "N/A"
GroupDescription = "N/A"
RoleDisplayName = $role.RoleDefinition.DisplayName
DirectoryScopeId = $role.DirectoryScopeId
AccountType = $accountType
AssignmentType = "Direct"
AccountId = $accountDetails.Id
GroupId = "N/A"
RoleId = $role.RoleDefinitionId
}
continue;
}
elseif($accountType -eq "Group") { # If it's a group then evaluate each member and store as seperate rows
for($i=0; $i -lt $accountDetails.Members.Length; $i++) {
$user = $users | Where-Object {$_.Id -eq $accountDetails.Members[$i].Id}
# Get any related activity logs for the given user and role
$relatedActivityLogs = $activityLogs | Where-Object {$_.id -eq $user.Id -and $_.TargetRoleId -eq $role.RoleDefinitionId}
$processedEligibleRoles += [pscustomobject]@{
DisplayName = $user.DisplayName
PrincipalName = $user.UserPrincipalName
InUse = $relatedActivityLogs.Length -gt 0
LastUsed = if($relatedActivityLogs.Length -gt 0) {$relatedActivityLogs | Measure-Object -Property TimeGenerated -Maximum | Select-Object -ExpandProperty Maximum} else {"Never"}
GroupName = $accountDetails.DisplayName
GroupDescription = $accountDetails.Description
RoleDisplayName = $role.RoleDefinition.DisplayName
DirectoryScopeId = $role.DirectoryScopeId
AccountType = "User"
AssignmentType = "Group"
AccountId = $user.Id
GroupId = $accountDetails.Id
RoleId = $role.RoleDefinitionId
}
}
continue;
}
else {
$processedEligibleRoles += [pscustomobject]@{
DisplayName = "None"
PrincipalName = "N/A"
InUse = $false
LastUsed = if($relatedActivityLogs.Length -gt 0) {$relatedActivityLogs | Measure-Object -Property TimeGenerated -Maximum | Select-Object -ExpandProperty Maximum} else {"Never"}
GroupName = "N/A"
GroupDescription = "N/A"
RoleDisplayName = $role.RoleDefinition.DisplayName
DirectoryScopeId = $role.DirectoryScopeId
AccountType = "Deleted"
AssignmentType = "N/A"
AccountId = "N/A"
GroupId = "N/A"
RoleId = $role.RoleDefinitionId
}
}
}
# Export the result as a CSV then open it
$processedEligibleRoles | Export-Csv -Path $exportFilePath -Force
Invoke-Item $exportFilePath
💡 Pro Tip – Run this script monthly to keep your privileged access policies tight and secure.
Have questions? Drop a comment below! 👇







Leave a comment