Skip to the main content.

ScriptRunner Blog

Teams Auditing – How to avoid scope creep and Teams sprawl

Table of contents


Post Featured Image

Do you want to learn more about monitoring Microsoft Teams with PowerShell? Read how to check settings over time to check for changes. Prevent (or solve) configuration drift.    


Where to start

Scope creep, configuration creep and Teams sprawl all have a common root problem – lack of monitoring or controls to prevent these changes. In this article, we will discuss how we can use PowerShell to monitor Teams, their configurations and determine changes that have occurred. Due to the size and complexity of Teams, we will not cover each and every configuration aspect but set administrators of for success by walking through some examples of how to use PowerShell. An inspiration for this is situated in the Security center known as the Configuration Analyzer which has a Drift Analysis and History tab.  


Starting point

As with any PowerShell script or tool usage, make sure that your Microsoft Teams PowerShell module is running the latest version: 

Update-Module MicrosoftTeams

Then close your PowerShell session as this ensures the next time the PowerShell module is loaded, it will be the latest version.

Validate your version is the latest:

Import-Module MicrosoftTeams
Get-Module MicrosoftTeams

Again, compare the version number again what is available (here).


What cmdlets to use

Building a custom script to audit a Microsoft Teams configuration can be done purely with Get-* cmdlets as these are discovery PowerShell cmdlets, querying settings and objects and providing needed details. A quick list of these can be found with this one-liner (once connected to Microsoft Teams PowerShell endpoint):

Get-Command Get-* |
Where Source -eq MicrosoftTeams |
Sort Name |
ft Name

We won't review each and every one of the cmdlets available as there are over 150 of them, but we will review some select examples to assist in discovering important items in Teams for an 'audit' of settings.


Microsoft Teams and Teams channels

List of teams

Keeping track of the number and types of teams is an important aspect of auditing a Teams configuration as Teams can sprawl beyond what an organization intended to create or maintain. By validating team counts we can see the growth and usage before the numbers get out of hand. First, querying the number of teams:


Which should display a similar result: 


Get-Team |
ft DisplayName,Visibility,Archived,GroupId

02_get-team results


List of team channels

Each team could have one or more channels to keep track of:

Get-Team | 
ForEach-Object {Write-Host $_.DisplayName -ForegroundColor Yellow ;Get-TeamChannel -GroupID $_.GroupID |
ft DisplayName,Membership*,Description}


03_list of team channels-1

Using the previous PowerShell cmdlets above, we were able to gather a lot of data and settings about our Teams environment. How do we use this data to truly audit an environment? For one, we can monitor Teams and Teams Channel sprawl. In the below example, we check Teams and Teams channels every month to see what has changed:


When the number of Teams and Channels is first checked there may be 35 teams and 56 channels and then when the cmdlets are run again, perhaps a month later, these numbers may increase to 40 and 65. We can pull the initial counts like so:

# Team count

# Teams channel count
Get-Team |
Foreach-Object {$Channels = (Get-TeamChannel -GroupID $_.GroupID).Count;$TeamsChannelCount=$TotalChannels+$Channels}

Later, on a scheduled basis, we can programmatically check these numbers again: 

$TeamsCount = (Get-Team).Count
Get-Team |
Foreach-Object {$Channels = (Get-TeamChannel -GroupID $_.GroupID).Count;$TeamsChannelCount=$TotalChannels+$Channels}


Report real world example

When we want to start auditing the number of Teams and Teams Channels, we need a way to store counts and compare these counts against the current numbers in Teams.

First Run:

# Date column
$CurrentMonth = Get-Date -Format "MM.yyyy"

# File header
"Date,TeamsCount,TeamsChannelCount" |
Out-File 'TeamsAndChannelCounts.csv'

# First month counts
$TeamsCount = (Get-Team).Count
Get-Team |
Foreach-Object {$Channels = (Get-TeamChannel -GroupID $_.GroupID).Count;$TeamsChannelCount=$TotalChannels+$Channels}
$Output = "$CurrentMonth,$TeamsCount,$TeamsChannelCount" |
Out-File 'TeamsAndChannelCounts.csv' -Append


Scheduled runs where we have data lines which can be examined for differences:

# Import data
$TeamsData = Import-Csv .\TeamsAndChannelCounts.csv

# Last line
$CurrentMonth = $TeamsData[-1]

# Next to last line to compare
$LastMonth = $TeamsData[-2]

# Check each Teams data point for change
If ($CurrentMonth.TeamsCount -gt $LastMonth.TeamsCount) {Write-Host 'There are new Teams present.'}
If ($CurrentMonth.TeamsChannelCount -gt $LastMonth.TeamsChannelCount) {Write-Host 'There are new Teams Channels present.'}


If we need to dig into the weeds a bit, we can also check which teams are new since the last time as well. In order to do that, we can run this the first time:

Get-Team | 
select DisplayName,Visibility,GroupId,Archived,Description |
Export-Csv -Path HistoricalTeams.csv

Each month we can check for changes:

