Stale Computer Accounts

As noted in the blog about stale user accounts, it is very important to perform regular audits of Active Directory in order to identify stale computer accounts. There are three attributes in Active Directory that could be used to identify whether a computer account is stale: pwdLastSet, lastLogon, and lastLogonTimeStamp. Many administrators define as a starting threshold for stale computer accounts a time period that is 3 times the maximum computer password age (3 x 30 days). An account is stale if all of the attributes are over a defined threshold.

What are the three attributes?

The PwdLastSet attribute is the date and time that the password for an account was last changed. The machine account password change is initiated by a computer every 30 days by default. It is important to note that a computer account passwords as such do not expire in Active Directory. They are exempted from the domain’s password policy, and its (machine account password) changes are driven by the CLIENT (computer), and not the AD.

The LastLogonTimeStamp is the replicated version of the LastLogon attribute. This attribute is not updated every time a user or computer logs on to the domain. The decision to update the value is based on the current date minus the value of the ms-DS-Logon-Time-Sync-Interval attribute minus a random percentage of 5.

See the following blog if you wish to know how it works:
Walkthrough of a LastLogonTimeStamp update:
1. (Assuming the value of the ms-DS-Logon-Time-Sync-Interval is at the default of 14)
2. User logs on to the domain
3. The lastLogontimeStamp attribute value of the user is retrieved
4. 14 – (Random percentage of 5) = X
5. Current date – value of lastLogontimeStamp = Y
6. X ≤ Y – update lastLognTimeStamp
7. X > Y – do not update lastLogontimeStamp

The LastLogonDate is a converted version of the LastLogontimestamp attribute. It’s a locally calculated value of the LastLogontimestamp attribute value replicated in AD.

With Windows PowerShell and the Microsoft Active Directory (AD) module, the task of identifying and moving (or deleting) the stale computer accounts is an easy one. In the following sections I will present a PowerShell script that I use to either disable and move inactive computer accounts or remove disabled computer accounts. The script will filter the stale accounts based on the computer’s PasswordLastSet and LastLogonDate attributes:

$DaysInactive = 90  
$time = (Get-Date).Adddays(-($DaysInactive))
Get-ADComputer -Filter {PasswordLastSet -lt $time -and LastLogonDate -lt $time -and Enabled -eq $true})

The PowerShell Script

This script first creates an Excel Report about the computer accounts to be either disabled/moved or removed, and then continues to disable/move or remove stale accounts depending on the submitted parameters.

Here is the script named: DisableOrRemove-StaleComputerAccounts.ps1.

<# 
    Name:   DisableOrRemove-StaleComputerAccounts.ps1
    Author: Alex Dujakovic
    Date:   November 2014
    Description: Disables inactive computer accounts in Active Directory
                 Removes disabled computer accounts in Active Directory
    Examples: 

    DisableOrRemove-StaleComputerAccounts.ps1 -disableForInactiveDays 90 -WhatIf
    DisableOrRemove-StaleComputerAccounts.ps1 -disableForInactiveDays 90 -move -ouName "Stale-Accounts"
    DisableOrRemove-StaleComputerAccounts.ps1 -removeForDisabledDays 180 -WhatIf
    DisableOrRemove-StaleComputerAccounts.ps1 -removeForDisabledDays 180
    
    Requires - Version 3.0 
 #> 
            
[CmdletBinding(SupportsShouldProcess=$True,DefaultParametersetName="DisableComputer")]
 
param( 
[Parameter(ParameterSetName="DisableComputer", Mandatory=$True, Position=0)] 
[int]$disableForInactiveDays = 90, 

[Parameter(ParameterSetName="DisableComputer", Position=1)] 
[switch]$move, 

[Parameter(ParameterSetName="DisableComputer", Position=2)] 
[string]$ouName, 

[Parameter(ParameterSetName="RemoveComputer", Mandatory=$True, Position=0)] 
[int]$removeForDisabledDays = 180
)

Import-Module ActiveDirectory

# Edit these lines according to your requirement
$computersOU = "OU=Computers,OU=AlexFirstLocation,DC=ALEXTEST,DC=LOCAL"
$staleAccountsOU = "OU=Stale-Accounts,OU=AlexFirstLocation,DC=ALEXTEST,DC=LOCAL"
$strFolder = "C:\Test"
# -------------------------------------------------------------------------

If(!(Test-Path $strFolder)){
    Write-Host "Path for log files does not exist!" -ForegroundColor Red
    Return
    }
