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.

Leave a Comment

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

Scroll to Top