Use the Audit Log to Monitor Membership Changes in Selected Microsoft 365 Groups

Use PowerShell to Monitor Group Membership Changes

A reader asks how to monitor specific Azure AD groups so that they are notified if anyone updates the membership of these groups. Because of the pervasive use of Teams in Microsoft 365 tenants and the range of resources available to team members, it’s quite common to find that organizations want to keep an eye on some sensitive groups, like those used by senior management or for confidential purposes, like merger and acquisition activities. Various commercial products include this functionality but it’s always fun to see if you can create a solution from the out-of-the-box components available to tenants.

I’ve been down a similar road in the past, such as reporting the deletions of Microsoft 365 groups, but this request is a little different because it involves marking groups to be monitored. My initial thought was if this could be a scenario to use Azure AD custom security attributes? Administrators can define attribute sets and assign attributes to Azure AD objects. Unfortunately, Azure AD currently only supports custom security attributes for user objects, managed identities, and service principals (for apps). Azure AD custom security attributes are still in preview and the range of supported object types is likely to change before general availability but doesn’t help in this situation.

Exchange Online Custom Attributes for Groups

Exchange Online mail-enabled objects, like groups, have fifteen custom attributes capable of storing single values and five multi-value custom attributes. It should be easy to assign a custom attribute to mark groups for monitoring. In this example, I’m checking for membership changes to marked Microsoft 365 groups.

The first thing is to update the targeted Microsoft 365 groups with the flag. This is easily done with the Set-UnifiedGroup cmdlet (neither the Exchange admin center nor the Microsoft 365 admin center support access to the custom attributes):

Set-UnifiedGroup -Identity "Contract Workers" -CustomAttribute15 "Monitor"

After marking the groups, it’s possible to find the groups with a server-side filter. This is important because we don’t want to have to retrieve every group and then check its properties:

[array]$Groups = Get-UnifiedGroup -Filter {CustomAttribute15 -eq "Monitor"}

Writing the Code

The code to report membership changes to monitored groups has the following steps:

  • Find the set of monitored Microsoft 365 Groups.
  • Build a hash table of the group object identifiers and display names. For maximum performance, the script uses the hash table to check if an audit record relates to a monitored group.
  • Define a variable holding the set of audit events to look for.
  • Define the start and end date for the search. In this example, we look back 30 days.
  • Run the Search-UnifiedAuditLog cmdlet to perform the search.
  • Examine each event returned by the search to check if it’s related to a monitored group. If it is, capture information about the event.

Here’s the code to monitor group membership changes:

Connect-ExchangeOnline
Write-Host "Finding groups to monitor..."
[array]$Groups = Get-UnifiedGroup -Filter {CustomAttribute15 -eq "Monitor"}
If (!($Groups)) {Write-Host "No groups found to monitor - exiting" ; break }

$GroupIds = @{}
ForEach ($G in $Groups) {
   $GroupIds.Add($G.ExternalDirectoryObjectId,$G.DisplayName) }

$Operations = 'Add member to group', "Remove member from group"
$StartDate = (Get-Date).AddDays(-30); $EndDate = (Get-Date) 

Write-Host "Finding audit records for group member adds and deletes..."
[array]$Records = (Search-UnifiedAuditLog -Operations $Operations -StartDate $StartDate -EndDate $EndDate -ResultSize 1000 -Formatted)
If (!($Records)) { Write-Host "No audit records found for add or delete members from monitored groups - exiting" ; break }

$Report = [System.Collections.Generic.List[Object]]::new() # Create output file 

ForEach ($Rec in $Records) {
   $AuditData = ConvertFrom-Json $Rec.Auditdata
   Switch ($AuditData.Operation) {
    "Remove member from group." {
       $GroupId = $AuditData.ModifiedProperties | Where-Object {$_.Name -eq 'Group.ObjectId'} | Select-Object -ExpandProperty OldValue }
    "Add member to group." {
       $GroupId = $AuditData.ModifiedProperties | Where-Object {$_.Name -eq 'Group.ObjectId'} | Select-Object -ExpandProperty NewValue }
   }
   $GroupName = $GroupIds[$GroupId]
   If ($GroupName -ne $Null) { # Update is for one of the monitored groups
      
      $ReportLine = [PSCustomObject] @{
        TimeStamp = Get-Date($AuditData.CreationTime) -format g
        User      = $AuditData.UserId
        Group     = $GroupName 
        Member    = $AuditData.ObjectId
        Action    = $AuditData.Operation }        
      $Report.Add($ReportLine) 
  } #End if  

} # End ForEach audit record

$Report | Out-GridView

Figure 1 shows the result of the search and analysis of the audit records to find events relating to membership changes in the monitored groups.

Details of additions and removals to group membership

Monitor group membership changes
Figure 1: Details of additions and removals to group membership

Monitor Group Membership Changes Automatically

This is an excellent example of the kind of periodic check that’s suitable for execution by an Azure Automation runbook with the results delivered by email or posted to a Teams channel for action by whoever’s responsible for monitoring the membership of the groups.

The point is that the audit log holds a lot of useful information that can answer questions about Microsoft 365 operations. All you need to do is take advantage of the available data.


Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work, including how to monitor group membership changes without paying for another product.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.