# Date format
$strDate = Get-Date -format "yyyy-MM-dd"
# Error Log for Stale Accounts
$stringErrorComputerLog = "$strFolder\Error-StaleAccountsLog-"
$stringErrorComputerLog += [string]$strDate + ".log"
# Excel Report for Disabled Stale Accounts
$reportDisabledComputerAccounts = "$strFolder\Report-DisabledComputers-"
$reportDisabledComputerAccounts += [string]$strDate + ".xls"
# Excel Report for Removed Stale Accounts
$reportRemovedComputerAccounts = "$strFolder\Report-RemovedComputers-"
$reportRemovedComputerAccounts += [string]$strDate + ".xls"

# -------------------------|Functions|------------------------------------

Function Open-ExcelFile($fileNameOpen,$action){
$Script:objExcel = New-Object -ComObject Excel.Application
$objExcel.Visible = $True
    if (Test-Path $fileNameOpen){ 
        # Open excel file 
        $Script:finalWorkBook = $objExcel.WorkBooks.Open($fileNameOpen)
        $Script:finalWorkSheet = $finalWorkBook.Worksheets.Item(1) 
    }
    else { 
        # Create excel file
        $Script:finalWorkBook = $objExcel.Workbooks.Add()
        $Script:finalWorkSheet = $finalWorkBook.Worksheets.Item(1)

    switch($action){
        "DISABLE"  {
            $finalWorkSheet.Cells.Item(1,1) = "Name" 
            $finalWorkSheet.Cells.Item(1,2) = "OperatingSystem" 
            $finalWorkSheet.Cells.Item(1,3) = "Enabled" 
            $finalWorkSheet.Cells.Item(1,4) = "Action" 
            $finalWorkSheet.Cells.Item(1,5) = "PasswordLastSet" 
            $finalWorkSheet.Cells.Item(1,6) = "LastLogonDate"
            $finalWorkSheet.Cells.Item(1,7) = "MovedToOU" 
            $finalWorkSheet.Cells.Item(1,8) = "DN-Name"           
        }
        "REMOVE"{
            $finalWorkSheet.Cells.Item(1,1) = "Name" 
            $finalWorkSheet.Cells.Item(1,2) = "Enabled" 
            $finalWorkSheet.Cells.Item(1,3) = "Action"  
            $finalWorkSheet.Cells.Item(1,4) = "LastLogonDate"
            $finalWorkSheet.Cells.Item(1,5) = "DN-Name"        
        }
        } # End switch

        $Script:range = $finalWorkSheet.UsedRange 
        $range.Interior.ColorIndex = 16 
        $range.Font.ColorIndex = 11 
        $range.Font.Bold = $True  
        $range.EntireColumn.AutoFit()
    }

} # End Open-ExcelFile function

Function Close-ExcelFile($fileNameClose){
    
    Function Release-Ref ($ref) { 
    ([System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$ref) -gt 0) 
    [System.GC]::Collect() 
    [System.GC]::WaitForPendingFinalizers() 
    } 

    if (Test-Path $fileNameClose){
        # Save excel file
        $range.EntireColumn.AutoFit()
        $finalWorkBook.Save()
        $objExcel.Quit()
        $close = Release-Ref($finalWorkBook) 
        $close = Release-Ref($objExcel)
    }
    else{
        # Save As excel file
        $range.EntireColumn.AutoFit()
        $finalWorkBook.SaveAs($fileNameClose)
        $objExcel.Quit()
        $close = Release-Ref($finalWorkBook) 
        $close = Release-Ref($objExcel)
    } 
} # End Close-ExcelFile function

