diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/preview.png b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/preview.png
new file mode 100644
index 0000000000..9098eba2c6
Binary files /dev/null and b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/preview.png differ
diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json
new file mode 100644
index 0000000000..25ffb0489a
--- /dev/null
+++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json
@@ -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"
+ }
+ ]
+ }
+]
diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx
new file mode 100644
index 0000000000..8ed4cb4cab
--- /dev/null
+++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx
@@ -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.
+
+
+
+
+ ```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
+ ```
+
+
diff --git a/package.json b/package.json
index 7133ed1428..ee9cbcfef2 100644
--- a/package.json
+++ b/package.json
@@ -177,7 +177,7 @@
"Levert, Sebastien ",
"Lingstuyl, Martin ",
"Macháček, Martin ",
- "Maestrini Tobias ",
+ "Maestrini, Tobias ",
"Maillot, Michaël ",
"Mastykarz, Waldek ",
"McDonnell, Kevin ",
diff --git a/src/m365/spo/commands/list/list-get.spec.ts b/src/m365/spo/commands/list/list-get.spec.ts
index ad2786adfd..4dec616c95 100644
--- a/src/m365/spo/commands/list/list-get.spec.ts
+++ b/src/m365/spo/commands/list/list-get.spec.ts
@@ -1281,4 +1281,24 @@ describe(commands.LIST_GET, () => {
const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', id: '0CD891EF-AFCE-4E55-B836-FCE03286CCCF' } }, commandInfo);
assert(actual);
});
-});
+
+ it('retrieves the default list in the specified site by providing a webUrl', async () => {
+ const defaultSiteList = { ...listResponse, BaseTemplate: 101, ParentWebUrl: "/", ListItemEntityTypeFullName: "SP.Data.Shared_x0020_DocumentsItem" };
+
+ sinon.stub(request, 'get').resolves({
+ ...defaultSiteList
+ });
+
+ await command.action(logger, {
+ options: {
+ webUrl: 'https://contoso.sharepoint.com'
+ }
+ });
+
+ assert(loggerLogSpy.calledWithMatch({
+ BaseTemplate: 101,
+ ParentWebUrl: "/",
+ ListItemEntityTypeFullName: "SP.Data.Shared_x0020_DocumentsItem"
+ }));
+ });
+});
\ No newline at end of file
diff --git a/src/m365/spo/commands/list/list-get.ts b/src/m365/spo/commands/list/list-get.ts
index 53476a01f5..5165480ede 100644
--- a/src/m365/spo/commands/list/list-get.ts
+++ b/src/m365/spo/commands/list/list-get.ts
@@ -35,7 +35,7 @@ class SpoListGetCommand extends SpoCommand {
}
public get description(): string {
- return 'Gets information about the specific list';
+ return 'Gets information about the specific list or returns information about the default list in a site';
}
constructor() {
@@ -44,7 +44,6 @@ class SpoListGetCommand extends SpoCommand {
this.#initTelemetry();
this.#initOptions();
this.#initValidators();
- this.#initOptionSets();
}
#initTelemetry(): void {
@@ -101,10 +100,6 @@ class SpoListGetCommand extends SpoCommand {
);
}
- #initOptionSets(): void {
- this.optionSets.push({ options: ['id', 'title', 'url'] });
- }
-
public async commandAction(logger: Logger, args: CommandArgs): Promise {
if (this.verbose) {
await logger.logToStderr(`Retrieving information for list in site at ${args.options.webUrl}...`);
@@ -112,6 +107,9 @@ class SpoListGetCommand extends SpoCommand {
let requestUrl: string = `${args.options.webUrl}/_api/web/`;
+ if (!args.options.id && !args.options.title && !args.options.url) {
+ requestUrl += `DefaultDocumentLibrary`;
+ }
if (args.options.id) {
requestUrl += `lists(guid'${formatting.encodeQueryParameter(args.options.id)}')`;
}