Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extends 'spo list get' command with support for retrieving any default list in a given site #6445

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b150ee4
Add new sample to find obsolete m365 groups in the tenant. Closes #2475
Aug 14, 2024
c3c96bb
add preview image to assets
Aug 14, 2024
9643937
fix typos and change several minor implementations due to readabiltiy
Aug 27, 2024
793b05b
change implementation after rewiew
Sep 9, 2024
b2ae47f
Merge branch 'pnp:main' into main
tmaestrini Sep 18, 2024
34bdeaa
change console output to make the output path more precise for the user
Sep 22, 2024
bf41310
change retrieval of owners and members to increase script performance
Sep 22, 2024
c8ca5af
fix error related to comparison in case that group is empty
Sep 25, 2024
2b723ff
fix handling of groups with 0 or only 1 owner
Sep 25, 2024
bc31a5b
change logging behavior (reverted) in order to avoid overflooding the…
Sep 25, 2024
0c3779d
change member retrieval by excluding guests from the members' list
Sep 25, 2024
1aadf57
change retrieval of groups and procedure to check whether the group i…
Sep 25, 2024
e43a560
add additional information to exported object (group conversation infos)
Sep 25, 2024
310bd82
change sample preview image
Sep 25, 2024
692a8ad
fix formatting
Sep 25, 2024
988904f
Merge branch 'pnp:main' into main
tmaestrini Sep 28, 2024
329ef17
Merge branch 'pnp:main' into main
tmaestrini Oct 12, 2024
9bbfee1
remove option set dependency to reduce mandatory options
Oct 20, 2024
6a5ec86
change description
Oct 20, 2024
e7d8518
change option selectors to retrieve default document library
Oct 20, 2024
01e32f2
add test
Oct 20, 2024
e3b6341
Merge branch 'pnp:main' into 5856-retrieve-default-list-in-site
tmaestrini Oct 22, 2024
143c299
change email address
Oct 22, 2024
a59350d
fix test definition to cover new implementation to retrieve the site'…
Oct 22, 2024
2f640fb
Fixes casing and autocomplete for enums based on zod
waldekmastykarz Sep 21, 2024
b06b283
Updates docs contribution guide with Zod. Closes #6322
waldekmastykarz Oct 6, 2024
b8e931a
Removes obsolete example. Closes #6272
Oct 6, 2024
d6ab197
Fixes 'm365 setup' app registration to use 'CLI for M365'. Closes #6367
Jwaegebaert Oct 8, 2024
46e40b6
Removes aad options and aliasses. Closes #5823, #5676
nanddeepn Jul 12, 2024
a667411
Updates release notes and upgrade guidance
Adam-it Oct 13, 2024
f283f19
Fixes 'spo site sharingpermission set' example. Closes #6397
milanholemans Sep 28, 2024
5fbec54
Enhances 'spo folder move' with new endpoint. Closes #6154
milanholemans Sep 26, 2024
7c07bbf
Enhance `entra m365group user set` command. Closes #6224
nicodecleyre Aug 26, 2024
55ec884
Updates release notes
MathijsVerbeeck Oct 16, 2024
47ed12a
Enhances 'entra m365group user remove' command. Closes #6058
MathijsVerbeeck Sep 16, 2024
bd7309b
Updates release notes
Adam-it Oct 16, 2024
3073bff
Enhances teams util. Closes #5901
MathijsVerbeeck Sep 20, 2024
603ad9e
Adds command 'spo tenant site membership list'. Closes #5980
mkm17 Jun 9, 2024
fc660ae
Updates dependencies
waldekmastykarz Oct 8, 2024
db579ab
Adds command 'spp model list'. Closes #6103
mkm17 Jul 22, 2024
d6dcd68
Enhances 'spo list remove' with recycle option. Closes #6270
ktskumar Sep 22, 2024
cff20c1
Removes duplicate drive util. Closes #6393
Saurabh7019 Oct 11, 2024
ca0430e
Adds bypassSharedLock option to 'spo file remove' command. Closes #6313
Saurabh7019 Oct 14, 2024
878ed5f
Updates docs dependencies
Jwaegebaert Oct 15, 2024
4ca7c62
Updates release notes
milanholemans Oct 17, 2024
42d0be7
change email address
Oct 22, 2024
f0afacf
fix test definition to cover new implementation to retrieve the site'…
Oct 22, 2024
f0b7f30
Merge branch '5856-retrieve-default-list-in-site' of https://github.c…
Oct 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
[
{
"name": "pnp-find-obsolete-m365-groups",
"source": "pnp",
"title": "Finding Obsolete Microsoft 365 Groups with PowerShell",
"url": "https://pnp.github.io/cli-microsoft365/sample-scripts/entra/find-obsolete-m365-groups",
"creationDateTime": "2024-08-14",
"updateDateTime": "2024-08-14",
"shortDescription": "Understand to what extent the Microsoft 365 groups in your tenant are being used or even not.",
"longDescription": [
"Like any resource within your Microsoft 365 tenant, M365 Groups can become unused over time. This routine uses PowerShell with CLI for Microsoft 365 to create a report of all M365 groups that are possibly obsolete."
],
"products": ["SharePoint", "M365 Groups", "Teams", "Exchange Online"],
"categories": [],
"tags": [
"provisioning",
"libraries",
"group mailbox",
"governance",
"m365 groups",
"teams",
"usage",
"insights"
],
"metadata": [
{
"key": "CLI-FOR-MICROSOFT365",
"value": "v8.0.0"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://raw.githubusercontent.com/pnp/cli-microsoft365/main/docs/docs/sample-scripts/find-obsolete-m365-groups/assets/preview.png",
"alt": "preview image for the sample"
}
],
"authors": [
{
"gitHubAccount": "tmaestrini",
"pictureUrl": "https://avatars.githubusercontent.com/u/69770609?v=4",
"name": "Tobias Maestrini"
}
],
"references": [
{
"name": "Want to learn more about CLI for Microsoft 365 and the commands",
"description": "Check out the CLI for Microsoft 365 site to get started and for the reference to the commands.",
"url": "https://aka.ms/cli-m365"
},
{
"name": "Original article by Tony Redmond",
"description": "Check out the original article on which this script is based.",
"url": "https://petri.com/identifying-obsolete-office-365-groups-powershell"
}
]
}
]
309 changes: 309 additions & 0 deletions docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
---
tags:
- provisioning
- libraries
- group mailbox
- governance
- teams
- m365 groups
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Finding obsolete Microsoft 365 groups with PowerShell