Function Disable-ComputerAccounts($numberOfDaysInactive, $move, $ouName){
$DaysInactive = $numberOfDaysInactive  
$time = (Get-Date).Adddays(-($DaysInactive))

$listOfComputers = (Get-ADComputer -Filter {PasswordLastSet -lt $time -and `
 LastLogonDate -lt $time -and Enabled -eq $true} -SearchBase $computersOU -Properties * | 
Sort LastLogonDate )

# Document inactive status of AD Computer Accounts
Function Document-InactiveComputerAccounts($computerCollection){
$RowNum = 2
foreach($computer In $computerCollection){
#Name OperatingSystem Enabled Action PasswordLastSet LastLogonDate	MovedToOU	DN-Name
$finalWorkSheet.Cells.Item($RowNum,1) = $computer.Name
$finalWorkSheet.Cells.Item($RowNum,2) = $computer.OperatingSystem 
$finalWorkSheet.Cells.Item($RowNum,3) = $computer.Enabled 
$finalWorkSheet.Cells.Item($RowNum,4) = "" 
$finalWorkSheet.Cells.Item($RowNum,5) = $computer.PasswordLastSet 
$finalWorkSheet.Cells.Item($RowNum,6) = $computer.LastLogonDate
$finalWorkSheet.Cells.Item($RowNum,7) = "" 
$finalWorkSheet.Cells.Item($RowNum,8) = """$($computer.DistinguishedName)""" 
$RowNum++
}

} # End Document-InactiveComputerAccounts function

# Disable and update description field in AD
Function DisableAndUpdateDescription($computerName){
    Try{
        $computer = Get-ADComputer -Identity $computerName -Properties *
        $computer.Description += " - Disabled $numberOfDaysInactive Days Inactive"
        $computer.Enabled = $False

        Set-ADComputer -Identity $computer.DistinguishedName -Description $computer.Description `
        -Enabled $computer.Enabled  -ErrorAction Stop
        Write-Host "Disabling computer: $($computerName)" -ForegroundColor Green
            $RowNum = 2
        While ($finalWorkSheet.Cells.Item($RowNum,8).Text -ne "") {
            $excelComputerName = $finalWorkSheet.Cells.Item($RowNum,8).Text
            if ($excelComputerName -match "$value"){
			    $finalWorkSheet.Cells.Item($RowNum,4) = "Disabled"
	        }    
            $RowNum++
          }    
        }
    Catch{
        Write-Host "Error while disabling computer: $($computerName)" -ForegroundColor Red
        "$($computer.Name)- Error: $($_.Exception.GetType().FullName) - Message: $($_.Exception.Message)" |
        Out-File -FilePath $stringErrorComputerLog -Encoding ascii -Append
        }
}

# Move disabled AD Accounts
Function MoveInactiveComputerAccounts($OU,$computerMove){
    try{
        Move-ADObject -Identity $computerMove -TargetPath (Get-ADOrganizationalUnit -LDAPFilter "(Name=$OU)")
        Write-Host "Moving computer: $($computerMove)" -ForegroundColor Green
        
            $RowNum = 2

        While ($finalWorkSheet.Cells.Item($RowNum,8).Text -ne "") {
            $excelComputerName = $finalWorkSheet.Cells.Item($RowNum,8).Text
            if ($excelComputerName  -match "$value"){
			        $finalWorkSheet.Cells.Item($RowNum,7) = "$OU"
	            }    
            $RowNum++
          }

        }
    catch{
        Write-Host "Error while moving computer: $($computerMove)" -ForegroundColor Red
        "$($computerMove)- Error: $($_.Exception.GetType().FullName) - Message: $($_.Exception.Message)" |
        Out-File -FilePath $stringErrorComputerLog -Encoding ascii -Append
        }
}
$action = "DISABLE"
Open-ExcelFile -fileNameOpen $reportDisabledComputerAccounts $action
Document-InactiveComputerAccounts $listOfComputers
foreach($computerDN In $listOfComputers){
    DisableAndUpdateDescription -computerName $computerDN.DistinguishedName
    If($move){
       MoveInactiveComputerAccounts -OU $ouName -computerMove $computerDN.DistinguishedName
    }
}
Close-ExcelFile -fileNameClose $reportDisabledComputerAccounts

} #End function Disable-ComputerAccounts

Function Remove-ComputerAccounts($numberOfDaysDisabled){

$DaysDisabled = $numberOfDaysDisabled  
$time = (Get-Date).Adddays(-($DaysDisabled))

$listOfDisabledComputers = (Search-ADAccount -AccountDisabled -ComputersOnly -SearchBase $staleAccountsOU | 
Select-Object Name, Enabled, LastLogonDate, DistinguishedName | 
Where {$_.LastLogonDate -lt $time} |
Sort LastLogonDate )

# Document disabled status of AD Computer Accounts
Function Document-DisabledComputerAccounts($computerCollection){
$RowNum = 2
foreach($computer In $computerCollection){
# Name Enabled Action LastLogonDate DN-Name
$finalWorkSheet.Cells.Item($RowNum,1) = $computer.Name
$finalWorkSheet.Cells.Item($RowNum,2) = $computer.Enabled 
$finalWorkSheet.Cells.Item($RowNum,3) = "" 
$finalWorkSheet.Cells.Item($RowNum,4) = $computer.LastLogonDate
$finalWorkSheet.Cells.Item($RowNum,5) = """$($computer.DistinguishedName)""" 
$RowNum++
}

}# End Document-DisabledComputerAccounts function

# Remove disabled AD Computer Accounts
Function Remove-DisabledComputerAccounts($computerRemove){
    try{
        Remove-ADComputer -Identity $computerRemove -ErrorAction Stop -Confirm:$false
        Write-Host "Removing computer: $($computerRemove)" -ForegroundColor Green
                $RowNum = 2
            While ($finalWorkSheet.Cells.Item($RowNum,5).Text -ne "") {
                $excelComputerName = $finalWorkSheet.Cells.Item($RowNum,5).Text
                if($excelComputerName -match "$value"){
			        $finalWorkSheet.Cells.Item($RowNum,3) = "Removed"
	              }    
                $RowNum++
             }
        }
    catch{
         Write-Host "Error while removing computer: $($computerRemove)" -ForegroundColor Red
        "$($computerRemove)- Error: $($_.Exception.GetType().FullName) - Message: $($_.Exception.Message)" |
         Out-File -FilePath $stringErrorComputerLog -Encoding ascii -Append
         }

}
$action = "REMOVE"
Open-ExcelFile -fileNameOpen $reportRemovedComputerAccounts $action
Document-DisabledComputerAccounts $listOfDisabledComputers
foreach($computerDN In $listOfDisabledComputers){
    Remove-DisabledComputerAccounts -computerRemove $computerDN.DistinguishedName
}
Close-ExcelFile -fileNameClose $reportRemovedComputerAccounts

} #End function Remove-ComputerAccounts

