Skip to content

Commit 8af6d77

Browse files
committed
✨feat: add Get-InactiveUsers function to list inactive Active Directory users based on last logon timestamp
1 parent dde675a commit 8af6d77

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
function Get-InactiveUsers {
2+
<#
3+
.SYNOPSIS
4+
Lists inactive Active Directory users based on last logon timestamp information.
5+
6+
.DESCRIPTION
7+
The script queries Active Directory for the 'lastLogonTimeStamp' attribute of enabled user accounts. This replicated
8+
attribute provides sufficient accuracy for inactive user detection while offering much better performance than
9+
querying all domain controllers. Accounts with no logon after the defined threshold are reported as inactive. Users
10+
who have never logged on are included if their account creation date is older than the DaysInactive threshold.
11+
12+
.PARAMETER DaysInactive
13+
Number of days of inactivity used to identify an inactive user. Also used as the threshold for including never-
14+
logged-on users (if their account was created more than this many days ago).
15+
16+
.PARAMETER ExcludeNeverLoggedOn
17+
Exclude users who have never logged on from the results, even if their account creation date exceeds the threshold.
18+
19+
.PARAMETER PassThru
20+
Return the inactive user objects to the pipeline instead of displaying formatted output. Useful for further processing
21+
or assignment to variables.
22+
23+
.PARAMETER ExportPath
24+
Optional path and filename to export results to a CSV file.
25+
26+
.EXAMPLE
27+
Get-InactiveUsers -DaysInactive 90
28+
Returns users who haven't logged on in 90+ days, plus never-logged-on users created 90+ days ago.
29+
30+
.EXAMPLE
31+
Get-InactiveUsers -DaysInactive 30 -ExportPath "C:\Reports\InactiveUsers.csv"
32+
Returns users who haven't logged on in 30+ days, plus never-logged-on users created 30+ days ago.
33+
Exports results to CSV and uses 30-day threshold.
34+
35+
.EXAMPLE
36+
Get-InactiveUsers -DaysInactive 60 -ExcludeNeverLoggedOn
37+
Returns only users who logged on but haven't logged on in 60+ days (excludes never-logged-on users).
38+
39+
.EXAMPLE
40+
$InactiveUsers = Get-InactiveUsers -DaysInactive 30 -PassThru
41+
Gets inactive users and stores them in a variable for further processing without displaying formatted output.
42+
43+
.NOTES
44+
Author: Sam Erde
45+
Version: 1.1
46+
Requires: Active Directory PowerShell Module
47+
48+
Uses lastLogonTimeStamp for faster execution (accurate within ~14 days)
49+
#>
50+
51+
[CmdletBinding()]
52+
param (
53+
[Parameter(HelpMessage = 'Number of days of inactivity to use as the cutoff (1-3650).')]
54+
[ValidateRange(1, 3650)]
55+
[int] $DaysInactive = 90,
56+
57+
[Parameter(HelpMessage = 'Path to export results to CSV.')]
58+
[string] $ExportPath,
59+
60+
[Parameter(HelpMessage = 'Exclude users who have never logged on from the results.')]
61+
[switch] $ExcludeNeverLoggedOn,
62+
63+
[Parameter(HelpMessage = 'Return objects to the pipeline instead of displaying formatted output.')]
64+
[switch] $PassThru
65+
)
66+
67+
# Requires the Active Directory module.
68+
try {
69+
Import-Module ActiveDirectory -ErrorAction Stop
70+
Write-Verbose 'Active Directory module imported successfully'
71+
} catch {
72+
Write-Error "Failed to import Active Directory module: $($_.Exception.Message)"
73+
break 1
74+
}
75+
76+
# Calculate the cutoff date and its file time representation for the AD filter.
77+
[datetime]$CutoffDate = (Get-Date).AddDays(-$DaysInactive)
78+
[long]$CutoffFileTime = $CutoffDate.ToFileTime()
79+
80+
Write-Host "Searching for users inactive since: $($CutoffDate.ToString('yyyy-MM-dd HH:mm:ss'))." -ForegroundColor Cyan
81+
Write-Host 'Using lastLogonTimeStamp for better performance (precise to within ~14 days).' -ForegroundColor Yellow
82+
83+
# Build the server-side filter for Get-ADUser.
84+
# This is more efficient as it filters objects on the domain controller.
85+
$Filter = "Enabled -eq 'True' -and (lastLogonTimeStamp -lt '$CutoffFileTime' -and lastLogonTimeStamp -ne 0)"
86+
87+
if (-not $ExcludeNeverLoggedOn) {
88+
$Filter += " -or (lastLogonTimeStamp -notlike '*' -and whenCreated -lt '$CutoffDate')"
89+
Write-Host 'Including never-logged-on users created before the cutoff date.' -ForegroundColor Yellow
90+
}
91+
92+
# Get user accounts matching the optimized filter.
93+
Write-Host 'Retrieving inactive user accounts...' -ForegroundColor Yellow
94+
try {
95+
$InactiveUsersResult = Get-ADUser -Filter $Filter -Properties samAccountName, DisplayName, lastLogonTimeStamp, whenCreated, DistinguishedName -ErrorAction Stop
96+
Write-Host "Found $($InactiveUsersResult.Count) inactive user account(s)." -ForegroundColor Green
97+
} catch {
98+
Write-Error "Failed to get user accounts: $($_.Exception.Message)"
99+
break 1
100+
}
101+
102+
$InactiveUsers = @()
103+
[Int16] $UserCount = 0
104+
105+
foreach ($User in $InactiveUsersResult) {
106+
$UserCount++
107+
$PercentComplete = [math]::Round(($UserCount / $InactiveUsersResult.Count) * 100, 1)
108+
Write-Progress -Activity 'Processing user data' -Status "Processing $($User.SamAccountName) ($UserCount of $($InactiveUsersResult.Count))" -PercentComplete $PercentComplete
109+
110+
# Reset blank defaults before adding a user to the results.
111+
$LastLogonTimeStamp = $User.lastLogonTimeStamp
112+
$LastLogonDate = $null
113+
$Status = ''
114+
115+
if ($null -eq $LastLogonTimeStamp -or $LastLogonTimeStamp -eq 0) {
116+
$Status = 'Never logged on'
117+
} else {
118+
$LastLogonDate = [DateTime]::FromFileTime($LastLogonTimeStamp)
119+
$Status = 'Inactive'
120+
}
121+
122+
# Calculate days since last logon
123+
$DaysSinceLogon = if ($LastLogonDate) {
124+
[math]::Round((New-TimeSpan -Start $LastLogonDate -End (Get-Date)).TotalDays)
125+
} else {
126+
$null
127+
}
128+
129+
$InactiveUsers += [PSCustomObject]@{
130+
SamAccountName = $User.SamAccountName
131+
DisplayName = $User.DisplayName
132+
LastLogonDate = $LastLogonDate
133+
DaysSinceLogon = $DaysSinceLogon
134+
Status = $Status
135+
WhenCreated = $User.whenCreated
136+
DaysSinceCreated = [math]::Round((New-TimeSpan -Start $User.whenCreated -End (Get-Date)).TotalDays)
137+
DistinguishedName = $User.DistinguishedName
138+
}
139+
}
140+
141+
Write-Progress -Activity 'Processing user data.' -Completed
142+
143+
# Process results
144+
if ($InactiveUsers.Count -gt 0) {
145+
# Sort once for all operations
146+
$SortedInactiveUsers = $InactiveUsers | Sort-Object LastLogonDate
147+
148+
# Handle PassThru parameter
149+
if ($PassThru) {
150+
# Return objects to pipeline and suppress other output
151+
Write-Verbose "Found $($InactiveUsers.Count) inactive user(s) - returning objects to pipeline"
152+
153+
# Export to CSV if requested (silent operation with PassThru)
154+
if ($ExportPath) {
155+
try {
156+
$SortedInactiveUsers | Export-Csv -Path $ExportPath -NoTypeInformation -ErrorAction Stop
157+
Write-Verbose "Results exported to: $ExportPath"
158+
} catch {
159+
Write-Error "Failed to export results: $($_.Exception.Message)"
160+
}
161+
}
162+
163+
# Return sorted objects to pipeline
164+
$SortedInactiveUsers
165+
} else {
166+
# Normal display mode
167+
Write-Host "`nFound $($InactiveUsers.Count) inactive user(s):" -ForegroundColor Red
168+
169+
# Display formatted table
170+
$SortedInactiveUsers | Format-Table -AutoSize -Property SamAccountName, DisplayName, LastLogonDate, DaysSinceLogon, DaysSinceCreated
171+
172+
# Export to CSV if requested
173+
if ($ExportPath) {
174+
try {
175+
$SortedInactiveUsers | Export-Csv -Path $ExportPath -NoTypeInformation -ErrorAction Stop
176+
Write-Host "Results exported to: $ExportPath" -ForegroundColor Green
177+
} catch {
178+
Write-Error "Failed to export results: $($_.Exception.Message)"
179+
}
180+
}
181+
182+
# Summary statistics
183+
$NeverLoggedOn = ($InactiveUsers | Where-Object { $_.Status -eq 'Never logged on' }).Count
184+
$InactiveCount = ($InactiveUsers | Where-Object { $_.Status -eq 'Inactive' }).Count
185+
186+
Write-Host "`nSummary:" -ForegroundColor Cyan
187+
Write-Host " Total inactive users: $($InactiveUsers.Count)" -ForegroundColor White
188+
Write-Host " Never logged on: $NeverLoggedOn" -ForegroundColor White
189+
Write-Host " Inactive (logged on before cutoff): $InactiveCount" -ForegroundColor White
190+
Write-Host " Cutoff date: $($CutoffDate.ToString('yyyy-MM-dd HH:mm:ss'))" -ForegroundColor White
191+
}
192+
} else {
193+
if ($PassThru) {
194+
# Return empty array for consistency
195+
Write-Verbose 'No inactive users found - returning empty array'
196+
return @()
197+
} else {
198+
Write-Host "`nNo inactive users found matching the specified criteria." -ForegroundColor Green
199+
Write-Host "Cutoff date: $($CutoffDate.ToString('yyyy-MM-dd HH:mm:ss'))" -ForegroundColor White
200+
}
201+
}
202+
203+
}

0 commit comments

Comments
 (0)