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:
- Set the number of days back in AD for computer accounts that have not updated their LastLogonTimeStamp. The example script sets the threshold date to 90 days ago.
# Establish how far back to look for stale PCs
$Days = "90"
$Threshold = (Get-Date).AddDays($("-" + $days))
- Define the Disabled OUs for laptops and desktops in AD. The script will move disabled computers to these OUs towards the end if they meet the criteria.
# Disabled OUs
$LaptopDisabledOu = "OU=Laptops,OU=Disabled,OU=Computers,DC=FAKEORG,DC=com"
$DesktopDisabledOu = "OU=Desktops,OU=Disabled,OU=Computers,DC=FAKEORG,DC=com"
- Define the location for the report file in which the script will store its results (for reporting purposes only)
$ReportFile = "C:\InactivePCs\InactivePCReport.csv"
- Define an array of OUs in AD to look for production workstations.
# 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 using the LastLogonTimestamp via the Get-ADComputer cmdlet. The script gathers all the Windows 10 computers in the OUs that have a last logon time stamp older than the threshold date.
# 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 and check if the inactive computers from AD are also inactive in ConfigMgr. The script gets the device information for each computer in ConfigMgr and checks the client active status.
- The thought here is, if a PC connects over Cloud Management Gateway (CMG), then it’s still in use. It’s just remote and not on the corporate network. That’s a separate potential issue for your org to deal with. If you manage PCs that are AzureAD Only Joined, (instead of Hybrid), you won’t have this problem.
# 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. The script loops through the results and moves inactive computers to the appropriate disabled OU and disables the computer account in AD. It will also put a disabled date in the description of the AD object for that PC for reporting or future automations to delete the AD record.
# 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.