I am working to write some simple powershell scripts to verify account status and inactivity. I am running into an issue where the Inactivity is not returned due to a non-existent registry key. Is there a way to still pull local account last login without using Event Logs and checking every 4624 Event ID for most recent log in since the Event log is cleared weekly after Auditing. I currently have this script and cannot find a resolution myself:
# Set the inactivity threshold (in days)
$InactiveDays = 90
# Get current date
$CurrentDate = Get-Date
# Calculate the date 90 days ago
$InactiveDate = $CurrentDate.AddDays(-$InactiveDays)
# Find disabled local accounts
Write-Host "Disabled Local Accounts:" -ForegroundColor Green
$DisabledAccounts = Get-WmiObject -Class Win32_UserAccount -Filter "Disabled = True"
if ($DisabledAccounts) {
foreach ($Account in $DisabledAccounts) {
Write-Host " - $($Account.Name)"
}
} else {
Write-Host " No disabled local accounts found."
}
Write-Host ""
# Find accounts not logged in for more than 90 days (local)
Write-Host "Accounts Inactive for More Than $($InactiveDays) Days:" -ForegroundColor Green
# Get all local user accounts
$LocalAccounts = Get-WmiObject -Class Win32_UserAccount
# Loop through each account and check the LastLogin property
foreach ($Account in $LocalAccounts) {
# Get last login time from registry (if available)
$LastLoginKeyPath = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$($Account.SID)\LastLoginTime"
$LastLoginValue = Get-ItemPropertyValue -Path $LastLoginKeyPath -Name LastLoginTime -ErrorAction SilentlyContinue
if ($LastLoginValue) {
# Convert the registry value to a DateTime object
$LastLoginTime = [datetime]::FromFileTime($LastLoginValue)
# Check if the account is inactive
if ($LastLoginTime -lt $InactiveDate) {
Write-Host " - $($Account.Name) - Last Login: $($LastLoginTime)"
}
}
else {
}
}
Read-Host "Press Enter to exit"
You can use Get-LocalUser
to accomplish your needs:
Enabled
.LastLogon
.If you're curious how the cmdlet gets the LastLogon
, it uses SamQueryInformationUser
function from samlib.dll
, the pointer returned by this function is then converted to a USER_ALL_INFORMATION
struct and lastly it converts LastLogon.QuadPart
to a DateTime
using DateTimeFromSam
, which is essentially a DateTime.FromFileTime
when .QuadPart
is not 0
or int64.MaxValue
.
In summary, using Get-LocalUser
, your code can be:
# Set the inactivity threshold (in days)
$InactiveDays = 90
# Calculate the date 90 days ago
$InactiveDate = [datetime]::Now.AddDays(-$InactiveDays)
$fGreen = @{ ForegroundColor = 'Green' }
# Find disabled local accounts
Write-Host 'Disabled Local Accounts:' @fGreen
$DisabledAccounts = Get-LocalUser | Where-Object -Not Enabled
if ($DisabledAccounts) {
# Find accounts not logged in for more than 90 days (local)
$InactiveAccounts = foreach ($Account in $DisabledAccounts) {
Write-Host " - $($Account.Name)"
if ($Account.LastLogon -lt $InactiveDate) {
$Account
}
}
}
else {
Write-Host ' No disabled local accounts found.'
# Probably want to `return` here to exit early if no Disabled accounts
}
if ($InactiveAccounts) {
Write-Host "Accounts Inactive for More Than $($InactiveDays) Days:" @fGreen
$InactiveAccounts | ForEach-Object {
Write-Host " - $($_.Name) - Last Login: $($_.LastLogon)"
}
}
Read-Host 'Press Enter to exit'
You might also want to consider excluding the well known accounts from your query, and for that you could define this helper class:
using namespace System.Security.Principal
class SidHelper {
hidden static [WellKnownSidType[]] $_sids
static SidHelper() {
[SidHelper]::_sids = [Enum]::GetValues([WellKnownSidType])
}
static [bool] IsWellKnown([SecurityIdentifier] $sid) {
foreach ($s in [SidHelper]::_sids) {
if ($sid.IsWellKnown($s)) {
return $true
}
}
return $false
}
}
And then in your call to Get-LocalUser
you could do:
$DisabledAccounts = Get-LocalUser |
Where-Object { -not $_.Enabled -and -not [SidHelper]::IsWellKnown($_.Sid) }