# ---------------------------|End Of Functions|----------------------------------

    switch ($PsCmdlet.ParameterSetName) 
    { 
    "DisableComputer" { Disable-ComputerAccounts $disableForInactiveDays $move $ouName } 
    "RemoveComputer"  { Remove-ComputerAccounts $removeForDisabledDays } 
    }

The script implements the WhatIf parameter by setting SupportsShouldProcess to true inside the parentheses of the cmdletbinding attribute. One should always run this script for the first time as shown in this example:

DisableOrRemove-StaleComputerAccounts.ps1 -disableForInactiveDays 90 -move -ouName “Stale-Accounts” –WhatIf

Disable inactive accounts and optionally quarantine

The default option is to disable computer accounts and this script is looking for three attributes: PasswordLastSet, LastLogonDate, and Enabled. See the section of the script below:

$DaysInactive = $numberOfDaysInactive  
$time = (Get-Date).Adddays(-($DaysInactive))
$computersOU = "OU=Computers,OU=AlexFirstLocation,DC=ALEXTEST,DC=LOCAL" 
$listOfComputers = (Get-ADComputer -Filter {PasswordLastSet -lt $time -and `
 LastLogonDate -lt $time -and Enabled -eq $true} -SearchBase $computersOU -Properties * | Sort LastLogonDate ) 

Once the collection of all inactive computers has been created, the script, depending on the parameters submitted will first create an Excel File to document this collection, disable accounts, and optionally move stale accounts into the specified OU. For all the accounts being disabled and / or moved, the script will update the appropriate columns in the Excel file, see the picture below.

Disable-MoveComputerAccounts

To produce the above report in my test environment I’ve ran this script as follows:

DisableOrRemove-StaleComputerAccounts.ps1 -disableForInactiveDays 90 -move -ouName “Stale-Accounts”

Please note that the description attribute of all disabled accounts will be updated by adding suffix: $computer.Description += ” – Disabled $numberOfDaysInactive Days Inactive”

Remove disabled accounts

If you want to remove disabled computer accounts, the script will look into the stale accounts container and filter out only the computers that are disabled and with LastLogonDate attribute older that the specified number of days. See the section of the script below:

DaysDisabled = $numberOfDaysDisabled  
$time = (Get-Date).Adddays(-($DaysDisabled))
$listOfDisabledComputers = (Search-ADAccount -AccountDisabled -ComputersOnly -SearchBase $staleAccountsOU | 
Select-Object Name, Enabled, LastLogonDate, DistinguishedName | 
Where {$_.LastLogonDate -lt $time} |
Sort LastLogonDate )

An Excel file will be created and for all the accounts being removed, this script will update the appropriate column in the Excel file, see the picture below.

RemoveComputerAccounts

To produce this report and remove computers, I’ve type the following line:

DisableOrRemove-StaleComputerAccounts.ps1 -removeForDisabledDays 90

As with the rest of the scripts, you can download this script form the download section, under PowerShell folder / DisableOrRemove-StaleAccounts.





Please note: Although the author has made every reasonable attempt to achieve complete accuracy of the content, he assumes no responsibility for errors or omissions. Also, you should use this information as you see fit, and at your own risk.

One thought on “Stale Computer Accounts

  1. Porter

    Having read this I thought it was very informative. I appreciate you spending some time and energy to put this information together.
    I once again find myself personally spending a lot of time both reading and commenting.
    But so what, it was still worth it!

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *