← Back to all posts

Published: February 12, 2023

Find and Disable Stale Workstations in AD - Ensure Inactivity with ConfigMgr

As organizations grow and their IT footprint expands, it becomes important to regularly clean up inactive devices to keep the environment organized. Let’s try to automate that!

In this blog post, we will discuss how to use PowerShell to find stale Active Directory computers that haven’t updated their LastLogonTimeStamp in for over x days (for example: 90 days) and cross-reference them with ConfigMgr to ensure they are actually inactive and not connected through a cloud management gateway while outside the corporate network or VPN.

We don’t want to inadvertently disable a PC that is in use remotely.

Example PowerShell Script to Find and Move Stale Workstations

Below is a sample PowerShell script that I quickly put together for this blog post. In my org, we separate Laptops and Desktop between two OUs. The script is not perfect, so please use as inspiration and make it suitable for your environment.

Import-Module ActiveDirectory
Import-Module ConfigurationManager

# Establish how far back to look for stale PCs
$Days = "90"
$Threshold = (Get-Date).AddDays($("-" + $days))

# Disabled OUs
$LaptopDisabledOu = "OU=Laptops,OU=Disabled,OU=Computers,DC=FAKEORG,DC=com"
$DesktopDisabledOu = "OU=Desktops,OU=Disabled,OU=Computers,DC=FAKEORG,DC=com"

$ReportFile = "C:\InactivePCs\InactivePCReport.csv"

# List of OUs in AD to look for workstations
$OUs = @('OU=Desktops,OU=Computers,DC=FAKEORG,DC=com',
         'OU=Laptops,OU=Computers,DC=FAKEORG,DC=com',
         'OU=Desktops,OU=IT Computers,DC=FAKEORG,DC=com',
         'OU=Laptops,OU=IT Computers,DC=FAKEORG,DC=com')

# Gather stale workstation accounts in AD
$computers = @()
foreach ($OU in $OUs) {
    $computers += Get-ADComputer -Properties LastLogonTimeStamp -Filter {(LastLogonTimeStamp -lt $Threshold) -and (OperatingSystem -like "Windows 10*") -and (Enabled -eq $True)} -SearchBase $OU |
                  Where-Object {$_.LastLogonDate -lt $threshold}
}

# Connect to Configmgr
$SiteCode = "PS1"
if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {New-PSDrive -Name $SiteCode -PSProvider CMSite -Root "ConFigMgr.FAKEORG.com"}
Set-Location "$($SiteCode):\"

# Check if the Inactive Computers from AD are also inactive in ConfigMgr
$results = @()
foreach ($computer in $computers) {
    $device = $null
    $device = Get-CMDevice -Name $computer.Name
    if ($device){$status = $device.clientactivestatus}else{$status = ""}
    if ($status -eq '0'){$status = ""}elseif($status -eq "1"){$status = "Active"}
        $results += [PSCustomObject]@{
            ComputerName = $computer.Name
            LastLogonTimeStamp = $computer.LastLogonTimeStamp
            Active = $status
        }
}

# Record results in csv file
$results | sort active -Descending | select computername, @{N='lastlogontimestamp'; E={[DateTime]::FromFileTime($_.lastlogontimestamp)}}, Active | Export-Csv -Path $ReportFile -NoTypeInformation

# Move and Disable Workstations
foreach ($pc in $results) {
    if ($pc.Active -ne "Active"){

        # Get computer Account in AD
        $Workstation = get-adcomputer $pc.ComputerName -Properties *

        # Get the current OU of the computer object
        $currentOU = $Workstation.DistinguishedName

        # Determine if the current OU contains the word "laptop" or "desktop"
        if ($currentOU -like "*laptop*") {

            # Move the computer to the Laptop OU
            $Workstation | Move-ADObject -TargetPath $LaptopDisabledOu

            # Disable-ADAccount
            $Workstation | Set-ADComputer -Description "Disabled date: $(get-date -Format "MM/dd/yyy")"
            Disable-ADAccount -Identity $Workstation
        }
        elseif ($currentOU -like "*desktop*") {

            # Move the computer to the Desktop OU
            $Workstation | Move-ADObject -TargetPath $DesktopDisabledOu

            # Disable-ADAccount
            $Workstation | Set-ADComputer -Description "Disabled date: $(get-date -Format "MM/dd/yyy")"
            Disable-ADAccount -Identity $Workstation
        }
        else {
          Write-Error "The current OU does not contain the word 'laptop' or 'desktop'"
          Write-Error "Can't disable PC:"$pc.computername" OU:$currentOU"
        }
    }else{
        Write-Host $pc.ComputerName is active and will be skipped
        }
}

