<# .SYNOPSIS Deletes (or disables) stale computer accounts in Active Directory and logs every decision to CSV. .DESCRIPTION Takes computer names from a list or the pipeline, or finds candidates under an OU, then checks each one before touching it. A computer only counts as stale when both its lastLogonTimestamp and its machine password are older than -InactiveDays. Domain controllers and recently created accounts are always skipped. Every computer gets a row in the CSV log, whether it was removed, disabled, skipped or failed. Supports -WhatIf. .PARAMETER ComputerName Computer names to consider. Accepts pipeline input, so Get-Content .\list.txt | works. .PARAMETER SearchBase Instead of a list, consider every computer under this OU. .PARAMETER InactiveDays How long a computer must have been quiet to count as stale. Default: 90. .PARAMETER DisableOnly Disable the stale accounts instead of deleting them. .PARAMETER LogPath CSV log path. Defaults to stale-computers-.csv in the current folder. .PARAMETER Server Domain or domain controller to use. .EXAMPLE Get-Content .\old-pcs.txt | .\Remove-StaleADComputer.ps1 -WhatIf .EXAMPLE .\Remove-StaleADComputer.ps1 -SearchBase 'OU=Workstations,DC=contoso,DC=com' -InactiveDays 120 -DisableOnly #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'List')] param( [Parameter(Mandatory, ParameterSetName = 'List', ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('Name')][AllowEmptyString()][string[]]$ComputerName, [Parameter(Mandatory, ParameterSetName = 'OU')][string]$SearchBase, [ValidateRange(30, 3650)][int]$InactiveDays = 90, [switch]$DisableOnly, [string]$LogPath = (Join-Path (Get-Location) ('stale-computers-{0:yyyyMMdd-HHmm}.csv' -f (Get-Date))), [string]$Server ) begin { Import-Module ActiveDirectory -ErrorAction Stop $ad = @{ ErrorAction = 'Stop' } if ($Server) { $ad.Server = $Server } $props = 'lastLogonTimestamp', 'PasswordLastSet', 'whenCreated', 'userAccountControl', 'OperatingSystem' $cutoff = (Get-Date).AddDays(-$InactiveDays) $log = [System.Collections.Generic.List[object]]::new() $action = if ($DisableOnly) { 'Disable' } else { 'Delete' } function Add-LogRow($Name, $Computer, $Result, $Detail) { $lastLogon = if ($Computer.lastLogonTimestamp) { [datetime]::FromFileTime($Computer.lastLogonTimestamp) } else { $null } $row = [pscustomobject]@{ Time = (Get-Date).ToString('s') Name = $Name LastLogon = $lastLogon PasswordLastSet = $Computer.PasswordLastSet OperatingSystem = $Computer.OperatingSystem Action = $action Result = $Result Detail = $Detail DistinguishedName = $Computer.DistinguishedName } $log.Add($row) $row } function Invoke-Cleanup($Computer, [string]$Name) { $lastLogon = if ($Computer.lastLogonTimestamp) { [datetime]::FromFileTime($Computer.lastLogonTimestamp) } else { $null } # 8192 = SERVER_TRUST_ACCOUNT: this is a domain controller. Never touch those here. if ($Computer.userAccountControl -band 8192) { return Add-LogRow $Name $Computer 'Skipped' 'Domain controller' } if ($Computer.whenCreated -gt $cutoff) { return Add-LogRow $Name $Computer 'Skipped' "Created $($Computer.whenCreated.ToString('d')), too new to judge" } if ($lastLogon -and $lastLogon -gt $cutoff) { return Add-LogRow $Name $Computer 'Skipped' "Active: last logon $($lastLogon.ToString('d'))" } if ($Computer.PasswordLastSet -gt $cutoff) { return Add-LogRow $Name $Computer 'Skipped' "Active: machine password changed $($Computer.PasswordLastSet.ToString('d'))" } $why = if ($lastLogon) { "No logon since $($lastLogon.ToString('d'))" } else { 'Never logged on' } if (-not $PSCmdlet.ShouldProcess("$Name ($why)", "$action computer account")) { return Add-LogRow $Name $Computer 'WhatIf' $why } try { if ($DisableOnly) { Disable-ADAccount -Identity $Computer.DistinguishedName -Confirm:$false @ad } else { # -Recursive because computers can have child objects (BitLocker keys, Hyper-V, printers) # that make a plain Remove-ADComputer fail with "the object is not a leaf". Remove-ADObject -Identity $Computer.DistinguishedName -Recursive -Confirm:$false @ad } Add-LogRow $Name $Computer 'Done' $why } catch { Add-LogRow $Name $Computer 'Failed' $_.Exception.Message } } } process { if ($PSCmdlet.ParameterSetName -eq 'OU') { foreach ($pc in Get-ADComputer -Filter * -SearchBase $SearchBase -Properties $props @ad) { Invoke-Cleanup $pc $pc.Name } return } foreach ($name in $ComputerName) { $name = $name.Trim() if (-not $name) { continue } try { $pc = Get-ADComputer -Filter "Name -eq '$($name -replace "'", "''")'" -Properties $props @ad } catch { Add-LogRow $name $null 'Failed' $_.Exception.Message; continue } if (-not $pc) { Add-LogRow $name $null 'Skipped' 'Not found in AD'; continue } Invoke-Cleanup $pc $name } } end { $log | Export-Csv -Path $LogPath -NoTypeInformation -WhatIf:$false -Confirm:$false $summary = $log | Group-Object Result | ForEach-Object { "$($_.Count) $($_.Name)" } Write-Host ("{0}. Log saved to {1}" -f ($summary -join ', '), $LogPath) }