Group Membership Change Summary with Microsoft Graph PowerShell

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
Loading...