The script above does the following at a high level:

# Establish how far back to look for stale PCs
$Days = "90"
$Threshold = (Get-Date).AddDays($("-" + $days))
# Disabled OUs
$LaptopDisabledOu = "OU=Laptops,OU=Disabled,OU=Computers,DC=FAKEORG,DC=com"
$DesktopDisabledOu = "OU=Desktops,OU=Disabled,OU=Computers,DC=FAKEORG,DC=com"
$ReportFile = "C:\InactivePCs\InactivePCReport.csv"
# List of OUs in AD to look for workstations
$OUs = @('OU=Desktops,OU=Computers,DC=FAKEORG,DC=com',
         'OU=Laptops,OU=Computers,DC=FAKEORG,DC=com',
         'OU=Desktops,OU=IT Computers,DC=FAKEORG,DC=com',
         'OU=Laptops,OU=IT Computers,DC=FAKEORG,DC=com')
# Gather stale workstation accounts in AD
$computers = @()
foreach ($OU in $OUs) {
    $computers += Get-ADComputer -Properties LastLogonTimeStamp -Filter {(LastLogonTimeStamp -lt $Threshold) -and (OperatingSystem -like "Windows 10*") -and (Enabled -eq $True)} -SearchBase $OU |
                  Where-Object {$_.LastLogonDate -lt $threshold}
}
# Connect to Configmgr
$SiteCode = "PS1"
if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {New-PSDrive -Name $SiteCode -PSProvider CMSite -Root "ConFigMgr.FAKEORG.com"}
Set-Location "$($SiteCode):\"

# Check if the Inactive Computers from AD are also inactive in ConfigMgr
$results = @()
foreach ($computer in $computers) {
    $device = $null
    $device = Get-CMDevice -Name $computer.Name
    if ($device){$status = $device.clientactivestatus}else{$status = ""}
    if ($status -eq '0'){$status = ""}elseif($status -eq "1"){$status = "Active"}
        $results += [PSCustomObject]@{
            ComputerName = $computer.Name
            LastLogonTimeStamp = $computer.LastLogonTimeStamp
            Active = $status
        }
}
# Move and Disable Workstations
foreach ($pc in $results) {
    if ($pc.Active -ne "Active"){

        # Get computer Account in AD
        $Workstation = get-adcomputer $pc.ComputerName -Properties *

        # Get the current OU of the computer object
        $currentOU = $Workstation.DistinguishedName

        # Determine if the current OU contains the word "laptop" or "desktop"
        if ($currentOU -like "*laptop*") {

            # Move the computer to the Laptop OU
            $Workstation | Move-ADObject -TargetPath $LaptopDisabledOu

            # Disable-ADAccount
            $Workstation | Set-ADComputer -Description "Disabled date: $(get-date -Format "MM/dd/yyy")"
            Disable-ADAccount -Identity $Workstation
        }
        elseif ($currentOU -like "*desktop*") {

            # Move the computer to the Desktop OU
            $Workstation | Move-ADObject -TargetPath $DesktopDisabledOu

            # Disable-ADAccount
            $Workstation | Set-ADComputer -Description "Disabled date: $(get-date -Format "MM/dd/yyy")"
            Disable-ADAccount -Identity $Workstation
        }
        else {
          Write-Error "The current OU does not contain the word 'laptop' or 'desktop'"
          Write-Error "Can't disable PC:"$pc.computername" OU:$currentOU"
        }
    }else{
        Write-Host $pc.ComputerName is active and will be skipped
        }
}

After setting up a similar script to run on a schedule, you’d want to create another script to delete those AD records after x days. This could likely be safe to be around 30 days in most cases.