Author: [Tobias Maestrini](https://github.com/tmaestrini)

This script is based on the [original article](https://petri.com/identifying-obsolete-office-365-groups-powershell) written by [Tony Redmond](https://twitter.com/12Knocksinna).

Like any resource within your Microsoft 365 tenant, M365 Groups can become unused over time.

This routine uses PowerShell with CLI for Microsoft 365
- To gather insights about SharePoint file activity within the related SharePoint site.
- To do a check against conversation items in the group mailbox.
- To denote the amount of active people (group owners, members and guests) in the group.

These metrics can help us understand the extent to which the resource is being used from a governance perspective – or even not.
Use this script to create a report of all M365 groups that are possibly obsolete.

<Tabs>
<TabItem value="PowerShell">

```powershell
$ErrorActionPreference = "Stop"

class GroupInfo {
[PSCustomObject] $Reference
[PSCustomObject] $Membership
[PSCustomObject] $SharePointStatus
[PSCustomObject] $MailboxStatus
[PSCustomObject] $ChatStatus
[string] $TestStatus
[string[]] $Reasons
}

function Start-Routine {
# START ROUTINE
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)] [Switch] $KeepConnectionsAlive,
[Parameter(Mandatory = $false)] [Switch] $KeepOutputPath
)

try {
Initialize-Params
if ($KeepOutputPath.IsPresent) { Initialize-ExportPath -KeepOutputPath }
else { Initialize-ExportPath }
Get-AllM365Groups
Get-AllGuestUsers
Get-AllTeamSites
Start-GroupInsightsTests

Write-Host "`n✔︎ Routine terminated" -ForegroundColor Green
if (!$KeepConnectionsAlive.IsPresent) {
m365 logout
}
}
catch {
Write-Error $_.Exception.Message
}
}

function Initialize-Params {
Write-Host "🚀 Generating report of obsolete M365 groups within your organization"

# define globals
$Global:Path
$Script:ReportPath = $null
$Script:Groups = @()
$Script:Guests = @()
$Script:TeamSites = @()
$Global:ObsoleteGroups = [System.Collections.Generic.Dictionary[string, GroupInfo]]::new()

Write-Output "Connecting to M365 tenant: please follow the instructions."
Write-output "IMPORTANT: You'll need to have at least global reader permissions!`n"
if ((m365 status --output text) -eq "Logged out") {
m365 login
}
}