Get-Team | 
select DisplayName,Visibility,GroupId,Archived,Description |
Export-Csv -Path CurrentTeams.csv

Then we can compare these two files with Compare-Object:

$Historical = Get-Content .\HistoricalTeams.csv
$Current = Get-Content .\CurrentTeams.csv
Compare-Object -ReferenceObject $Historical -DifferenceObject $Current


We see in this example that there are two different groups, which are new as the SideIndicator value is pointing to the file data in the $Current variable. 


One more step we can perform with PowerShell and that to replace the Historical Teams files with the current one so that we have the latest counts to compare at all times:

Remote-Item HistoricalTeams.csv
Rename-Item CurrentTeams.csv HistoricalTeams.csv


Guest access

Within teams there are controls for what your Guest has access to, and we can control these settings with PowerShell. Monitoring Teams configuration aspects for Guest users is an import aspect of security for your Teams configuration in general. As such we can explore this with PowerShell to see what we can discover and monitor.

First the PowerShell cmdlets:

Get-Command Get-*team*guest*

Which provides these cmdlets:


Each one of these will reveal a different aspect of the configuration. Using PowerShell, we can document and then compare the settings for changes, just like we did with Teams and Channels.


Guest calling configuration

First, let's break down the basic Guest Calling Configuration:

$CurrentMonth = Get-Date -Format "MM.yyyy"
$GuestCallingCfg = (Get-CsTeamsGuestCallingConfiguration).AllowPrivateCalling
$FileHeader = "Date,AllowPrivateCalling" |
Out-File 'GuestCallingConfiguration.csv'
$Output = "$CurrentMonth,$GuestCallingCfg" |
Out-File 'GuestCallingConfiguration.csv' -append


There is only one value of importance here. We can verify this has no changes over time and we would have to run this monthly, quarterly or so on:

$CurrentMonth = Get-Date -Format "MM.yyyy"
$GuestCallingCfg = (Get-CsTeamsGuestCallingConfiguration).AllowPrivateCalling
$FileHeader = "Date,AllowPrivateCalling" |
Out-File 'GuestCallingConfiguration.csv'
$Output = "$CurrentMonth,$GuestCallingCfg" |
Out-File 'GuestCallingConfiguration-current.csv' -append


Then we can compare the two output files like so:

$Historical = Import-Csv GuestCallingConfiguration.csv
$Current = Import-Csv GuestCallingConfiguration-current.csv
$Historical.AllowPrivateCalling -eq $Current.AllowPrivateCalling


If a change occurs, the result would look like so:



Guest meeting configuration

Next up, we have the Guest Meeting Configuration which contains a few more settings to manage. First, we need to create a baseline or discovery what we have configured today: 

First Run:

# Discover current configuration 
$GuestMeeting = Get-CsTeamsGuestMeetingConfiguration |
Select AllowIPVideo,ScreenSharingMode,AllowMeetNow,LiveCaptionsEnabledType,AllowTranscription

# Create a Hash table output to be exported
$Hashtable = New-Object System.Collections.Hashtable
$Hashtable['AllowIPVideo'] = $GuestMeeting.AllowIPVideo
$Hashtable['ScreenSharingMode'] = $GuestMeeting.ScreenSharingMode
$Hashtable['AllowMeetNow'] = $GuestMeeting.AllowMeetNow
$Hashtable['LiveCaptionsEnabledType'] = $GuestMeeting.LiveCaptionsEnabledType
$Hashtable['AllowTranscription'] = $GuestMeeting.AllowTranscription

# Export $Hashtable variable to a CSV for comparison
$HashTable.GetEnumerator() |  
Select-Object -Property Key,Value |  
Export-Csv -NoTypeInformation -Path GuestMeeting.csv


Then, over time, we can make subsequent Runs [Monthly, Quarterly, Yearly] which will use the same code above, but change the output file to 'GuestMeeting-Current.csv':  

$HashTable.GetEnumerator() |  
Select-Object -Property Key,Value |  
Export-Csv -NoTypeInformation -Path GuestMeeting-Current.csv


Comparing the two configurations:

# Compare the original and current CSV files
$CSV1 = Get-Content .\GuestMeeting.csv
$CSV2 = Get-Content .\GuestMeeting-Current.csv
Compare-Object $CSV1 $CSV2


If a difference is detected, we should see something like this:



Guest messaging configuration

The same process can also be performed with the Guest Messaging Configuration. For our first run, we can use this code:

# Discover current configuration 
$GuestMessaging = Get-CsTeamsGuestMessagingConfiguration |
Select AllowUserEditMessage,AllowUserDeleteMessage,AllowUserDeleteChat,AllowUserChat,AllowGiphy,GiphyRatingType,AllowMemes,AllowImmersiveReader,AllowStickers

