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:



(Get-Team).Count

Which should display a similar result: 

01_get-team


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:

Example

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
(Get-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. 

04_inputobject

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:


Get-CsTeamsGuestCallingConfiguration
Get-CsTeamsGuestMeetingConfiguration
Get-CsTeamsGuestMessagingConfiguration

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:

05_historical

 

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:

06_inputobject

 

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

 

Comparison:


# 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:

07_inputobject

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:


Month,AllowIPVideo,ScreenSharingMode,AllowMeetNow,LiveCaptionsEnabledType,AllowTranscription
07.2023,True,SingleApplication,True,DisabledUserOverride,False
08.2023,True,SingleApplication,False,DisabledUserOverride,False
09.2023,True,SingleApplication,False,DisabledUserOverride,False
10.2023,True,SingleApplication,True,DisabledUserOverride,False
11.2023,True,SingleApplication,True,DisabledUserOverride,False
12.2023,True,SingleApplication,False,DisabledUserOverride,False
01.2024,True,SingleApplication,True,DisabledUserOverride,False

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)
Connect-ExchangeOnline

# 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:

08_teamsessionstarted

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:


TeamsSessionStarted
MeetingParticipantDetail
MessageReadReceiptReceived
ReactedToMessage
MeetingDetail

 

Conclusion

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. 

 

 

Good2know

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.

 Teams-Cheat-Sheet2024

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: