Entra / Microsoft 365 · Users & guests
Report PIM role assignments by email
Search the Entra ID audit log for PIM role assignment events and email a weekly report to specified addresses.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -NoWelcome -Scopes AuditLog.Read.All, RoleManagement.Read.Directory, Mail.Send
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([int] $LookbackDays = 7,[string] $StartDate = (Get-Date).AddDays(-$LookbackDays).toString('yyyy-MM-dd'),[string] $DestinationEmailAddress = "Admin.Info@contoso.com")[array]$RequiredScopes = "AuditLog.Read.All","RoleManagement.Read.Directory","Mail.Send"$Interactive = $false# Define the mailboxes that are used to send and receive the report. If run interactvely, the email goes from the signed-in account$MsgFrom = 'Customer.Services@contoso.com'# Determine if we're interactive or notIf ([Environment]::UserInteractive) {# We're running interactively...Clear-HostWrite-Host "Script running interactively... connecting to the Graph" -ForegroundColor YellowConnect-MgGraph -NoWelcome -Scopes $RequiredScopes$MsgFrom = (Get-MgContext).Account$Interactive = $true} Else {# We're not, so likely in Azure AutomationWrite-Output "Executing the runbook to create the last activity report for guest access outside the tenant..."Connect-MgGraph -Identity -NoWelcome}# Check that we have the right permissions - in Azure Automation, we assume that the automation account has the right permissionsIf ($Interactive) {[int]$RequiredScopesCount = $RequiredScopes.Count[string[]]$CurrentScopes = (Get-MgContext).Scopes[string[]]$RequiredScopes = $RequiredScopes$CheckScopes =[object[]][Linq.Enumerable]::Intersect($RequiredScopes,$CurrentScopes)If ($CheckScopes.Count -ne $RequiredScopesCount ) {Write-Host ("To run this script, you need to connect to Microsoft Graph with the following scopes: {0}" -f $RequiredScopes) -ForegroundColor RedBreak}}# Retrieve role assignment events from the last weekWrite-Output "Checking for role assignments in the last week since $StartDate..."[array]$Records = Get-MgAuditLogDirectoryAudit -All -Filter `"(activityDisplayName eq 'Add member to role' or activityDisplayName eq 'Remove member from role') and ActivityDateTime gt $StartDate"If ($Records.Count -eq 0) {Write-Host "No PIM role assignment events found in the last week." -ForegroundColor YellowBreak} Else {Write-Output ("Found {0} PIM role assignment events in the last week." -f $Records.Count)}$CriticalRoles = @("Global Administrator","Privileged Role Administrator","User Administrator","Security Administrator","Exchange Administrator","SharePoint Administrator","Teams Administrator")Write-Output "Processing audit records to build report..."$Report = [System.Collections.Generic.List[Object]]::new()ForEach ($Record in $Records) {$CriticalFlag = $falseIf ($Record.ActivityDisplayName -eq 'Add member to role') {$AssignmentType = "Add role"} ElseIf ($Record.ActivityDisplayName -eq 'Remove member from role') {$AssignmentType = "Remove role"} Else {$AssignmentType = "Unknown"}If ($Record.initiatedBy.app.displayName -eq 'MS-PIM') {# Skip system initiated changes$AssignmentMethod = "Privileged Identity Management"$ActionedBy = "MS-PIM"} Else {$ActionedBy = $Record.initiatedBy.user.userPrincipalName$AssignmentMethod = "Direct Assignment"}$Role = (Get-MgDirectoryRole -DirectoryRoleId $Record.TargetResources[1].Id).DisplayNameIf ($Role -in $CriticalRoles) {$CriticalFlag = $true}$TargetUser = Get-MgUser -UserId $Record.TargetResources[0].Id$ReportItem = [PSCustomObject]@{'Assigned Role' = $Role'Assignee' = $TargetUser.DisplayName'Assignee UPN' = $TargetUser.UserPrincipalName'Actioned By' = $ActionedBy'Assignment Method' = $AssignmentMethod'Assignment Date' = Get-Date $Record.ActivityDateTime -format 'dd-MMM-yyyy HH:mm''Assignment Type' = $AssignmentType'Is Critical Role' = If ($CriticalFlag) { "🚨" } Else { $null }}$Report.Add($ReportItem)}# Build attachment for the emailWrite-Output "Generating output... "If (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $trueImport-Module ImportExcel -ErrorAction SilentlyContinue$ExcelOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\PIM Role Assignments.xlsx"If (Test-Path $ExcelOutputFile) {Remove-Item $ExcelOutputFile -ErrorAction SilentlyContinue}$Report | Export-Excel -Path $ExcelOutputFile -WorksheetName "PIM Role Assignments" -Title ("PIM Role Assignments Report {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "PIMRoleAssignments" -AutoSize -AutoFilter$AttachmentFile = $ExcelOutputFile} Else {$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\PIM Role Assignments.CSV"$Report | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding Utf8$AttachmentFile = $CSVOutputFile}If ($ExcelGenerated) {Write-Output ("Excel worksheet output written to {0}" -f $ExcelOutputFile)} Else {Write-Output ("CSV output file written to {0}" -f $CSVOutputFile)}# Send the spreadsheet as an email attachment$EncodedAttachmentFile = [Convert]::ToBase64String([IO.File]::ReadAllBytes($AttachmentFile))$MsgAttachments = @(@{'@odata.type' = '#microsoft.graph.fileAttachment'Name = (Split-Path $AttachmentFile -Leaf)ContentBytes = $EncodedAttachmentFileContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})$HTMLAddRole = $Report | Where-Object {$_.'Assignment Type' -eq "Add role"} | Select-Object 'Assignment Date', 'Assigned Role','Assignee','Assignee UPN','Actioned By','Is Critical Role' | ConvertTo-Html -Fragment$HTMLRemoveRole = $Report | Where-Object {$_.'Assignment Type' -eq "Remove role"} | Select-Object 'Assignment Date', 'Assigned Role','Assignee','Assignee UPN','Actioned By','Is Critical Role' | ConvertTo-Html -Fragment# Build the array of a single TO recipient detailed in a hash table - change the sender and recipient to the appropriate recipient for your tenant$ToRecipient = @{}$ToRecipient.Add("emailAddress",@{'address'=$DestinationEmailAddress})[array]$MsgTo = $ToRecipient$MsgSUbject = "Weekly Role Assignments Report - $(Get-Date -Format 'dd-MMM-yyyy')"$HtmlMsg = "</body></html><p>The output file for the <b>Weekly Role Assignments Report</b> is attached to this message. Please review the information at your convenience.</p>"$HtmlMsg += "<h2>Weekly Role Assignment Report</h2>"$HtmlMsg += "<p><h3>Role Assignments Made</h3></p>"$HtmlMsg += $HTMLAddRole$HtmlMsg += "<p><h3>Role Assignments Removed</h3></p>"$HtmlMsg += $HTMLRemoveRole$htmlMsg += "</body></html>"# Construct the message body$MsgBody = @{}$MsgBody.Add('Content', "$($HtmlMsg)")$MsgBody.Add('ContentType','html')# Build the pmessage parameters$Message = @{}$Message.Add('subject', $MsgSubject)$Message.Add('toRecipients', $MsgTo)$Message.Add('body', $MsgBody)$Message.Add("attachments", $MsgAttachments)$EmailParameters = @{}$EmailParameters.Add('message', $Message)$EmailParameters.Add('saveToSentItems', $true)$EmailParameters.Add('isDeliveryReceiptRequested', $true)# Send the messageTry {Send-MgUserMail -UserId $MsgFrom -BodyParameter $EmailParameters -ErrorAction StopWrite-Output ("Weekly role assignments report emailed to {0}" -f $DestinationEmailAddress)Write-Output "All done!"} Catch {Write-Output "Unable to send email"Write-Output $_.Exception.Message}
Parameters
ParameterDefaultNotes
-LookbackDays7Number of days back to search for PIM role assignment audit events.-StartDate(Get-Date).AddDays(-7).toString('yyyy-MM-dd')Start of the PIM audit search window.-DestinationEmailAddress""Email address that receives the generated report.Attribution
Author
Office365itpros