Managing Abandoned M365 Groups

icp
Also, magnets

If you’re using Azure, Teams, SharePoint, Yammer, etc. you’ve probably noticed M365 groups magically appearing in your tenant. They seem to pop up whenever you do anything relating to Microsoft’s Cloud and it’s even harder to go back and figure out what they are used for. In addition, as people come and go from an organization these groups can easily become abandoned with no owner. I’m going to hopefully help you solve the following:

  • What is this M365 Group used for?
  • Find abandoned groups with no owner
  • Find Groups with a single owner, but that owner’s account is disabled (aka abandoned)

First, you need to get connected to Exchange Online. I agree, it’s a bit odd that the management of M365 groups is governed by the Exchange Online PowerShell module but what can you do.

Connect-ExchangeOnline -ShowBanner:$false
we’re in

We’re going to want the rest of this script saved within an array so we can output the results easily. To do that, we start with this line

$Output = @(

You now need to pull all the M365 groups. Depending on the size of your tenant, this might take some time.

$groups = Get-UnifiedGroup -SortBy Name -ResultSize Unlimited

Once you’ve gathered all the M365 groups, it’s time to parse through them to find the owners and see what they are for. We start with one of these bad boys:

ForEach($Group in $Groups){

Now that that’s done, let’s grab the owners of the M365 Group. Be sure to store this as an array by putting it inside @(…). If you don’t do this, and it only returns one result, PowerShell’s ability to effectively “count” won’t work.

$Owners = @($Group | Select-Object -ExpandProperty ManagedBy)

Once you have the owners, let’s see how many there are for the group. If there’s less than 2, let’s get some more information about that user in AD. This can also take a good chunk of time depending on how many groups the first command returned.

Pay special attention to how I’ve written the filter here. Get-ADUser is a bit funny about using variables within the filter portion of the command, and this is the only way I could get it cooperating correctly. The $Owners variable at this point can contain spaces so it needs to be enclosed in quotes, and those quotes need to be preceded by a tick `. Single quotes will not work here, just copy/paste what I have below.

There’s also some nested IF statements, which I realize is a bit hard to read when it’s not tabbed correctly. Thanks WordPress.

If($Owners.count -lt 2){
If($Owners.count -eq 1){$User = Get-ADUser -Filter "Name -eq `"$Owners`"" -Properties Mail}
Else{$User = $null}

You’ll also notice I didn’t close the first IF statement. That’s because the next section also needs to be within there. What we’re doing with the above code is filtering out groups with 2 or more owners, as the whole point of this is to find groups that are likely to be / are already abandoned.

If you wanted to include all the groups in the output instead of just the ones with a single / zero owners, you’ll need to modify the script and remove the portions about $Owners and $User. I’m not going to get into the details here though as that’s not the focus of the post.

Ok now it’s time to figure out what the groups with one or less owners are actually used for. I look at the ResourceProvisioningOptions attribute to determine if the group is for Teams or Stream. I then also look at the GroupSKU attribute to determine if it’s used for Yammer.

Because a group can be used for more than a single service, assigning a true/false value to a variable for each service type will give us the best output.

$ResourceProvisioning = ($Group | Select-Object -ExpandProperty ResourceProvisioningOptions) -join ","
If($ResourceProvisioning -like "team"){$team = $true}Else{$team = $false}
If($ResourceProvisioning -like "stream"){$stream = $true}Else{$stream = $false}
If($Group.groupsku -eq "Yammer"){$yammer = $true}Else{$yammer = $false}

At this point, you’ve gathered everything you need to generate a detailed report about M365 Groups with one or less owners and what they are used for.

Let’s build the custom object. I always call it $obj because that’s how I learned how to do it.

$obj = [PSCustomObject]@{
     GroupDisplayName = $Group.DisplayName
     GroupAlias = $Group.Alias
     GroupName = $Group.Name
     GroupNotes = $Group.Notes
     GroupAccessType = $Group.AccessType
     GroupCreationTime = $Group.WhenCreated
     GroupExpirationTime = $Group.ExpirationTime
     Teams = $team
     Stream = $stream
     Yammer = $yammer
     GroupSharePointSiteUrl = $Group.SharePointSiteUrl
     GroupMemberCount = $Group.GroupMemberCount
     OwnerEmail = $user.EmailAddress
     OwnerUPN = $user.UserPrincipalName
     OwnerSAM = $user.SamAccountName
     OwnerEnabled = $user.enabled
}

Alright now let’s output $obj object to the $output array, then close the IF statement, the ForEach Loop, and the $output array

$obj
          } #closes the IF
     } #closes the ForEach
) #closes the array

