Private/EntraMonitor/Detections/Test-EntraPrivilegedRoleChange.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Test-EntraPrivilegedRoleChange {
    [CmdletBinding()]
    param(
        [hashtable[]]$AuditEvents = @()
    )

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Activity display names that indicate role assignment changes
    $roleActivities = @(
        'Add member to role'
        'Add eligible member to role'
        'Add scoped member to role'
        'Remove member from role'
        'Remove eligible member from role'
        'Add member to role in PIM requested (permanent)'
        'Add member to role in PIM requested (timebound)'
        'Add member to role completed (PIM activation)'
    )

    # Sensitive directory role keywords
    $sensitiveRoleKeywords = @(
        'Admin'
        'Administrator'
        'Privileged'
        'Security'
        'Compliance'
        'Exchange'
        'SharePoint'
        'Intune'
        'Authentication'
        'Helpdesk'
        'Password'
        'Billing'
        'License'
        'User Account'
        'Application'
        'Cloud App'
        'Conditional Access'
    )

    foreach ($event in $AuditEvents) {
        $activity = $event.ActivityDisplayName
        $isRoleActivity = $false
        foreach ($ra in $roleActivities) {
            if ($activity -match [regex]::Escape($ra)) {
                $isRoleActivity = $true
                break
            }
        }
        # Also catch category-based role changes
        if (-not $isRoleActivity -and $event.Category -eq 'RoleManagement') {
            $isRoleActivity = $true
        }
        if (-not $isRoleActivity) { continue }

        # Extract role name from target resources
        $roleName = ''
        $targetUser = ''
        foreach ($resource in $event.TargetResources) {
            if ($resource.Type -eq 'Role') {
                $roleName = $resource.DisplayName
            }
            if ($resource.Type -eq 'User') {
                $targetUser = $resource.UserPrincipalName
                if (-not $targetUser) { $targetUser = $resource.DisplayName }
            }
            # Check modified properties for role name
            foreach ($prop in $resource.ModifiedProperties) {
                if ($prop.DisplayName -eq 'Role.DisplayName' -and $prop.NewValue) {
                    $roleName = $prop.NewValue -replace '"', ''
                }
            }
        }

        # Determine if this is a sensitive role
        $isSensitive = $false
        foreach ($keyword in $sensitiveRoleKeywords) {
            if ($roleName -match $keyword) {
                $isSensitive = $true
                break
            }
        }

        $initiator = $event.InitiatedBy.UserPrincipalName
        if (-not $initiator) { $initiator = $event.InitiatedBy.AppDisplayName }

        $results.Add([PSCustomObject]@{
            Timestamp     = $event.Timestamp
            Activity      = $activity
            Result        = $event.Result
            InitiatedBy   = $initiator
            RoleName      = $roleName
            TargetUser    = $targetUser
            IsSensitive   = $isSensitive
            Category      = $event.Category
            CorrelationId = $event.CorrelationId
        })
    }

    return @($results)
}