function Initialize-ExportPath {
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)] [Switch] $KeepOutputPath
)

if (!$KeepOutputPath.IsPresent -or $null -eq $Global:Path) {
$Script:Path = Read-Host "Set the path to the folder where you want to export the report data as csv file"
}

$TestPath = Test-Path -Path $Script:Path
$tStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
if ($TestPath -ne $true) {
New-Item -ItemType directory -Path $Script:Path | Out-Null
Write-Host "Will create file in $($Script:Path): M365GroupsReport-$tStamp.csv" -ForegroundColor Yellow
}
else {
Write-Host "Following report file will be created in $($Script:Path): 'M365GroupsReport-$($tStamp).csv'."
Write-Host "`nAll data will be exported to $($Script:Path): M365GroupsReport-$($tStamp).csv." -ForegroundColor Blue
Write-Host "Do not edit this file during the scan." -ForegroundColor Blue
}
$Script:ReportPath = "$($Script:Path)/M365GroupsReport-$($tStamp).csv"
}

function Get-AllM365Groups {
$groups = m365 entra m365group list --includeSiteUrl | ConvertFrom-Json
$Script:Groups = $groups | Where-Object { $null -ne $_.siteUrl }
}

function Get-AllGuestUsers {
$Script:Guests = m365 entra user list --type Guest | ConvertFrom-Json
}

function Get-AllTeamSites {
$Script:TeamSites = m365 spo site list --type TeamSite | ConvertFrom-Json
}

function Start-GroupInsightsTests {
Write-Host "Checking $($Script:Groups.Count) groups for activity"

$Script:Groups | ForEach-Object {
$groupInfo = [GroupInfo]::new()
$groupInfo.Reference = $_
$groupInfo.Membership = @{Owners = 0; Members = 0; Guests = 0 }
$groupInfo.TestStatus = "🟢 OK"

Write-Host "☀︎ $($groupInfo.Reference.displayName)"

# Tests
Test-GroupMembership -Group $groupInfo
Test-SharePointActivity -Group $groupInfo
Test-ConversationActivity -Group $groupInfo

# Report
New-Report -Group $groupInfo
}

#Give feedback to user
Write-Host "`n-------------------------------------------------------------------"
Write-Host "`SUMMARY" -ForegroundColor DarkGreen
Write-Host "`-------------------------------------------------------------------"
Write-Host "`n👉 Found $($Global:ObsoleteGroups.Count) group$($Global:ObsoleteGroups.Count -gt 1 ? 's' : '') with possibly low activity."
Write-Host "` Please review the report: " -NoNewline
Write-Host "$($Script:ReportPath)" -ForegroundColor DarkBlue
}

function Test-GroupMembership {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)] [GroupInfo] $Group
)

# Original lists
$users = m365 entra m365group user list --groupId $Group.Reference.id | ConvertFrom-Json
$owners = $users | Where-Object { $_.roles -contains "Owner" }
$members = $users | Where-Object { $_.roles -contains "Member" -and $_.id -notin $Script:Guests.id }
$guests = $users | Where-Object { $_.id -in $Script:Guests.id }

# Modify the $members list to only contain users that are not in the $owners list
if($null -ne $owners -and $null -ne $members) {
$members = Compare-Object $members $owners -PassThru
}

$Group.Membership = [ordered] @{
Owners = $owners
Members = $members
Guests = $guests
}

if ($owners.Count -eq 0) {
Write-Host " → potentially obsolete (abandoned group: no owner)" -ForegroundColor Yellow
$reason = "Low user count"

$Group.Membership.Status = "Abandoned ($reason)"
$Group.TestStatus = "🟡 Warning"
$Group.Reasons += $reason

try {
$Global:ObsoleteGroups.Add($Group.Reference.id, $Group)
}
catch { }
return
}

if ($owners.Count -le 1 -and ($members.Count + $guests.Count) -eq 0) {
Write-Host " → potentially obsolete (abandoned group: only $($owners.Count) owner left)" -ForegroundColor Yellow
$reason = "Low user count"

$Group.Membership.Status = "Abandoned ($reason)"
$Group.TestStatus = "🟡 Warning"
$Group.Reasons += $reason

try {
$Global:ObsoleteGroups.Add($Group.Reference.id, $Group)
}
catch { }
}
}

function Test-SharePointActivity {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)] [GroupInfo] $Group
)

$WarningDate = (Get-Date).AddDays(-90)

