From 2f676693ac5c9b479dc45129557b92246af58341 Mon Sep 17 00:00:00 2001 From: juandresrodca Date: Wed, 24 Jun 2026 11:23:34 +0100 Subject: [PATCH] Add CLI for Microsoft 365 sample to get-disabled-or-inactive-user-accounts (#885) Adds a CLI for Microsoft 365 (PowerShell) implementation alongside the existing PnP PowerShell versions, covering the same three discovery sources: disabled accounts (Entra ID via Microsoft Graph), accounts hidden from the GAL (SharePoint People search), and inactive accounts by last interactive sign-in. Updates sample metadata, tags, references and contributor list. Co-Authored-By: Claude Opus 4.8 --- .../README.md | 137 ++++++++++++++++++ .../assets/sample.json | 20 ++- 2 files changed, 155 insertions(+), 2 deletions(-) diff --git a/scripts/get-disabled-or-inactive-user-accounts/README.md b/scripts/get-disabled-or-inactive-user-accounts/README.md index 4214db8a4..2122862ef 100644 --- a/scripts/get-disabled-or-inactive-user-accounts/README.md +++ b/scripts/get-disabled-or-inactive-user-accounts/README.md @@ -242,6 +242,142 @@ $userd1 | Export-Csv -Path "C:\temp\disabledusers.csv" -NoTypeInformation ``` [!INCLUDE [More about PnP PowerShell](../../docfx/includes/MORE-PNPPS.md)] +# [CLI for Microsoft 365 using PowerShell](#tab/cli-m365-ps) + +In order to keep your tenant clean (Governance), you might want to ensure that disabled or inactive user accounts will be replaced where appropriate (Think Owners of sites/groups, assignedto user on tasks/planner and so on). This script will help you find those accounts using CLI for Microsoft 365. + +```powershell + +# ========================================= +# Script: User Status Discovery (CLI for Microsoft 365) +# Purpose: Identify disabled and inactive users from multiple sources +# ========================================= + +# User Input +$InactiveDays = 90 +$ExportPath = "$env:TEMP\UserStatusFindings.csv" +# People result source id used for the SharePoint user profile search +$peopleSourceId = "b09a7990-05ea-4af9-81ef-edfab16c4e31" + +# Connect to Microsoft 365 +if ($(m365 status) -match "Logged Out") { + m365 login +} + +# Configure the CLI to output as JSON on each execution +$m365output = m365 cli config get --key output +if ($m365output -notmatch "json") { + m365 cli config set --key output --value json +} + +# Get CLI commands JSON output converted as objects +function Get-CLIValue { + [cmdletbinding()] + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true)] + $input + ) + + $output = $input | ConvertFrom-Json + if ($null -ne $output.error) { + throw $output.error + } + return $output +} + +# Helper to call Microsoft Graph and follow paging (@odata.nextLink) +function Invoke-GraphRequestAll { + param( + [Parameter(Mandatory)][string]$Url + ) + + $items = @() + $next = $Url + while ($next) { + $page = m365 request --url $next | Get-CLIValue + if ($page.value) { $items += $page.value } + $next = $page.'@odata.nextLink' + } + return $items +} + +# 1) Disabled accounts from Entra ID (Microsoft Graph) +function Get-DisabledUsersFromGraph { + Invoke-GraphRequestAll -Url "https://graph.microsoft.com/v1.0/users?`$select=displayName,userPrincipalName,mail,accountEnabled" | + Where-Object { $_.accountEnabled -eq $false } | + ForEach-Object { + [PSCustomObject]@{ + DisplayName = $_.displayName + UserPrincipalName = $_.userPrincipalName + Mail = $_.mail + Reason = "AccountDisabled" + Source = "EntraID" + } + } +} + +# 2) Accounts tagged as left/disabled in the SharePoint user profile (People search) +function Get-DisabledUsersFromSharePointSearch { + # How an account is tagged as disabled varies from org to org, so you might need to change the + # filter below (e.g. a custom EmployeeStatus / DateLeft property, or a naming convention). + $results = m365 spo search --queryText "*" --sourceId $peopleSourceId ` + --selectProperties "WorkEmail,SPS-HideFromAddressLists,PreferredName" --allResults | Get-CLIValue + + $results | + Where-Object { "$($_.'SPS-HideFromAddressLists')" -eq "true" } | + ForEach-Object { + [PSCustomObject]@{ + DisplayName = $_.PreferredName + UserPrincipalName = $_.WorkEmail + Mail = $_.WorkEmail + Reason = "HiddenFromGAL" + Source = "SharePointSearch" + } + } +} + +# 3) Inactive accounts based on last interactive sign-in (requires AuditLog.Read.All + Entra ID P1) +function Get-InactiveUsersFromGraph { + param( + [int]$Days = 90 + ) + + $cutoff = (Get-Date).AddDays(-$Days) + $users = Invoke-GraphRequestAll -Url "https://graph.microsoft.com/v1.0/users?`$select=id,displayName,userPrincipalName" + + foreach ($user in $users) { + $signInUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$top=1&`$filter=userPrincipalName eq '$($user.userPrincipalName)'" + $signIn = m365 request --url $signInUrl | Get-CLIValue + + if ($signIn.value.Count -eq 0 -or [datetime]$signIn.value[0].createdDateTime -lt $cutoff) { + [PSCustomObject]@{ + DisplayName = $user.displayName + UserPrincipalName = $user.userPrincipalName + Mail = $null + Reason = "Inactive > $Days days" + Source = "AuditLogs" + } + } + } +} + +# --------------------------- +# Execution +# --------------------------- + +$results = @() +$results += Get-DisabledUsersFromGraph +$results += Get-DisabledUsersFromSharePointSearch +$results += Get-InactiveUsersFromGraph -Days $InactiveDays + +$results | + Sort-Object UserPrincipalName, Reason | + Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 + +Write-Host "Exported $($results.Count) findings to $ExportPath" + +``` +[!INCLUDE [More about CLI for Microsoft 365](../../docfx/includes/MORE-CLIM365.md)] *** @@ -252,6 +388,7 @@ $userd1 | Export-Csv -Path "C:\temp\disabledusers.csv" -NoTypeInformation |-----------| | Kasper Larsen | | [Josiah Opiyo](https://github.com/ojopiyo) | +| [juandresrodca](https://github.com/juandresrodca) | [!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)] diff --git a/scripts/get-disabled-or-inactive-user-accounts/assets/sample.json b/scripts/get-disabled-or-inactive-user-accounts/assets/sample.json index 40f1a8873..50177824a 100644 --- a/scripts/get-disabled-or-inactive-user-accounts/assets/sample.json +++ b/scripts/get-disabled-or-inactive-user-accounts/assets/sample.json @@ -9,7 +9,7 @@ "In order to keep your tenant clean (Governance), you might want to ensure that disabled or inactive user accounts will be replaced where oppropriate (Think Owners of sites/groups, assignedto user on tasks/planner and so on). This script will help you find those accounts." ], "creationDateTime": "2023-10-15", - "updateDateTime": "2025-12-18", + "updateDateTime": "2026-06-24", "products": [ "SharePoint", "Graph", @@ -19,6 +19,10 @@ { "key": "PNP-POWERSHELL", "value": "3.1.0" + }, + { + "key": "CLI-FOR-MICROSOFT365", + "value": "11.6.0" } ], "categories": [ @@ -27,7 +31,8 @@ "Security" ], "tags": [ - "Invoke-PnPGraphMethod,Invoke-PnPSearchQuery,Invoke-RestMethod" + "Invoke-PnPGraphMethod,Invoke-PnPSearchQuery,Invoke-RestMethod", + "m365 request,m365 spo search" ], "thumbnails": [ { @@ -49,6 +54,12 @@ "company": "", "pictureUrl": "https://github.com/kasperbolarsen.png", "name": "Kasper Larsen" + }, + { + "gitHubAccount": "juandresrodca", + "company": "", + "pictureUrl": "https://github.com/juandresrodca.png", + "name": "juandresrodca" } ], "references": [ @@ -56,6 +67,11 @@ "name": "Want to learn more about PnP PowerShell and the cmdlets", "description": "Check out the PnP PowerShell site to get started and for the reference to the cmdlets.", "url": "https://aka.ms/pnp/powershell" + }, + { + "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" } ] }