Back to script library
Entra / Microsoft 365 · Groups

Report M365 group memberships

A script to report the membership of all Microsoft 365 Groups in a tenant.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

Connect-MgGraph -Scope Organization.Read.All, GroupMember.Read.All -NoWelcome

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

Clear-Host
Connect-MgGraph -Scope Organization.Read.All, GroupMember.Read.All -NoWelcome
$OrgName = (Get-MgOrganization).displayName
$Version = "2.0"
$ReportFile = "c:\temp\M365MembersReport.html"
$CSVFileSummary = "c:\temp\M365MembersSummaryReport.csv"
$CSVFileMembers = "c:\temp\M365MembersReport.csv"
$MemberList = [System.Collections.Generic.List[Object]]::new()
$SummaryData = [System.Collections.Generic.List[Object]]::new()
$CreationDate = Get-Date -format g
Clear-Host
Write-Host "Fetching user information from Entra ID..."
[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual `
-CountVariable Records -All -PageSize 500
If (!($Users)) {
Write-Host "Can't get user information from Entra ID - exiting" ; break
}
$Users = $Users | Sort-Object DisplayName
$S1 = Get-Date
# Get a list of Teams and put them into a hash table so that we can mark the groups we process as being team-enabled
[array]$Teams = Get-MgTeam -All
$TeamsHash = @{}
$Teams.ForEach( {
$TeamsHash.Add($_.Id, $_.DisplayName) } )
Clear-Host
# Set up progress bar
$ProgDelta = 100/($Users.Count); $CheckCount = 0; $UserNumber = 0
ForEach ($User in $Users) {
$UserNumber++
$UserStatus = $User.DisplayName + " ["+ $UserNumber +"/" + $Users.Count + "]"
Write-Progress -Activity "Checking groups for user" -Status $UserStatus -PercentComplete $CheckCount
$CheckCount += $ProgDelta
$UserType = "Tenant user"
# Find any groups for the user
[array]$Groups = Get-MgUserMemberOfAsGroup -UserId $User.Id -All
If ($Groups) { # We found some groups for this recipient - process them
[string]$AllGroups = $Groups.displayName -join ", "
ForEach ($Group in $Groups) {
[array]$Owners = $null # Get group owners
[array]$Owners = Get-MgGroupOwner -GroupId $Group | Select-Object -ExpandProperty AdditionalProperties
$OwnersOutput = $Owners.displayName -join ", "
$GroupOwnersEmail = $Owners.mail -join ", "
If ($TeamsHash[$Group]) {
$GroupName = $Group.DisplayName + " (** Team **)"
} Else {
$GroupName = $Group.DisplayName
}
$MemberLine = [PSCustomObject][Ordered]@{ # Write out details of the group
"User" = $User.DisplayName
UPN = $User.UserPrincipalName
"User type" = $User.UserType
"Group Name" = $GroupName
"Group Description" = $Group.description
"Group Email" = $Group.mail
"Group Owners" = $OwnersOutput
"Owners Email" = $GroupOwnersEmail
}
$MemberList.Add($MemberLine)
}
$SummaryLine = [PSCustomObject][Ordered]@{ # Write out summary record for the user
"User" = $User.DisplayName
UPN = $User.UserPrincipalName
"User type" = $User.UserType
"Groups count" = $Groups.count
"Member Of" = $AllGroups
}
$SummaryData.Add($SummaryLine)
} Else { #No groups found for this user, so just write a summary record
$SummaryLine = [PSCustomObject][Ordered]@{
"User" = $User.DisplayName
UPN = $User.UserPrincipalName
"User type" = $UserType
"Groups count" = 0
"Member Of" = "No groups found for user"
}
$SummaryData.Add($SummaryLine)
}
} #End For
$SummaryData = $SummaryData | Sort-Object "Groups Count" -Descending
$GCount = $MemberList | Select-Object "Group Email" | Sort-Object "Group EMail" -Unique
$UsersNoGroups = ($SummaryData | Where-Object {$_."Groups Count" -eq 0}).Count
$UsersWithGroups = ($SummaryData.Count - $UsersNoGroups)
$S2 = Get-Date
$TotalSeconds = [math]::round(($S2-$S1).TotalSeconds,2)
$SecondsPerUser = [math]::round(($TotalSeconds/$Users.count),2)
# Create the HTML report
$htmlhead="<html>
<style>
BODY{font-family: Arial; font-size: 8pt;}
H1{font-size: 22px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}
H2{font-size: 18px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}
H3{font-size: 16px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}
TABLE{border: 1px solid black; border-collapse: collapse; font-size: 8pt;}
TH{border: 1px solid #969595; background: #dddddd; padding: 5px; color: #000000;}
TD{border: 1px solid #969595; padding: 5px; }
td.pass{background: #B7EB83;}
td.warn{background: #FFF275;}
td.fail{background: #FF2626; color: #ffffff;}
td.info{background: #85D4FF;}
</style>
<body>
<div align=center>
<p><h1>Microsoft 365 Groups and Teams Membership Report</h1></p>
<p><h2><b>Microsoft 365 Groups in the " + $Orgname + " organization</b></h2></p>
<p><h3>Generated: " + (Get-Date -format g) + "</h3></p></div>"
$htmlbody1 = $MemberList | ConvertTo-Html -Fragment
$htmlbody1 = $htmlbody1 + '<div class="page-break"></div>'
$htmlbody2 = $SummaryData | ConvertTo-Html -Fragment
$htmltail = "<p>Report created for: " + $OrgName + "</p>" +
"<p>Created: " + $CreationDate + "<p>" +
"<p>-----------------------------------------------------------------------------------------------------------------------------</p>"+
"<p>Number of users in groups: " + $UsersWithGroups + "</p>" +
"<p>Number of users not in groups: " + $UsersNoGroups + "<p>"+
"<p>Number of Microsoft 365 Groups: " + $GCount.Count + "</p>" +
"<p>Number of Microsoft Teams: " + $Teams.Count + "</p>" +
"<p>-----------------------------------------------------------------------------------------------------------------------------</p>"+
"<p>Microsoft 365 Group Membership Report <b>" + $Version + "</b>"
$htmlreport = $htmlhead + $htmlbody1 + "<p><p>" + $htmlbody2 + $htmltail
$htmlreport | Out-File $ReportFile -Encoding UTF8
$MemberList | Export-CSV -NoTypeInformation $CSVFileMembers
$SummaryData | Export-CSV -NoTypeInformation $CSVFileSummary
Clear-Host
Write-Host "Microsoft 365 Group Membership Report - Job Complete"
Write-Host "----------------------------------------------------"
Write-Host " "
Write-Host "Outputs:"
Write-Host "--------"
Write-Host "HTML report available in" $ReportFile
Write-Host " "
Write-Host "Contains all the data generated by the script."
Write-Host " "
Write-Host "CSV file for members in groups available in" $CSVFileMembers
Write-Host " "
Write-Host "Lists details of group membership for individual user accounts."
Write-Host " "
Write-Host "CSV summary report available in" $CSVFileSummary
Write-Host " "
Write-Host "Summarizes the groups that users belong to."
Write-Host " "
Write-Host ("Total processing time {0} seconds ({1} seconds per user) for {2} user accounts and {3} Microsoft 365 Groups" -f $TotalSeconds, $SecondsPerUser, $Users.Count, $Gcount.count)
Attribution