Entra / Microsoft 365 ยท Teams
Report groups teams activity
A script to check the activity of Microsoft 365 Groups and Teams and report the groups and teams that might be deleted because they're not used.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-ExchangeOnline -SkipLoadingCmdletHelp
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $TenantId = "",[string] $AppId = "",[int] $LookbackDays = 365)=======# Uses an Entra ID registered app with consent for the following Graph application permissions:# Group.Read.All: Read groups# Reports.Read.All: Read usage reports# User.Read.All: Read users# GroupMember.Read.All: Read group membership# Sites.Read.All: Read SharePoint Online sites for groups# Organization.Read.All: Read details of tenant# Teams.ReadBasic.All: Read basic Teams information##+-------------------------- Functions etc. -------------------------Function Get-TeamsStats {# Function to retrieve per-team usage stats so that there's no need for the admin to download the report. The output is a hash table that# we check for Teams data[array]$TeamsData = $nullRemove-Item $TempDataFile -ErrorAction SilentlyContinue[array]$TeamsData = Get-MgReportTeamActivityDetail -Period D180 -OutFile $TempDataFile[array]$TeamsData = Import-Csv -Path $TempDataFile | Sort-Object 'Team Name'$PerTeamStats = [System.Collections.Generic.List[Object]]::new()$TeamsDataHash = @{}ForEach ($Team in $TeamsData) {If (!([string]::IsNullOrWhiteSpace($Team.'Last Activity Date'))) {$DaysSinceActive = (New-Timespan -Start ($Team.'Last Activity Date' -as [datetime]) -End ($Team.'Report Refresh date' -as [datetime])).Days$LastActiveDate = Get-Date ($Team.'Last Activity Date') -format dd-MMM-yyyy} Else {$DaysSinceActive = "> 90"$LastActiveDate = "More than 90 days ago"}$ReportLine = [PSCustomObject] @{Team = $Team.'Team Name'Privacy = $Team.'Team Type'TeamId = $Team.'Team Id'LastActivity = $LastActiveDateReportPeriod = $Team.'Report Period'DaysSinceActive = $DaysSinceActiveActiveUsers = $Team.'Active Users'Posts = $Team.'Post Messages'ChannelMessages = $Team.'Channel Messages'Replies = $Team.'Reply Messages'Urgent = $Team.'Urgent Messages'Mentions = $Team.MentionsGuests = $Team.GuestsActiveChannels = $Team.'Active Channels'ActiveGuests = $Team.'Active external users'Reactions = $Team.Reactions}$PerTeamStats.Add($ReportLine)# Update hash file$DataLine = [PSCustomObject] @{Id = $Team.'Team Id'DisplayName = $Team.'Team Name'Privacy = $Team.'Team Type'Posts = $Team.'Post Messages'Replies = $Team.'Reply Messages'Messages = $Team.'Channel messages'LastActivity = $LastActiveDateDaysSinceActive = $DaysSinceActive}$TeamsDataHash.Add([string]$Team.'Team Id', $DataLine)} #end ForEach$TeamsDataHash}# ------Clear-Host# Check that we are connected to Exchange Online$ModulesLoaded = Get-Module | Select-Object -ExpandProperty NameIf ("ExchangeOnlineManagement" -notin $ModulesLoaded) {Write-Host "Connecting to Exchange Online..."Connect-ExchangeOnline -SkipLoadingCmdletHelp}$CertificateThumbprint = "F79286DB88C21491110109A0222348FACF694CBD"# Connect to the Microsoft GraphConnect-MgGraph -NoWelcome -AppId $AppId -CertificateThumbprint $CertificateThumbprint -TenantId $TenantId$Global:TempDataFile = 'c:\temp\TempData.csv'$DataObfuscationOn = $False$Headers = @{}$Headers.Add("consistencyLevel", "eventual")$Headers.Add("Content-Type", "application/json")# We need full access to report data, so check the setting and update if necessaryIf ((Get-MgBetaAdminReportSetting).DisplayConcealedNames -eq $True) {$DataObfuscationOn = $True$Parameters = @{ displayConcealedNames = $False }Update-MgBetaAdminReportSetting -BodyParameter $Parameters}# Setup some stuff we use$WarningEmailDate = (Get-Date).AddDays(-$LookbackDays); $Today = (Get-Date)$TeamsEnabled = $False; $ObsoleteSPOGroups = 0; $ObsoleteEmailGroups = 0; $ArchivedTeams = 0$SharedDocFolder = "/Shared%20Documents" # These values are to allow internationalization of the SPO document library URL. For French, this would be "/Documents%20partages"$SharedDocFolder2 = "/Shared Documents" # Add both values$Version = "V6.00"$RunDate = Get-Date -format 'dd-MMM-yyyy HH:mm'# Get tenant name$OrgName = Get-MgOrganization | Select-Object -ExpandProperty DisplayName$HeaderInfo = ("Report generated for the {0} tenant on {1}" -f $OrgName, $RunDate)$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; }</style><body><div align=center><p><h1>Microsoft 365 Groups and Teams Activity Report</h1></p><p><h3>" + $HeaderInfo + "</h3></p></div>"Clear-HostWrite-Host ("Teams and Groups Activity Report {0} starting up..." -f $Version)$S1 = Get-Date #Start of processing# Get a list of Groups in the tenant[Int]$GroupsCount = 0; [int]$TeamsCount = 0# Retrieve Teams usage data$TeamsUsageHash = Get-TeamsStats# Get SharePoint site usage dataWrite-Host "Retrieving SharePoint Online site usage data..."Remove-Item $TempDataFile -ErrorAction SilentlyContinue[array]$SPOUsage = Get-MgReportSharePointSiteUsageDetail -Period D180 -Outfile $TempDataFile[array]$SPOUsage = Import-Csv -Path $TempDataFile | Sort-Object 'Owner display name'# This code inserted to fetch site information and build a hash table of SharePoint site URLs# to fix problem where Microsoft's API doesn't turn the URL in the usage data$DataTable = @{}[array]$SPOSiteData = Get-MgSite -All -PageSize 999Write-Host ("Processing details for {0} sites..." -f $SPOSiteData.count)$SPOSiteLookup = @{}ForEach ($S in $SPOSiteData) {$SPOSiteLookup.Add([string]$S.id.split(",")[1], [string]$S.weburl)}ForEach ($Site in $SPOUsage) {If ($Site."Root Web Template" -eq "Group") {If ([string]::IsNullOrEmpty($Site."Last Activity Date")) { # No activity for this site$LastActivityDate = $Null} Else {$LastActivityDate = Get-Date($Site."Last Activity Date") -format g$LastActivityDate = $LastActivityDate.Split(" ")[0]}$SiteDisplayName = $Site."Owner Display Name".IndexOf("Owners") # Extract site nameIf ($SiteDisplayName -ge 0) {$SiteDisplayName = $Site."Owner Display Name".SubString(0,$SiteDisplayName)} Else {$SiteDisplayName = $Site."Owner Display Name"}$StorageUsed = [string]([math]::round($Site."Storage Used (Byte)"/1GB,2)) + " GB"# Fix inserted here because Microsoft has screwed up the SharePoint site usage API$SiteURL = $Site.'Site URL'If ([string]::IsNullOrEmpty($SiteURL)) {$SiteURL = $SPOSiteLookup[$Site.'Site Id']}$SingleSiteData = @{'DisplayName' = $SiteDisplayName'LastActivityDate' = $LastActivityDate'FileCount' = $Site."File Count"'StorageUsed' = $StorageUsed }# Update hash table with details of the siteTry {$DataTable.Add([String]$SiteURL,$SingleSiteData)}Catch { # Error for some reason - it doesn't make much differenceWrite-Host ("Couldn't add details for site {0} to the DataTable - continuing" -f $SiteDisplayName)}}}# Create list of Microsoft 365 Groups in the tenant. We also get a list of Teams. In both cases, we build a hashtable# to store the object identifier and display name for the group.Write-Host ("Checking Microsoft 365 Groups and Teams for: {0}" -f $OrgName)Write-Host "This phase can take some time because we need to fetch every group in the organization to be able to"Write-Host "analyze its settings and activity. Please wait..."[array]$Groups = Get-MgGroup -Filter "groupTypes/any(a:a eq 'unified')" -PageSize 999 -All `-Property id, displayname, visibility, assignedlabels, description, createdDateTime, renewedDateTime, drive `-Sort "displayName DESC" -ConsistencyLevel "eventual" -CountVariable NumberGroupsIf (!($Groups)) {Write-Host "Can't find any Microsoft 365 Groups"; break}Write-Host ("Found {0} groups to process. Now analyzing each group." -f $Groups.Count)$i = 0$GroupsList = [System.Collections.Generic.List[Object]]::new()ForEach ($Group in $Groups) {$i++Write-Host ("Processing group {0} {1}/{2}" -f $Group.DisplayName, $i, $Groups.Count)# Check for expired access token and renew if necessary$GroupOwnerEmail = $Null; $OwnerNames = $Null# Get Group Owners[array]$GroupData = Get-MgGroupOwner -GroupId $Group.Id[array]$GroupOwners = $GroupData.AdditionalPropertiesIf ($GroupOwners) {If ($GroupOwners[0].'@odata.type' -eq '#microsoft.graph.user') { # Found a group owner, so extract names$OwnerNames = $GroupOwners.displayName -join ", "$GroupOwnerEmail = $GroupOwners[0].mail} Else { # ownerless group$OwnerNames = "No owners found"$GroupOwnerEMail = $null}}# Get SharePoint site URL$SPOUrl = $Null; $SPODocLib = $Null; $SPOLastDateActivity = $Null$Uri = ("https://graph.microsoft.com/v1.0/groups/{0}/drive" -f $Group.Id)Try {[array]$SPOData = Invoke-MgGraphRequest -Uri $Uri -Method Get} Catch {Write-Host ("Error fetching SharePoint data for {0}. Site migh be archived. Continuing..." -f $Group.DisplayName)$SPOData = $null}If ($SPOData) {[int]$LLVValue = $SPOData.WebUrl.IndexOf($SharedDocFolder) # Can we find a local-language value for the document library in the data returned?If (($SPOData.id) -and ($SPOData.DriveType -eq "documentLibrary")) { # Using language-specific values to identify the document library definedIf ($LLVValue -gt 0) { # If we have a local language value, parse it to extract the document library URL$SPOUrl = $SPOData.WebUrl.SubString(0,$SPOData.WebUrl.IndexOf($SharedDocFolder))$SPODocLib = $SPOUrl + $SharedDocFolder2# $SPOQuotaUsed = [Math]::Round($SPOData.quota.used/1Gb,2)$SPOLastDateActivity = Get-Date ($SPOData.lastModifiedDateTime) -format 'dd-MMM-yyyy HH:mm'} Else { # Just report what we read from the Graph$SPOUrl = $SPOData.WebUrl$SPODocLib = $SPOUrl + $SharedDocFolder2# $SPOQuotaUsed = [Math]::Round($SPOData.quota.used/1Gb,2)$SPOLastDateActivity = Get-Date ($SPOData.lastModifiedDateTime) -format 'dd-MMM-yyyy HH:mm'}}} Else {# Drive is probably locked because the site is archived, so get what we can...$Data = $SPOSiteData | Where-Object {$_.Name -eq $Group.displayname}$SPOUrl = $Data.WebUrl$SPODocLib = $null$SPOLastDateActivity = "Site archived"}# Get Member and Guest member counts[array]$Members = Get-MgGroupMember -GroupId $Group.Id -All -PageSize 500[array]$Members = $Members.AdditionalProperties[array]$GuestMembers = $Members | Where-Object {$_.userPrincipalName -like "*#EXT#*"}# Update list with group information$ReportLine = [PSCustomObject][Ordered]@{DisplayName = $Group.DisplayNameObjectId = $Group.IdManagedBy = $OwnerNamesGroupContact = $GroupOwnerEmailGroupMembers = $Members.countGuestMembers = $GuestMembers.countSharePointURL = $SPOUrlSharePointDocLib = $SPODocLibLastSPOActivity = $SPOLastDateActivityWhenCreated = Get-Date ($Group.createdDateTime) -format 'dd-MMM-yyyy HH:mm'WhenRenewed = Get-Date ($Group.renewedDateTime) -format 'dd-MMM-yyyy HH:mm'Visibility = $Group.visibilityDescription = $Group.DescriptionLabel = ($Group.assignedLabels.displayName -join ", ")}$GroupsList.Add($ReportLine)}Write-Host "Getting information about team-enabled groups..."# Get Teams[array]$Teams = Get-MgTeam -All -PageSize 999 | Sort-Object DisplayName$TeamsHash = @{}$Teams.ForEach( {$TeamsHash.Add($_.Id, $_.DisplayName) } )# All groups and teams found...$TeamsCount = $Teams.Count$GroupsCount = $GroupsList.CountIf (!$GroupsCount) {Write-Host "No Microsoft 365 Groups can be found - exiting"; break}If (!$TeamsCount) {Write-Host "No Microsoft Teams found in the tenant - continuing..." }$S2 = Get-Date # End of fetchingClear-HostWrite-Host ("Fetching data for {0} Microsoft 365 Groups took {1} seconds" -f $GroupsCount, (($S2 - $S1).TotalSeconds))# Set up progress bar and create output list$ProgDelta = 100/($GroupsCount); $CheckCount = 0; $GroupNumber = 0$Report = [System.Collections.Generic.List[Object]]::new()# Main loopForEach ($G in $GroupsList ) { #Because we fetched the list of groups with a Graph call, the first thing is to get the group properties$GroupNumber++$DisplayName = $G.DisplayName$SPOStatus = "SPO: OK"; $MailboxStatus = "Mbx: OK"; $TeamsStatus = "Teams: OK"$GroupStatus = $DisplayName + " ["+ $GroupNumber +"/" + $GroupsCount + "]"Write-Progress -Activity "Analyzing and reporting group" -Status $GroupStatus -PercentComplete $CheckCount$CheckCount += $ProgDelta; $ObsoleteReportLine = $DisplayName$NumberWarnings = 0; $NumberofChats = 0; $TeamsChatData = $Null; $TeamsEnabled = $False; [string]$LastItemAddedtoTeams = "N/A"; $ObsoleteReportLine = $Null# Group Age$GroupAge = (New-TimeSpan -Start $G.WhenCreated -End $Today).Days# Team-enabled or not?$GroupIsTeamEnabled = $FalseIf ($TeamsHash[$G.ObjectId]) {$GroupIsTeamEnabled = $True}If ($GroupIsTeamEnabled -eq $False) { # Not a Teams-enabled group, so look at the Inbox etc.# Fetch information about activity in the Inbox folder of the group mailboxTry {[array]$Data = (Get-ExoMailboxFolderStatistics -Identity $G.ObjectId -IncludeOldestAndNewestITems -FolderScope Inbox -ErrorAction SilentlyContinue)} Catch {Write-Host ("Can't read information from the group mailbox for {0} - continuing." -f $G.DisplayName)$Data = $Null}If ([string]::IsNullOrEmpty($Data.NewestItemReceivedDate)) {$LastConversation = "No items found"$NumberConversations = 0} Else {Try {$LastConversation = Get-Date ($Data.NewestItemReceivedDate) -Format 'dd-MMM-yyyy HH:mm'$NumberConversations = $Data.ItemsInFolder} Catch [System.Management.Automation.ParameterBindingException] {# Caused by the Exo cmdlets returning bad dates, so we need to do this...[string]$LastDate = $Data.NewestItemReceivedDate[0][datetime]$LastDateConversation = $LastDate$LastConversation = Get-Date($LastDateConversation) -format 'dd-MMM-yyyy HH:mm'[string]$NumberConversations = $Data.ItemsInfolder[0].toString()$Error.Clear()} Catch {Write-Host "Error converting date values"}}If ($G.resourceBehaviorOptions -eq "CalendarMemberReadOnly") {$LastConversation = "Yammer group"}If ($LastConversation -le $WarningEmailDate) {# Write-Host "Last conversation item created in" $G.DisplayName "was" $LastConversation "-> Obsolete?"$ObsoleteReportLine = ("Last Outlook conversation dated {0}." -f $LastConversation)$MailboxStatus = "Group Inbox Not Recently Used"$ObsoleteEmailGroups++$NumberWarnings++} Else { # Some conversations exist - but if there are fewer than 20, we should flag this...If ($NumberConversations -lt 20) {$ObsoleteReportLine = $ObsoleteReportLine + "Only " + $NumberConversations + " Outlook conversation item(s) found."$MailboxStatus = "Low number of conversations"$NumberWarnings++}}} Else { # It's a team-enabled group, so we don't need to check the mailbox and so populate the values appropriately$LastConversation = "Teams-enabled group"$NumberConversations = "N/A"}# Check for activity in the group's SharePoint site$SPOFileCount = 0; $SPOStorageUsed = "N/A"; $SPOLastActivityDate = $Null; $DaysSinceLastSPOActivity = "N/A"If ($Null -ne $G.SharePointURL) {If ($Datatable[$G.SharePointURL]) { # Look up hash table to find usage information for the site$ThisSiteData = $Datatable[$G.SharePointURL]$SPOFileCount = $ThisSiteData.FileCount$SPOStorageUsed = $ThisSiteData.StorageUsed$SPOLastActivityDate = $ThisSiteData.LastActivityDateIf ($Null -ne $SPOLastActivityDate) {$DaysSinceLastSPOActivity = (New-TimeSpan -Start $SPOLastActivityDate -End $Today).Days}} Else { # The SharePoint document library URL is blank, so the document library was never created for this group$ObsoleteSPOGroups++;$ObsoleteReportLine = $ObsoleteReportLine + " SharePoint document library never created."}}If ($DaysSinceLastSPOActivity -gt 90) { # No activity in more than 90 days$ObsoleteSPOGroups++; $ObsoleteReportLine = $ObsoleteReportLine + " No SPO activity detected in the last 90 days." }# Generate warnings for SPOIf ($Null -ne $G.SharePointDocLib) {$SPOStatus = "Document library never created"$NumberWarnings++ }# Write-Host "Processing" $G.DisplayName# If the group is team-enabled, find the date of the last Teams conversation compliance recordIf ($GroupIsTeamEnabled -eq $True) { # We have a team-enabled group$TeamsEnabled = $True; $NumberOfChats = 0; [string]$LastItemAddedToTeams = $NullIf (-not $TeamsUsageHash.ContainsKey($G.ObjectId)) { # Check do we have Teams usage data stored in a hash table# Nope, so we have to get the data from Exchange Online by looking in the TeamsMessagesData file in the non-IPM rootWrite-Host "Checking Exchange Online for Teams activity data..."Try {[array]$TeamsChatData = (Get-ExoMailboxFolderStatistics -Identity $G.ObjectId -IncludeOldestAndNewestItems -FolderScope NonIPMRoot -ErrorAction SilentlyContinue | `Where-Object {$_.FolderType -eq "TeamsMessagesData" })}Catch { # Report the errorWrite-Host ("Error fetching Team message data for {0} - continuing" -f $G.DisplayName)}If ($TeamsChatData.ItemsInFolder -gt 0) {[datetime]$LastItemAddedtoTeams = $TeamsChatData.NewestItemReceivedDate$LastItemAddedtoTeams = Get-Date ($LastItemAddedtoTeams) -Format 'dd-MMM-yyyy HH:mm'}$NumberOfChats = $TeamsChatData.ItemsInFolder} Else { # Read the data from the Teams usage data$ThisTeamData = $TeamsUsageHash[$G.ObjectId]$NumberOfChats = [int]$ThisTeamData.Posts + [int]$ThisTeamData.Replies[int]$DaysSinceTeamsActivity = 0; [string]$LastItemAddedToTeams = $nullIf (!([string]::IsNullOrWhitespace($ThisTeamData.LastActivity)) -and ($ThisTeamData.LastActivity -ne 'More than 90 days ago')) {[datetime]$LastItemAddedToTeams = [datetime]::ParseExact($ThisTeamData.LastActivity, "dd-MMM-yyyy", $null)# [datetime]$LastItemAddedToTeams = $ThisTeamData.LastActivity$LastItemAddedToTeams = Get-Date ($LastItemAddedtoTeams) -Format 'dd-MMM-yyyy'[int]$DaysSinceTeamsActivity = (New-TimeSpan $LastItemAddedtoTeams).Days}} #End Else} # End if# Increase warnings if Teams activity is lowIf ($NumberOfChats -lt 20) {$NumberWarnings++$TeamsStatus = "Low number of Teams conversations"}If ($DaysSinceTeamsActivity -gt 90) {$NumberWarnings++$TeamsStatus = ("{0} last item added {1} days ago" -f $TeamsStatus, $DaysSinceTeamsActivity)}# Discover if team is archivedIf ($TeamsEnabled -eq $True) {Try {$TeamDetails = Get-MgTeam -TeamId $G.ObjectId -Property IsArchived, Id, displayName} Catch {Out-Null}If ([string]::IsNullOrWhitespace($TeamDetails)) {Write-Host ("Error reading team details for {0} ({1})" -f $G.displayName, $G.ObjectId)Write-Host "Check the Teams admin center for this team - it might be deleted but still shows up in the teams list"} Else {Switch ($TeamDetails.IsArchived) {$False {$DisplayName = $G.DisplayName}$True {$DisplayName = $G.DisplayName + " (Archived team)"$ArchivedTeams++}}}}# End of Processing Teams data# Calculate status$Status = $MailboxStatus,$SpoStatus,$TeamsStatus -join ", "$OverallStatus = "Pass"If ($NumberWarnings -gt 1) { $OverallStatus = "Issues"}If ($NumberWarnings -gt 2) { $OverallStatus = "Fail" }# Generate a line for this group and store it in the report$ReportLine = [PSCustomObject][Ordered]@{GroupName = $DisplayNameManagedBy = $G.ManagedByContactEmail = $G.GroupContactVisibility = $G.VisibilityMembers = $G.GroupMembers"External Guests" = $G.GuestMembersDescription = $G.Description"Sensitivity Label" = $G.Label"Team Enabled" = $TeamsEnabled"Last Teams message" = $LastItemAddedtoTeams"Number Teams messages" = $NumberOfChats"Last Email Inbox" = $LastConversation"Number Email Inbox" = $NumberConversations"Last SPO Activity" = $SPOLastActivityDate"SPO Storage Used (GB)" = $SPOStorageUsed"Number SPO Files" = $SPOFileCount"SPO Site URL" = $G.SharePointURL"Date Created" = $G.WhenCreated"Days Old" = $GroupAgeNumberWarnings = $NumberWarningsStatus = $Status"Overall Result" = $OverallStatus}$Report.Add($ReportLine)$S3 = Get-Date$TotalSeconds = [math]::round(($S3-$S2).TotalSeconds,2)$SecondsPerGroup = [math]::round(($TotalSeconds/$GroupNumber),2)Write-Host "Processed" $GroupNumber "groups in" $TotalSeconds "- Currently processing at" $SecondsPerGroup "seconds per group"#End of main loop}$OverallElapsed = [math]::round(($S3-$S1).TotalSeconds,2)If ($TeamsCount -gt 0) { # We have some teams, so we can calculate a percentage of Team-enabled groups$PercentTeams = ($TeamsCount/$GroupsCount)$PercentTeams = ($PercentTeams).tostring("P") }Else {$PercentTeams = "No teams found" }# Create the HTML report$htmlbody = $Report | ConvertTo-Html -Fragment$htmltail = "<p>Report created for: " + $OrgName + "</p><p>Number of groups scanned: " + $GroupsCount + "</p>" +"<p>Number of potentially obsolete groups (based on document library activity): " + $ObsoleteSPOGroups + "</p>" +"<p>Number of potentially obsolete groups (based on conversation activity): " + $ObsoleteEmailGroups + "<p>"+"<p>Number of Teams-enabled groups : " + $TeamsCount + "</p>" +"<p>Percentage of Teams-enabled groups: " + $PercentTeams + "</body></html>" +"<p>-----------------------------------------------------------------------------------------------------------------------------"+"<p>Microsoft 365 Groups and Teams Activity Report <b>" + $Version + "</b>"# Generate the HTML report$HTMLReport = $HTMLHead + $HTMLBody + $HTMLTail$HTMLReportFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\Teams and Groups Activity Report.html"$HTMLReport | Out-File $HTMLReportFile -Encoding utf8# Generate the report in either Excel worksheet or CSV format, depending on if the ImportExcel module is availableIf (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $TrueImport-Module ImportExcel -ErrorAction SilentlyContinue$ExcelOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\Teams and Groups Activity Report.xlsx"$Report | Export-Excel -Path $ExcelOutputFile -WorksheetName "Teams and Groups activity" -Title ("Teams and Groups Activity Report {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "Microsoft365LicensingReport"} Else {$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\Teams and Groups Activity Report.CSV"$Report | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding Utf8}$Report | Out-GridView# Summary detailsClear-HostWrite-Host " "Write-Host ("Results - Teams and Microsoft 365 Groups Activity Report {0}" -f $Version)Write-Host "-------------------------------------------------------------"Write-Host ("Number of Microsoft 365 Groups scanned: {0}" -f $GroupsCount)Write-Host ("Potentially obsolete groups (based on document library activity: {0}" -f $ObsoleteSPOGroups)Write-Host ("Potentially obsolete groups (based on conversation activity): {0}" -f $ObsoleteEmailGroups)Write-Host ("Number of Teams-enabled groups: {0}" -f $TeamsCount)Write-Host ("Number of archived teams: {0}" -f $ArchivedTeams)Write-Host ("Percentage of Teams-enabled groups: {0}" -f $PercentTeams)Write-Host " "Write-Host "Total Elapsed time: " $OverAllElapsed "seconds"If ($ExcelGenerated -eq $true) {Write-Host ("Teams and Groups activity report available in HTML {0} and Excel workbook {1}" -f $HTMLReportFile, $ExcelOutputFile)} Else {Write-Host ("Teams and Groups activity report available in HTML {0} and CSV file {1}" -f $HTMLReportFile, $CSVOutputFile)}# Reverse report data obfuscation if it was setIf ($DataObfuscationOn -eq $True) {$Parameters = @{ displayConcealedNames = $True }Update-MgBetaAdminReportSetting -BodyParameter $Parameters}
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.-AppId""Application (client) ID for the app registration used to connect.-LookbackDays365Number of days of inactivity after which groups or teams may be flagged for deletion.Attribution
Author
Office365itpros