Now all you need to do is output the results to a CSV. I usually append a date to the name of the CSV so I can have multiple iterations of the report. I’m outputting to the desktop but you can obviously adjust this as you need to

$date = Get-Date -Format yyyyMMdd
$Output | Export-Csv -Path "$env:USERPROFILE\Desktop\GroupsWithOneOrZeroOwners - $date.csv" -NoTypeInformation -Force

What can you determine from the CSV file? Well obviously you can tell which groups have one or less owners, and you can tell if they are used for Teams, Stream or Yammer. If the group isn’t used for Teams, Stream or Yammer but still has a SharePointSiteURL, then it’s probably just a SharePoint site. If it doesn’t have a SharePoint URL, it’s possibly being used as a “distribution group”.

Here’s an example of the output. The fields are pretty self explanatory.

hard to read CSV file

You’ve got when the group was created, when it’s scheduled for deletion (if you have Group Expiration enabled in your tenant), you have the services it determined the group is used for, how many members are in the group (maybe if it’s zero you can delete it), and finally you have the information about the owner of the group.

Also, I just made up the URLs in the examples (obviously) so they won’t all say /teams/ in a real result. Probably should have changed that but you’re an adult. You’ll figure it out.

This group is associated with a Teams site. It has a single owner and 5 members.
Here is a Yammer group that has 10 members but no owner
This is likely a SharePoint site with an owner that is Disabled in Active Directory
This is probably being used as a distribution group since everything is false and there’s no SharePoint URL

Last but not least, here’s the whole script in one go. Just remember if you have a large footprint in the cloud this can take a bit of time to process.

Connect-ExchangeOnline -ShowBanner:$false

$Output = @(
    $groups = Get-UnifiedGroup -SortBy Name -ResultSize Unlimited

    ForEach($Group in $Groups){
        $Owners = @($Group | Select-Object -ExpandProperty ManagedBy)
        If($Owners.count -lt 2){

            If($Owners.count -eq 1){$User = Get-ADUser -Filter "Name -eq `"$Owners`"" -Properties Mail}
            Else{$User = $null}

            $ResourceProvisioning = ($Group | Select-Object -ExpandProperty ResourceProvisioningOptions) -join ","
            If($ResourceProvisioning -like "team"){$team = $true}Else{$team = $false}
            If($ResourceProvisioning -like "stream"){$stream = $true}Else{$stream = $false}
            If($Group.groupsku -eq "Yammer"){$yammer = $true}Else{$yammer = $false}

            $obj = [PSCustomObject]@{
                GroupDisplayName = $Group.DisplayName
                GroupAlias = $Group.Alias
                GroupName = $Group.Name
                GroupNotes = $Group.Notes
                GroupAccessType = $Group.AccessType
                GroupCreationTime = $Group.WhenCreated
                GroupExpirationTime = $Group.ExpirationTime
                Teams = $team
                Stream = $stream
                Yammer = $yammer
                GroupSharePointSiteUrl = $Group.SharePointSiteUrl
                GroupMemberCount = $Group.GroupMemberCount
                OwnerEmail = $user.EmailAddress
                OwnerUPN = $user.UserPrincipalName
                OwnerSAM = $user.SamAccountName
                OwnerEnabled = $user.enabled
            }
            $obj
        }
    }
)

$date = Get-Date -Format yyyyMMdd
$Output | Export-Csv -Path "$env:USERPROFILE\Desktop\GroupsWithOneOrZeroOwners - $date.csv" -NoTypeInformation -Force

Leave a Reply

Your email address will not be published. Required fields are marked *