Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Beep Beep! Catching Road Runner Admins: A PowerShell Pursuit of Privilege Truth

Accountability in the Digital Age: Making Shadow Work Visible

Some people prefer to work in the shadows, believing their privileged activities go unnoticed. They would be wrong.

In environments with Privileged Identity Management (PIM), users activating highly privileged roles must provide a justification. Unfortunately, this is often just a freeform text field where users can enter whatever they desire and then proceed to do something entirely different with that access. Worse still, many provide vague activation reasons that leave you wondering what was actually accomplished with those elevated permissions.

This script fills in the missing pieces by looking for specific activities in the audit logs and reporting on them, allowing you to build a complete picture from the CSV output about what actions were actually taken with that privileged access. It transforms vague PIM justifications into concrete evidence of what someone did—or didn't do—during their elevated session.

Many people won't appreciate this level of auditing because they've grown comfortable operating behind the scenes, thinking their activities blend into the noise of system logs. This tool changes that dynamic by cutting through the noise and highlighting exactly what human-initiated changes occurred, when, and by whom.

The Problem: Noise in Audit Logs

I recently found myself struggling with this exact challenge. While Azure AD audit logs capture everything, they're filled with noise that makes it difficult to identify genuine permission changes. The logs include:

  • System-initiated activities that happen automatically
  • PIM activations/deactivations that are just users exercising existing permissions
  • Read-only operations like viewing groups or applications
  • Routine activities that don't actually change permissions

What I needed was a way to focus specifically on human-initiated permission changes - the activities that actually modify who can access what in my environment.

Lets walkthrough the solution logic using Microsoft Graph API to query audit logs with intelligent filtering. The script focuses on permission-related activities while excluding the noise.

Prerequisites and Permissions

To use this script, you'll need an Azure AD app registration with the following API permissions:

# Required Microsoft Graph permissions
AuditLog.Read.All          # Read audit log data
Directory.Read.All         # Read directory information  
Policy.Read.All           # Read policy information
User.Read                 # Read user profiles
UserAuthenticationMethod.Read.All  # Read auth methods

Categories Monitored

The script monitors several key categories of activities:

# Main categories checked
$categories = @(
    "GroupManagement",      # Group membership changes
    "RoleManagement",       # Role assignments and permissions
    "ApplicationManagement", # App registrations and enterprise apps
    "Policy",              # Policy changes
    "UserManagement",      # User-related changes
    "DeviceManagement",    # Intune device management
    "ResourceManagement"   # Azure resource permissions
)

Intelligent Filtering Logic

The core of the script lies in its filtering logic, which includes activities while excluding noise:

# Include actual permission changes
$targetActivities = @(
    "Add member to group",
    "Remove member from group",
    "Create application", 
    "Delete application",
    "Add app role assignment to user",
    "Remove app role assignment from user",
    "Register application",
    "Unregister application",
    "Create device compliance policy",
    "Update app protection policy"
    # ... and many more
)

# Exclude routine activities and noise
$excludeActivities = @(
    "Activate role",           # PIM activation (using existing permission)
    "Deactivate role",         # PIM deactivation  
    "GroupsODataV4_Get",       # Read-only operations
    "Get group",
    "Get user",
    "List applications"
)

The script also uses pattern matching to catch activities it might not explicitly list:

# Pattern-based filtering
($_.ActivityDisplayName -like "*permission*" -and $_.ActivityDisplayName -notlike "*Get*") -or
($_.ActivityDisplayName -like "*application*" -and $_.ActivityDisplayName -notlike "*Get*") -or
($_.ActivityDisplayName -match "(Create|Delete|Add|Remove|Update|Assign).*")

Usage and Command Line Options

The script accepts several parameters to customize the audit scope:

.\entra-audit-permissions.ps1 -UPN "lee@bythepowerofgreyskull.com" -TimeFrame "7days"

Available Time Frames

The script provides the option of 1 day, 7 days or 30 days (the default is 7 days if not specified)

.\entra-audit-permissions.ps1 -UPN "lee@bythepowerofgreyskull.com" -TimeFrame "1day"
.\entra-audit-permissions.ps1 -UPN "lee@bythepowerofgreyskull.com" -TimeFrame "7days"
.\entra-audit-permissions.ps1 -UPN "lee@bythepowerofgreyskull.com
" -TimeFrame "30days"

Connection and Authentication

The script uses app registration credentials for authentication:

# App Registration Connection
$tenantId = "your-tenant-id"
$clientId = "your-app-id"  
$clientSecret = "your-app-secret"

# Connect using app registration
$secureSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $credential

Output and Reporting

The script provides both console output and CSV export for further analysis:

Console Output

The script displays results with color-coded sections:

  • User resolution and validation
  • Category-by-category search progress
  • Detailed activity listings with timestamps, targets, and context
  • Summary statistics by activity type
That looks like this:


CSV Export

Each run automatically generates a CSV file when changes are detected which is then shown as the below, meaning you can then review the data in that CSV file.

"DateTime","Activity","Category","Result","InitiatedBy","TargetResources","PIMRelated","AdditionalDetails"
"11/08/2025 16:42:24","Update policy","Policy","success","System","Script Test : Did you catch this?","False","User-Agent: Microsoft Azure Graph Client Library 1.0"
"11/08/2025 16:42:24","Update conditional access policy","Policy","success","Lee Croucher (Lee.Croucher@bythepowerofgreyskull.com)","Script Test : Did you catch this?","False","Category: Conditional Access"Example CSV filename: PermissionAuditLog_Lee_Croucher_20250811_132041.csv.

Conclusion

This script has transformed how I approach permission auditing in Azure AD environments. By focusing on signal over noise, it enables security teams to efficiently monitor and investigate actual permission changes while filtering out the routine operational activities that typically clutter audit logs.

The automated CSV export also makes it easy to incorporate into existing security workflows, whether that's regular compliance reporting or incident investigation processes.


Previous Post Next Post

نموذج الاتصال