$spoSite = $Script:TeamSites | Where-Object { $_.GroupId -eq "/Guid($($Group.Reference.id))/" }
$spoWeb = m365 spo web get --url $spoSite.Url | ConvertFrom-Json
if ($spoWeb.LastItemUserModifiedDate -lt $WarningDate) {
Write-Host " → potentially obsolete (SPO last content modified: $($spoWeb.LastItemUserModifiedDate))" -ForegroundColor Yellow
$reason = "Low SharePoint activity ($($spoWeb.LastItemUserModifiedDate))"

$Group.SharePointStatus = @{
Reason = $reason
}
$Group.TestStatus = "🟡 Warning"
$Group.Reasons += $reason

try {
$Global:ObsoleteGroups.Add($Group.Reference.id, $Group)
}
catch { }
}
}

function Test-ConversationActivity {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)] [GroupInfo] $Group
)

$WarningDate = (Get-Date).AddDays(-365)

$conversations = m365 entra m365group conversation list --groupId $Group.Reference.id | ConvertFrom-Json | Sort-Object -Property lastDeliveredDateTime -Descending
$latestConversation = $conversations | Where-Object {
[datetime]$_.lastDeliveredDateTime -gt $WarningDate.Date
} | Select-Object -First 1

$Group.MailboxStatus = @{
NumberOfConversations = $conversations.Length
LastConversation = $conversations ? $conversations[0].lastDeliveredDateTime : "n/a"
OutdatedConversations = 0
Reason = ""
}

# Return if there are no conversations or the latest conversation is not outdated
if (!$conversations -or $latestConversation.Count -eq 1) { return }

$outdatedConversations = $conversations | Where-Object {
[datetime]$_.lastDeliveredDateTime -lt $WarningDate
}

Write-Host " → potentially obsolete ($($outdatedConversations.Length) conversation item$($outdatedConversations.Length -gt 1 ? 's' : '') created more than 1 year ago)" -ForegroundColor Yellow
$reason = "$($outdatedConversations.Length) conversation item$($outdatedConversations.Length -gt 1 ? 's' : '') created more than 1 year ago"

$Group.MailboxStatus.OutdatedConversations = $outdatedConversations | Sort-Object -Property lastDeliveredDateTime
$Group.MailboxStatus.Reason = $reason
$Group.TestStatus = "🟡 Warning"
$Group.Reasons += $reason

try {
$Global:ObsoleteGroups.Add($Group.Reference.id, $Group)
}
catch { Write-Information "Group was already added to the list of potentially obsolete groups" }
}

function New-Report {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)] [GroupInfo] $Group
)

$exportObject = [ordered] @{
"Group Name" = $Group.Reference.displayName
Description = $Group.Reference.description
"Managed by" = $Group.Membership.Owners ? $Group.Membership.Owners.displayName -join ", " : "n/a"
Owners = $Group.Membership.Owners.Count
Members = $Group.Membership.Members.Count
Guests = $Group.Membership.Guests.Count
"Group Status" = $Group.Membership.Status ?? "Normal"
"Number of Conversations" = $Group.MailboxStatus.NumberOfConversations ? $Group.MailboxStatus.NumberOfConversations : "n/a"
"Last Conversation" = $Group.MailboxStatus.LastConversation
"Conversation Status" = $Group.MailboxStatus.Reason ?? "Normal"
"Team enabled" = $Group.Reference.resourceProvisioningOptions -contains 'Team' ? "True" : "False"
"SPO Status" = $Group.SharePointStatus.Reason ?? "Normal"
"SPO Activity" = $Group.SharePointStatus ? "Low / No document library usage" : "Document library in use"
"Number of warnings" = $Group.Reasons.Count
Status = $Group.TestStatus
}

$exportObject | Export-Csv -Path $Script:ReportPath -Append -NoTypeInformation
}

# START the report generation
Start-Routine #-KeepConnectionsAlive -KeepOutputPath
```
</TabItem>
</Tabs>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@
"Levert, Sebastien <slevert@outlook.com>",
"Lingstuyl, Martin <mlingstuyl@live.com>",
"Macháček, Martin <machacek@edhouse.cz>",
"Maestrini Tobias <tobias@bee365.ch>",
"Maestrini, Tobias <tobias.maestrini@gmail.com>",
"Maillot, Michaël <battosaimykle@gmail.com>",
"Mastykarz, Waldek <waldek@mastykarz.nl>",
"McDonnell, Kevin <kevin@mcd79.com>",
Expand Down
Loading