Learn how to track who commonly adds or removes members from a Microsoft Entra ID group using Microsoft Graph PowerShell.
# Validated on Microsoft.Graph PowerShell SDK v2.29.1
$ErrorActionPreference = 'stop'
$requiredScopes = @('Group.Read.All', 'AuditLog.Read.All')
$ctx = Get-MgContext
if (-not $ctx -or ($requiredScopes | Where-Object { $ctx.Scopes -notcontains $_ })) {
Connect-MgGraph -Scopes $requiredScopes -NoWelcome
}
function Get-MgGroupMembershipAuditSummary {
[CmdletBinding()]
param(
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('GroupId', 'GroupName')]
[ValidateNotNullOrEmpty()]
[string]$Group,
[ValidateRange(1, 30)]
[int]$DaysAgo = 7
)
begin {
$resolvedGroupIds = @()
}
process {
$tmpGuid = [guid]::Empty
if ([guid]::TryParse($Group, [ref]$tmpGuid)) {
$resolvedGroupIds += , $Group
}
else {
$escaped = $Group -replace "'", "''"
$exact = Get-MgGroup -All -Filter "displayName eq '$escaped'" -ErrorAction Stop
if (-not $exact) {
$starts = Get-MgGroup -All -Filter "startsWith(displayName,'$escaped')" -ErrorAction Stop
if (-not $starts) {
throw "No groups found with displayName matching '$Group'."
}
$names = ($starts | Select-Object -First 5 -ExpandProperty DisplayName) -join ', '
if ($starts.Count -gt 1) {
throw "Ambiguous group name '$Group'. Candidates: $names"
}
$resolvedGroupIds += , ($starts | Select-Object -ExpandProperty Id -First 1)
}
else {
if ($exact.Count -gt 1) {
$names = ($exact | Select-Object -First 5 -ExpandProperty DisplayName) -join ', '
throw "Multiple exact matches for '$Group'. Candidates: $names"
}
$resolvedGroupIds += , ($exact | Select-Object -ExpandProperty Id -First 1)
}
}
}
end {
if (-not $resolvedGroupIds -or $resolvedGroupIds.Count -eq 0) {
throw "Unable to resolve any group IDs from input '$Group'."
}
$startDate = (Get-Date).ToUniversalTime().AddDays(-$DaysAgo).ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
$targetClause = ($resolvedGroupIds | ForEach-Object { "targetResources/any(t: t/id eq '$_')" }) -join ' or '
$filter = @(
"(activityDisplayName eq 'Add member to group' or activityDisplayName eq 'Remove member from group')"
"result eq 'success'"
"activityDateTime ge $startDate"
"($targetClause)"
) -join ' and '
$params = @{ All = $true; PageSize = 999; Filter = $filter }
$logs = Get-MgAuditLogDirectoryAudit @params
$mapped = $logs | ForEach-Object {
$initiated = 'Unknown'
if ($_.InitiatedBy) {
if ($_.InitiatedBy.User.UserPrincipalName) { $initiated = $_.InitiatedBy.User.UserPrincipalName }
elseif ($_.InitiatedBy.App.DisplayName) { $initiated = 'App: ' + $_.InitiatedBy.App.DisplayName }
elseif ($_.InitiatedBy.ServicePrincipal.DisplayName) { $initiated = 'SP: ' + $_.InitiatedBy.ServicePrincipal.DisplayName }
}
[pscustomobject]@{ InitiatedBy = $initiated; Action = $_.ActivityDisplayName }
}
$byInitiator = $mapped | Group-Object InitiatedBy
$result = foreach ($g in $byInitiator) {
$adds = ($g.Group | Where-Object { $_.Action -eq 'Add member to group' }).Count
$removes = ($g.Group | Where-Object { $_.Action -eq 'Remove member from group' }).Count
[pscustomobject]@{
InitiatedBy = $g.Name
AddCount = $adds
RemoveCount = $removes
}
}
$result | Sort-Object InitiatedBy
}
}
# Get-MgGroupMembershipAuditSummary -Group 'MyGroup'
# InitiatedBy AddCount RemoveCount
# ----------- -------- -----------
# ga@contoso.com 0 1
# user@contoso 10 5