# This script requires PowerShell 7 or later for the following reasons:
# 1. The Microsoft Graph PowerShell SDK (Microsoft.Graph module) is designed for PowerShell 7+ and may not work reliably in Windows PowerShell 5.1.
# 2. The SignInActivity property is not returned or accessible in PowerShell 5.1 due to .NET and serialization limitations.
# 3. Modern authentication methods used by Connect-MgGraph are only fully supported in PowerShell 7+.
# 4. Paging and the -All switch for Get-MgUser are more reliable in PowerShell 7+.
# 5. Microsoft is focusing new features and bug fixes for the Graph SDK on PowerShell 7+ only.
# Attempting to run this script in Windows PowerShell 5.1 or earlier will result in errors or missing data.
# Check for PowerShell 7+
if ($PSVersionTable.PSVersion.Major -lt 7) {
Write-Host "ERROR: This script requires PowerShell 7 or later. Please run it in PowerShell 7+ (pwsh.exe)." -ForegroundColor Red
exit 1
}
# Check if Microsoft.Graph module is installed
$module = Get-Module -Name Microsoft.Graph -ListAvailable
if (-not $module) {
Write-Host "Microsoft.Graph module not found. Installing..."
try {
Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force -ErrorAction Stop
Write-Host "Microsoft.Graph module installed successfully."
}
catch {
Write-Host "ERROR: Failed to install Microsoft.Graph module. Error: $_" -ForegroundColor Red
exit 1
}
}
else {
Write-Host "Microsoft.Graph module is already installed."
}
# Import the module if not already loaded
if (-not (Get-Command -Name Connect-MgGraph -ErrorAction SilentlyContinue)) {
Import-Module Microsoft.Graph -ErrorAction Stop
}
# Connect to Microsoft Graph with required scopes
Write-Host "Connecting to Microsoft Graph..."
Connect-MgGraph -Scopes "User.Read.All", "AuditLog.Read.All", "Device.Read.All", "Organization.Read.All"
# Define log file
$logFile = "InactiveUsersAndOldDevices_Report_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
# Dynamically retrieve the organization's primary domain
Write-Host "Retrieving organization details to determine primary domain..."
$org = Get-MgOrganization
$verifiedDomains = $org.VerifiedDomains
$initialDomain = $verifiedDomains | Where-Object { $_.IsInitial -eq $true } | Select-Object -ExpandProperty Name -First 1
$customDomains = $verifiedDomains | Where-Object { $_.IsInitial -eq $false } | Select-Object -ExpandProperty Name
if ($customDomains.Count -gt 0) {
$orgDomain = $customDomains[0] # Select the first custom domain as primary; adjust logic if needed for multiple domains
} else {
$orgDomain = $initialDomain # Fallback to initial domain if no custom domains exist
}
Write-Host "Primary domain dynamically set to: $orgDomain"
# Threshold: 6 months ago
$inactiveThreshold = (Get-Date).AddMonths(-6)
# Initialize log
Add-Content -Path $logFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Starting Inactive Users + Old Enabled Devices Report."
Add-Content -Path $logFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Primary domain: $orgDomain"
#####################################################################
# 1. INACTIVE ENABLED USERS (6+ months no sign-in)
#####################################################################
Write-Host "Retrieving inactive enabled users..." -ForegroundColor Cyan
$allUsers = Get-MgUser -All -Property Id, DisplayName, SignInActivity, AccountEnabled, UserType, UserPrincipalName -ConsistencyLevel eventual -CountVariable userCount |
Where-Object {
$_.AccountEnabled -eq $true -and
$_.SignInActivity.LastSignInDateTime -and
([datetime]$_.SignInActivity.LastSignInDateTime) -lt $inactiveThreshold
}
$guestUsers = $allUsers | Where-Object UserType -eq 'Guest'
$memberUsers = $allUsers | Where-Object UserType -eq 'Member'
# Log Guest Users
Add-Content -Path $logFile -Value "`n$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Inactive Guest Users (6+ months no sign-in):"
if ($guestUsers.Count -eq 0) {
$msg = "No inactive Guest users found."
Write-Host $msg
Add-Content -Path $logFile -Value $msg
} else {
foreach ($user in $guestUsers) {
$msg = "Guest: $($user.DisplayName) | UPN: $($user.UserPrincipalName) | Last sign-in: $($user.SignInActivity.LastSignInDateTime)"
Write-Host $msg -ForegroundColor Yellow
Add-Content -Path $logFile -Value $msg
}
}
# Log Member Users
Add-Content -Path $logFile -Value "`n$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Inactive Member Users (6+ months no sign-in):"
if ($memberUsers.Count -eq 0) {
$msg = "No inactive Member users found."
Write-Host $msg
Add-Content -Path $logFile -Value $msg
} else {
foreach ($user in $memberUsers) {
$msg = "Member: $($user.DisplayName) | UPN: $($user.UserPrincipalName) | Last sign-in: $($user.SignInActivity.LastSignInDateTime)"
Write-Host $msg -ForegroundColor Yellow
Add-Content -Path $logFile -Value $msg
}
}
#####################################################################
# 2. OLD ENABLED DEVICES (6+ months no sign-in)
#####################################################################
Write-Host "Retrieving old enabled devices (no sign-in for 6+ months)..." -ForegroundColor Cyan
# Get all Entra ID devices
$devices = Get-MgDevice -All -Property Id, DisplayName, ApproximateLastSignInDateTime, AccountEnabled, OperatingSystem, TrustType, ProfileType, DeviceOwnership
# Filter for enabled stale devices
$oldDevices = @()
foreach ($device in $devices) {
if ($device.AccountEnabled -eq $true -and
$device.ApproximateLastSignInDateTime -ne $null -and
([datetime]$device.ApproximateLastSignInDateTime) -lt $inactiveThreshold) {
# Get owner if available
$ownerUPN = "Unknown"
$ownerDisplayName = "Unknown"
try {
$ownerRef = Get-MgDeviceRegisteredOwner -DeviceId $device.Id -Top 1
if ($ownerRef) {
$ownerId = $ownerRef.Id
$owner = Get-MgUser -UserId $ownerId -Property DisplayName, UserPrincipalName
$ownerDisplayName = $owner.DisplayName
$ownerUPN = $owner.UserPrincipalName
}
} catch {
Write-Warning "Failed to get owner for device $($device.DisplayName): $_"
}
$oldDevices += [PSCustomObject]@{
DeviceName = $device.DisplayName
DeviceId = $device.Id
OperatingSystem = $device.OperatingSystem
TrustType = $device.TrustType
ProfileType = $device.ProfileType
DeviceOwnership = $device.DeviceOwnership
LastSignInDateTime = $device.ApproximateLastSignInDateTime
OwnerDisplayName = $ownerDisplayName
OwnerUPN = $ownerUPN
}
}
}