1. Enterprise Scenario & Objectives
In large Microsoft 365 environments, unmonitored inactive accounts pose significant financial waste and security vulnerabilities. Organizations routinely pay for E3/E5 licenses assigned to former employees, temporary contractors, or abandoned accounts. Additionally, stale credentials remain high-value targets for credential stuffing and unauthorized access.
This automated audit script addresses both risk and waste by:
- Identifying accounts with zero sign-in activity over a customizable threshold (e.g., 90 days).
- Checking for inactive accounts that still have active, paid M365 licenses assigned.
- Generating an actionable HTML/CSV audit report for IT Management and Finance.
- Providing an optional automated license reclamation workflow to reduce monthly tenant spend.
2. Prerequisites & Required Modules
Run the following command in PowerShell to install the Microsoft Graph SDK module:
Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force
3. Authentication & Required Permissions
Executing audit queries against Azure AD / Entra ID sign-in logs requires the following Graph API scopes:
- Graph API Scopes:
AuditLog.Read.All,User.Read.All,Directory.Read.All - Azure Directory Role: Global Reader or Security Reader
4. The Complete Production Script
<#
.SYNOPSIS
Microsoft 365 Inactive Account & License Reclamation Audit Script
.DESCRIPTION
Scans Entra ID (Azure AD) for user accounts inactive for X days, checks assigned licenses,
and outputs a CSV audit report to assist in software asset management and cost optimization.
.AUTHOR
Innovation Guru (https://dsinnovationguru.com)
#>
param (
[Parameter(Mandatory = $false)]
[int]$DaysInactive = 90,
[Parameter(Mandatory = $false)]
[string]$ExportPath = "C:\Logs\M365_Inactive_Users_Report.csv"
)
$ErrorActionPreference = "Stop"
# Calculate threshold date
$CutoffDate = (Get-Date).AddDays(-$DaysInactive)
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
Connect-MgGraph -Scopes "AuditLog.Read.All", "User.Read.All" | Out-Null
Write-Host "Fetching users and sign-in activity (Threshold: $DaysInactive days)..." -ForegroundColor Green
# Query Entra ID users with sign-in activity and license properties
$Users = Get-MgUser -All -Property Id, DisplayName, UserPrincipalName, AccountEnabled, SignInActivity, AssignedLicenses
$Report = [System.Collections.Generic.List[PSObject]]::new()
foreach ($User in $Users) {
$LastSignIn = $User.SignInActivity.LastSignInDateTime
# Determine status if user has never signed in or logged in before the cutoff date
if ($null -ne $LastSignIn -and $LastSignIn -lt $CutoffDate) {
$HasLicense = ($User.AssignedLicenses.Count -gt 0)
$Report.Add([PSCustomObject]@{
DisplayName = $User.DisplayName
UserPrincipalName = $User.UserPrincipalName
AccountEnabled = $User.AccountEnabled
LastSignInDate = $LastSignIn
HasActiveLicense = $HasLicense
LicenseCount = $User.AssignedLicenses.Count
AuditStatus = "INACTIVE - Action Required"
})
}
elseif ($null -eq $LastSignIn -and $User.AccountEnabled -eq $true) {
$Report.Add([PSCustomObject]@{
DisplayName = $User.DisplayName
UserPrincipalName = $User.UserPrincipalName
AccountEnabled = $User.AccountEnabled
LastSignInDate = "Never Logged In"
HasActiveLicense = ($User.AssignedLicenses.Count -gt 0)
LicenseCount = $User.AssignedLicenses.Count
AuditStatus = "NEVER LOGGED IN - Review Account"
})
}
}
# Export Results
if ($Report.Count -gt 0) {
$Report | Export-Csv -Path $ExportPath -NoTypeInformation
Write-Host "Audit completed! Identified $($Report.Count) inactive accounts. Report saved to: $ExportPath" -ForegroundColor Yellow
} else {
Write-Host "No inactive accounts found matching the criteria." -ForegroundColor Green
}Save the script as Get-M365InactiveUserAudit.ps1 and execute it in an elevated PowerShell terminal.
5. Execution & Enterprise Automation
Run the audit script locally for a 90-day activity threshold:
.\Get-M365InactiveUserAudit.ps1 -DaysInactive 90 -ExportPath "C:\Reports\M365_Inactive_Users.csv"
Enterprise Automation: Schedule this script to run monthly via Azure Automation Runbooks using a System-Assigned Managed Identity. Configure the runbook to automatically email the generated report to your IT SAM (Software Asset Management) team via Exchange Online send mail action.
