Public/activedirectory/Get-ADStaleComputer.ps1

#Requires -Version 5.1

function Get-ADStaleComputer {
    <#
    .SYNOPSIS
        Finds Active Directory computer accounts that have been inactive for a specified number of days
 
    .DESCRIPTION
        Scans Active Directory for computer accounts that have not authenticated within
        the specified number of days. Computers that have never logged in are included
        by default. Returns operating system details alongside staleness information
        to help identify obsolete machines. Results are sorted by days since last logon
        in descending order.
 
    .PARAMETER DaysInactive
        The number of days of inactivity to use as the threshold. Computer accounts with a
        last logon date older than this value will be returned. Defaults to 90 days.
        Valid range is 1 to 3650.
 
    .PARAMETER SearchBase
        The distinguished name of the OU to search within. If omitted, searches the entire domain.
 
    .PARAMETER IncludeDisabled
        When specified, includes disabled computer accounts in the results. By default only
        enabled accounts are returned.
 
    .PARAMETER Server
        Specifies the Active Directory Domain Services instance to connect to.
 
    .PARAMETER Credential
        Specifies the credentials to use for the Active Directory query.
 
    .EXAMPLE
        Get-ADStaleComputer
 
        Finds all enabled computer accounts inactive for more than 90 days (default).
 
    .EXAMPLE
        Get-ADStaleComputer -DaysInactive 180 -Server 'dc01.contoso.com'
 
        Finds stale computers from a specific domain controller using a 180-day threshold.
 
    .EXAMPLE
        Get-ADStaleComputer -DaysInactive 60 -IncludeDisabled -SearchBase 'OU=Workstations,DC=contoso,DC=com'
 
        Finds stale computers including disabled ones within a specific OU.
 
    .OUTPUTS
        PSWinOps.ADStaleComputer
        Returns objects with computer identity, operating system information, last logon
        date, and days since last logon sorted by most stale first.
 
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-04-04
        Requires: PowerShell 5.1+ / Windows only
        Requires: ActiveDirectory module (RSAT)
 
    .LINK
        https://github.com/k9fr4n/PSWinOps
 
    .LINK
        https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-adcomputer
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateRange(1, 3650)]
        [int]$DaysInactive = 90,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$SearchBase,

        [Parameter()]
        [switch]$IncludeDisabled,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Server,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]$Credential
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting — threshold: $DaysInactive days"

        try {
            Import-Module -Name 'ActiveDirectory' -ErrorAction Stop -Verbose:$false
        }
        catch {
            Write-Error -Message "[$($MyInvocation.MyCommand)] ActiveDirectory module is not available: $_"
            return
        }

        $adSplat = @{}
        if ($PSBoundParameters.ContainsKey('Server')) {
            $adSplat['Server'] = $Server
        }
        if ($PSBoundParameters.ContainsKey('Credential')) {
            $adSplat['Credential'] = $Credential
        }

        $cutoffDate = (Get-Date).AddDays(-$DaysInactive)

        $adProperties = @(
            'LastLogonDate'
            'PasswordLastSet'
            'WhenCreated'
            'Enabled'
            'Description'
            'OperatingSystem'
            'OperatingSystemVersion'
            'IPv4Address'
            'DistinguishedName'
        )
    }

    process {
        $searchSplat = @{
            Filter      = if ($IncludeDisabled) { '*' } else { "Enabled -eq `$true" }
            Properties  = $adProperties
            ErrorAction = 'Stop'
        }
        if ($PSBoundParameters.ContainsKey('SearchBase')) {
            $searchSplat['SearchBase'] = $SearchBase
        }

        try {
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying computer accounts"
            $computers = Get-ADComputer @searchSplat @adSplat
        }
        catch {
            Write-Error -Message "[$($MyInvocation.MyCommand)] Failed to query computers: $_"
            return
        }

        if (-not $computers) {
            Write-Warning -Message "[$($MyInvocation.MyCommand)] No computer accounts found"
            return
        }

        $now = Get-Date
        $queryTimestamp = $now.ToString('o')
        $results = [System.Collections.Generic.List[PSCustomObject]]::new()

        foreach ($computer in $computers) {
            $isStale = ($null -eq $computer.LastLogonDate) -or ($computer.LastLogonDate -lt $cutoffDate)
            if (-not $isStale) { continue }

            $daysSinceLogon = if ($computer.LastLogonDate) {
                [math]::Round(($now - $computer.LastLogonDate).TotalDays)
            }
            else { $null }

            $daysSincePasswordSet = if ($computer.PasswordLastSet) {
                [math]::Round(($now - $computer.PasswordLastSet).TotalDays)
            }
            else { $null }

            $results.Add([PSCustomObject]@{
                PSTypeName            = 'PSWinOps.ADStaleComputer'
                Name                  = $computer.Name
                SamAccountName        = $computer.SamAccountName
                Enabled               = $computer.Enabled
                OperatingSystem       = $computer.OperatingSystem
                OperatingSystemVersion = $computer.OperatingSystemVersion
                IPv4Address           = $computer.IPv4Address
                LastLogonDate         = $computer.LastLogonDate
                DaysSinceLogon        = $daysSinceLogon
                PasswordLastSet       = $computer.PasswordLastSet
                DaysSincePasswordSet  = $daysSincePasswordSet
                WhenCreated           = $computer.WhenCreated
                Description           = $computer.Description
                DistinguishedName     = $computer.DistinguishedName
                Timestamp             = $queryTimestamp
            })
        }

        $results | Sort-Object -Property @{
            Expression = {
                if ($null -eq $_.DaysSinceLogon) { [int]::MaxValue } else { $_.DaysSinceLogon }
            }
            Descending = $true
        }

        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed — $($results.Count) stale computer(s) found"
    }
}