|
| 1 | +function Get-InactiveADUser { |
| 2 | + <# |
| 3 | + .SYNOPSIS |
| 4 | + Find inactive enabled users in Active Directory based on logon activity. |
| 5 | +
|
| 6 | + .DESCRIPTION |
| 7 | + This script queries Active Directory for user accounts and checks both the LastLogonDate (replicated) and |
| 8 | + LastLogon (non-replicated) attributes across all domain controllers to determine true inactivity. It also |
| 9 | + displays the last password change date for each user. |
| 10 | +
|
| 11 | + .PARAMETER InactiveDays |
| 12 | + Number of days of inactivity to consider a user inactive. Default is 90 days. |
| 13 | +
|
| 14 | + .PARAMETER IncludeDisabled |
| 15 | + Switch to include disabled accounts in the results. |
| 16 | +
|
| 17 | + .PARAMETER CheckAllDCs |
| 18 | + Check the non-replication LastLogon user attribute on ALL domain controllers for extra validation of activity. |
| 19 | +
|
| 20 | + .PARAMETER ExportCSV |
| 21 | + Optional path and filename to export results to a CSV file. |
| 22 | +
|
| 23 | + .EXAMPLE |
| 24 | + Get-InactiveADUser -InactiveDays 60 |
| 25 | +
|
| 26 | + Finds users inactive for 60 or more days. |
| 27 | +
|
| 28 | + .EXAMPLE |
| 29 | + Get-InactiveADUser -InactiveDays 90 -IncludeDisabled |
| 30 | +
|
| 31 | + Finds all enabled and disabled users inactive for 90 or more days. |
| 32 | +
|
| 33 | + .NOTES |
| 34 | + Author: Sam Erde |
| 35 | + Version: 1.1.2 |
| 36 | + Requires: Active Directory PowerShell Module |
| 37 | +
|
| 38 | + The lastLogonTimeStamp is replicated to all domain controllers and is accurate within ~14 days. |
| 39 | +
|
| 40 | + .LINK |
| 41 | + https://learn.microsoft.com/en-us/services-hub/unified/health/remediation-steps-ad/regularly-check-for-and-remove-inactive-user-accounts-in-active-directory#context--best-practices |
| 42 | +
|
| 43 | + #> |
| 44 | + |
| 45 | + [CmdletBinding()] |
| 46 | + [OutputType('Microsoft.ActiveDirectory.Management.ADUser')] |
| 47 | + Param( |
| 48 | + # The minimum number of days of inactivity to use as a cutoff for considering a user to be inactive. |
| 49 | + [Parameter(Mandatory = $false, HelpMessage = 'Minimum number of days of inactivity to use as the cutoff (1-3650).')] |
| 50 | + [ValidateRange(1, 3650)] |
| 51 | + [int]$InactiveDays = 90, |
| 52 | + |
| 53 | + # Include disabled accounts in the review of inactive users. By default, only enabled users are reviewed. |
| 54 | + [switch]$IncludeDisabled, |
| 55 | + |
| 56 | + # Check users' LastLogon timestamp on all domain controllers for extra confirmation. |
| 57 | + [switch]$CheckAllDCs, |
| 58 | + |
| 59 | + # Export the results to a CSV file. |
| 60 | + [string] $ExportCSV |
| 61 | + ) |
| 62 | + |
| 63 | + # Requires the Active Directory module. |
| 64 | + try { |
| 65 | + Import-Module ActiveDirectory -ErrorAction Stop |
| 66 | + Write-Verbose 'Active Directory module imported successfully' |
| 67 | + } catch { |
| 68 | + Write-Error "Failed to import Active Directory module: $($_.Exception.Message)" |
| 69 | + return |
| 70 | + } |
| 71 | + |
| 72 | + $Results = [System.Collections.Generic.List[Microsoft.ActiveDirectory.Management.ADUser]]::new() |
| 73 | + |
| 74 | + $InactiveDate = (Get-Date).AddDays(-$InactiveDays) |
| 75 | + $DomainControllers = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName |
| 76 | + |
| 77 | + Write-Host "Finding inactive users (inactive for $InactiveDays days or more)..." -ForegroundColor Cyan |
| 78 | + Write-Host "Checking all domain controllers for most recent logon times..." -ForegroundColor Cyan |
| 79 | + |
| 80 | + # Build filter for user query |
| 81 | + $Filter = { Enabled -eq $true -and LastLogonDate -lt $InactiveDate } |
| 82 | + if ($IncludeDisabled) { |
| 83 | + $Filter = "LastLogonDate -lt $InactiveDate" |
| 84 | + } |
| 85 | + |
| 86 | + # Get all Active Directory users |
| 87 | + $Users = Get-ADUser -Filter $Filter -Properties LastLogonDate, PasswordLastSet, Enabled, DistinguishedName, CanonicalName, CN | Sort-Object CanonicalName |
| 88 | + |
| 89 | + foreach ($User in $Users) { |
| 90 | + $MostRecentLogon = $null |
| 91 | + |
| 92 | + # Check LastLogonDate (replicated attribute) |
| 93 | + if ($User.LastLogonDate) { |
| 94 | + $MostRecentLogon = $User.LastLogonDate |
| 95 | + } |
| 96 | + |
| 97 | + if ($CheckAllDCs) { |
| 98 | + # Skip the check across all DCs if there is already a LastLogonDate within the past 14 days and if the most recent logon is more recent than the inactive date threshold. |
| 99 | + if ( $MostRecentLogon -lt (Get-Date).AddDays(-14) -and (-not $MostRecentLogon -lt $InactiveDate) ) { |
| 100 | + # Check LastLogon (non-replicated) on every domain controller. |
| 101 | + foreach ($DC in $DomainControllers) { |
| 102 | + try { |
| 103 | + $DCUser = Get-ADUser -Identity $User.SamAccountName -Server $DC -Properties LastLogon -ErrorAction Stop |
| 104 | + |
| 105 | + if ($DCUser.LastLogon -gt 0) { |
| 106 | + $LastLogonDC = [DateTime]::FromFileTime($DCUser.LastLogon) |
| 107 | + |
| 108 | + if ($null -eq $MostRecentLogon -or $LastLogonDC -gt $MostRecentLogon) { |
| 109 | + $MostRecentLogon = $LastLogonDC |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + catch { |
| 114 | + Write-Warning "Could not query $DC for user $($User.SamAccountName): $($_.Exception.Message)" |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + # Determine if user is inactive by checking the most recent logon date. |
| 121 | + $IsInactive = $false |
| 122 | + if ($null -eq $MostRecentLogon) { |
| 123 | + $IsInactive = $true |
| 124 | + } elseIf ($MostRecentLogon -lt $InactiveDate) { |
| 125 | + $IsInactive = $true |
| 126 | + } |
| 127 | + $DaysInactive = if ($MostRecentLogon) { (New-TimeSpan -Start $MostRecentLogon -End (Get-Date)).Days } else { 'Never logged on' } |
| 128 | + |
| 129 | + # Add properties to the user object. |
| 130 | + $User | Add-Member -Force -MemberType NoteProperty -Name MostRecentLogon -Value $MostRecentLogon | Out-Null |
| 131 | + $User | Add-Member -Force -MemberType NoteProperty -Name IsInactive -Value $IsInactive | Out-Null |
| 132 | + $User | Add-Member -Force -MemberType NoteProperty -Name DaysInactive -Value $DaysInactive | Out-Null |
| 133 | + |
| 134 | + if ($IsInactive) { |
| 135 | + $Results.Add($User) |
| 136 | + } |
| 137 | + } # end foreach user |
| 138 | + |
| 139 | + # Display results |
| 140 | + if ($Results.Count -gt 0) { |
| 141 | + Write-Host "`nFound $($Results.Count) inactive user(s):" -ForegroundColor Yellow |
| 142 | + $Results | Format-Table CanonicalName, Enabled, MostRecentLogon, DaysInactive, PasswordLastSet -AutoSize | Out-Host |
| 143 | + |
| 144 | + # Optional: Export to CSV |
| 145 | + if ($PSBoundParameters.ContainsKey('ExportCSV')) { |
| 146 | + $ExportPath = ".\InactiveUsers_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" |
| 147 | + $Results | Export-Csv -Path $ExportPath -NoTypeInformation |
| 148 | + Write-Host "Results exported to: $ExportPath" -ForegroundColor Green |
| 149 | + } |
| 150 | + } else { |
| 151 | + Write-Host "`nNo inactive users found." -ForegroundColor Green |
| 152 | + } |
| 153 | + |
| 154 | + $Results |
| 155 | +} # end Get-InactiveADUser |
0 commit comments