# Create a Hash table output to be exported
$Hashtable = New-Object System.Collections.Hashtable
$Hashtable['AllowUserEditMessage'] = $GuestMessaging.AllowUserEditMessage
$Hashtable['AllowUserDeleteMessage'] = $GuestMessaging.AllowUserDeleteMessage
$Hashtable['AllowUserDeleteChat'] = $GuestMessaging.AllowUserDeleteChat
$Hashtable['AllowUserChat'] = $GuestMessaging.AllowUserChat
$Hashtable['AllowGiphy'] = $GuestMessaging.AllowGiphy
$Hashtable['GiphyRatingType'] = $GuestMessaging.GiphyRatingType
$Hashtable['AllowMemes'] = $GuestMessaging.AllowMemes
$Hashtable['AllowImmersiveReader'] = $GuestMessaging.AllowImmersiveReader
$Hashtable['AllowStickers'] = $GuestMessaging.AllowStickers

# Export $Hashtable variable to a CSV for comparison
$HashTable.GetEnumerator() |  
Select-Object -Property Key,Value |  
Export-Csv -NoTypeInformation -Path GuestMessaging.csv


Subsequent Runs: [Monthy, Quarterly, Yearly]

Use the same code above, but change the output file to 'GuestMessaging-Current.csv':

$HashTable.GetEnumerator() |  
Select-Object -Property Key,Value |  
Export-Csv -NoTypeInformation -Path GuestMessaging-Current.csv



# Compare the original and current CSV files 
$CSV1 = Get-Content .\GuestMessaging.csv
$CSV2 = Get-Content .\GuestMessaging-Current.csv
Compare-Object $CSV1 $CSV2


If a difference is detected, we should see something like this:


Note in the above comparison, two values have changed: AllowMemes and AllowStickers, and by the indicators (==>), the new configuration is now 'True' for both settings whereas the original configuration (<==) had both set to 'False'.


Comments so far

With any auditing process, organizations need to decide what is important to keep an eye on and this is true of Teams Configurations. While we could cover all settings in this blog article, that is not the intent and instead we've provided the tools you need to build your own audit process. A complete coverage of all settings would make this article too long and would mostly be a repetitive process of covering the settings, an entirely unnecessary process.


Additional tasks – historical data

If there is a need to track the configuration over time, we could also create historical charts for review. The way to do this is to export the current settings and record a date/timestamp for when this data set was true. A sample file could look something like this:


Depending on an organizations internal process, this could be placed on a central IT share, SharePoint, or perhaps emails to a Distribution List for awareness. 


Teams action auditing

A common task administrators perform is task auditing, which involves looking at logs and other data to determine a root cause for an issue and fix the issue. Additionally, auditing can also be used to query an activity that Microsoft Teams performs. This may assist in either determining general activity levels, or possibly when a particular service was used. In the background, Microsoft utilizes a Unified Log that stores information on actions taken on the Teams platform and there are two ways to retrieve this information: PowerShell and the Microsoft Defender interface.

Why do this in PowerShell? PowerShell provides flexibility that you may not get in the GUI. We can schedule this within either a Jumpbox that runs PowerShell scripts on-premises, or we can do this in an Azure Runbook to minimize assets on premises. Or we can do this within ScriptRunner as part of a set of management tasks.

# First, connect to Exchange Online PowerShell (v3)

# Retrieve Teams Events - stored in the Unified Audit Log
# Timeframe: Past 48 hours
$EndDate = Get-Date
$StartDate = $EndDate.AddDays(-2)

# All Teams events
Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -RecordType MicrosoftTeams

# Teams channels added
Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -RecordType MicrosoftTeams -Operations ChannelAdded

# Sensitivity Label changes
Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -RecordType MicrosoftTeams -Operations SensitivityLabelChanged

# Teams deleted
Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -RecordType MicrosoftTeams -Operations TeamDeleted

For reference (Microsoft Learn pages): Teams activities

When we run these cmdlets, we should see output like so:


Note that this is only one of many entries and happens to be a logged start of a Teams session.

Beyond these operations, we also have additional Teams operations we call pull data for:




PowerShell provides administrators with the capability of monitoring their own environments when other tools do not exist. In this case, we were able to use PowerShell to check settings over time to check for changes, making sure there is no configuration drift, or if there is, being able to take a rough date stamp to see when the change was made. With the proper coding, all configuration aspects of Microsoft Teams can be monitored over time with PowerShell which allows administrators to concentrate on other tasks. 




Microsoft Teams cheat sheet

Simplify and automate your Microsoft Teams management with our 8-page PowerShell cheat sheet for Microsoft Teams. This handy guide provides you with quick-reference cmdlets and code snippets to manage users, channels, policies and more with ease. Ideal for both beginners and seasoned administrators.


Your ultimate PowerShell Cheat Sheet

Unleash the full potential of PowerShell with our handy poster. Whether you're a beginner or a seasoned pro, this cheat sheet is designed to be your go-to resource for the most important and commonly used cmdlets.

The poster is available for download and in paper form.



Get your PowerShell goodies here!



Related links

Related posts

12 min read

Teams Auditing – How to avoid scope creep and Teams sprawl

10 min read

My top 10 ways to use Teams PowerShell

6 min read

Teams-Webhooks via PowerShell – Modern Alerting (part 3/3)

About the author: