Stale User Accounts

Many organizations have problems with proper provision and de-provision of users’ accounts in Active Directory. Most likely the proper provision and de-provision procedures are not being established or automated, or simply not being followed. As a consequence, these organizations have inactive or dormant Active Directory accounts which can serve as a gateway for attackers.

It is very important to perform regular audits of Active Directory in order to identify stale user accounts. These accounts need to be managed to ensure that Active Directory security is maintained. With this on mind, many AD administrators have a 60-day inactive policy, which flags user accounts for quarantine (moving into OU for stale accounts), disablement, and finally deletion if these accounts have been inactive for 60 days.

With Windows PowerShell and the Microsoft Active Directory (AD) module, the task of identifying and moving (or deleting) these accounts is an easy one. In this blog I will present a PowerShell script that I use to either disable and move inactive user accounts or remove disabled user accounts.

The PowerShell Script

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

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

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

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

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

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

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

Import-Module ActiveDirectory

# Edit these lines according to your requirement
$usersOU = "OU=Users,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
$stringErrorUserLog = "$strFolder\Error-StaleAccountsLog-"
$stringErrorUserLog += [string]$strDate + ".log"
# Excel Report for Disabled Stale Accounts
$reportDisabledUserAccounts = "$strFolder\Report-DisabledUsers-"
$reportDisabledUserAccounts += [string]$strDate + ".xls"
# Excel Report for Removed Stale Accounts
$reportRemovedUserAccounts = "$strFolder\Report-RemovedUsers-"
$reportRemovedUserAccounts += [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) = "Account" 
            $finalWorkSheet.Cells.Item(1,3) = "Enabled" 
            $finalWorkSheet.Cells.Item(1,4) = "Action" 
            $finalWorkSheet.Cells.Item(1,5) = "Created" 
            $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) = "Account" 
            $finalWorkSheet.Cells.Item(1,3) = "Enabled" 
            $finalWorkSheet.Cells.Item(1,4) = "Action"  
            $finalWorkSheet.Cells.Item(1,5) = "LastLogonDate"
            $finalWorkSheet.Cells.Item(1,6) = "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-UserAccounts($numberOfDaysInactive, $move, $ouName){
$DaysInactive = $numberOfDaysInactive  
$time = (Get-Date).Adddays(-($DaysInactive))

$listOfUsers = (Get-ADUser -Filter {LastLogonTimeStamp -lt $time -and Enabled -eq $true} `
 -SearchBase $usersOU -Properties * | 
Sort LastLogonDate )

# Document inactive status of AD User Accounts
Function Document-InactiveUserAccounts($userCollection){
$RowNum = 2
foreach($user In $userCollection){
#Name	Account	Enabled	Action	Created	LastLogonDate	MovedToOU	DN-Name
$finalWorkSheet.Cells.Item($RowNum,1) = $user.Name
$finalWorkSheet.Cells.Item($RowNum,2) = $user.sAMAccountName 
$finalWorkSheet.Cells.Item($RowNum,3) = $user.Enabled 
$finalWorkSheet.Cells.Item($RowNum,4) = "" 
$finalWorkSheet.Cells.Item($RowNum,5) = $user.Created 
$finalWorkSheet.Cells.Item($RowNum,6) = $user.LastLogonDate
$finalWorkSheet.Cells.Item($RowNum,7) = "" 
$finalWorkSheet.Cells.Item($RowNum,8) = """$($user.DistinguishedName)""" 
$RowNum++
}

} # End Document-InactiveUserAccounts function

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

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

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

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

        }
    catch{
        Write-Host "Error while moving user: $($userMove)" -ForegroundColor Red
        "$($userMove)- Error: $($_.Exception.GetType().FullName) - Message: $($_.Exception.Message)" |
        Out-File -FilePath $stringErrorUserLog -Encoding ascii -Append
        }
}
$action = "DISABLE"
Open-ExcelFile -fileNameOpen $reportDisabledUserAccounts $action
Document-InactiveUserAccounts $listOfUsers
foreach($userDN In $listOfUsers){
    DisableAndUpdateDescription -userName $userDN.DistinguishedName
    If($move){
       MoveInactiveUserAccounts -OU $ouName -userMove $userDN.DistinguishedName
    }
}
Close-ExcelFile -fileNameClose $reportDisabledUserAccounts

} #End function Disable-UserAccounts

Function Remove-UserAccounts($numberOfDaysDisabled){

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

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

# Document inactive status of AD User Accounts
Function Document-DisabledUserAccounts($userCollection){
$RowNum = 2
foreach($user In $userCollection){
#Name Account Enabled Action LastLogonDate DN-Name
$finalWorkSheet.Cells.Item($RowNum,1) = $user.Name
$finalWorkSheet.Cells.Item($RowNum,2) = $user.sAMAccountName 
$finalWorkSheet.Cells.Item($RowNum,3) = $user.Enabled 
$finalWorkSheet.Cells.Item($RowNum,4) = "" 
$finalWorkSheet.Cells.Item($RowNum,5) = $user.LastLogonDate
$finalWorkSheet.Cells.Item($RowNum,6) = """$($user.DistinguishedName)""" 
$RowNum++
}

}# End Document-DisabledUserAccounts function

# Remove disabled AD User Accounts
Function Remove-DisabledUserAccounts($userRemove){
    try{
        $user = Get-ADUser -Identity $userRemove -Properties *
        If($user.msExchHomeServerName){
            If((Get-Mailbox -Identity $userRemove |Select-Object -Property IsMailboxEnabled).IsMailboxEnabled){
                
                try{                
                    Remove-Mailbox -identity $userRemove -Permanent $true -WhatIf
                    Write-Host "Removing user: $($userRemove) Mailbox" -ForegroundColor Green
                }
                catch{
                     Write-Host "Error while removing user: $($userRemove) Mailbox" -ForegroundColor Red
                    "$($userRemove)- Error: $($_.Exception.GetType().FullName) - Message: $($_.Exception.Message)" |
                    Out-File -FilePath $stringErrorUserLog -Encoding ascii -Append
                }
              }
        } 
        Remove-ADUser -Identity $userRemove -ErrorAction Stop -WhatIf # -Confirm:$false
        Write-Host "Removing user: $($userRemove)" -ForegroundColor Green
                $RowNum = 2
            While ($finalWorkSheet.Cells.Item($RowNum,6).Text -ne "") {
                $excelUserName = $finalWorkSheet.Cells.Item($RowNum,6).Text
                if($excelUserName -match "$value"){
			        $finalWorkSheet.Cells.Item($RowNum,4) = "Removed"
	              }    
                $RowNum++
             }
        }
    catch{
         Write-Host "Error while removing user: $($userRemove)" -ForegroundColor Red
        "$($userRemove)- Error: $($_.Exception.GetType().FullName) - Message: $($_.Exception.Message)" |
         Out-File -FilePath $stringErrorUserLog -Encoding ascii -Append
         }

}
$action = "REMOVE"
Open-ExcelFile -fileNameOpen $reportRemovedUserAccounts $action
Document-DisabledUserAccounts $listOfDisabledUsers
foreach($userDN In $listOfDisabledUsers){
    Remove-DisabledUserAccounts -userRemove $userDN.DistinguishedName
}
Close-ExcelFile -fileNameClose $reportRemovedUserAccounts

} #End function Remove-UserAccounts

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

    switch ($PsCmdlet.ParameterSetName) 
    { 
    "DisableUser" { Disable-UserAccounts $disableForInactiveDays $move $ouName } 
    "RemoveUser"  { Remove-UserAccounts $removeForDisabledDays } 
    }

As you can see from the script’s parameters section, it 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-StaleUserAccounts.ps1 -disableForInactiveDays 60 -move -ouName “Stale-Accounts” –WhatIf

Disable inactive accounts and optionally quarantine

First let’s see what this script is looking for if one wants just to disable inactive accounts: it looks for an attribute called LastLogonTimeStamp, which is replicated between domain controllers every 9 to 14 days. The AD module also displays this attribute in an easy-to-read format called LastLogonDate. If the variable named $numberOfDaysInactive is set to 60, the script will search specified OU and the result will be a collection of all inactive user accounts that have not being logged-on for at least 60 days. See the section of the script below:

$DaysInactive = $numberOfDaysInactive
$time = (Get-Date).Adddays(-($DaysInactive))
$listOfUsers = (Get-ADUser -Filter {LastLogonTimeStamp -lt $time -and Enabled -eq $true} `
-SearchBase $usersOU -Properties * |
Sort LastLogonDate )

Once the collection of all inactive user accounts 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 inactive users 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.

DisabledAndMoved

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

One can change the script and set the Description attribute with the date when the accounts were disabled and use the following line of code:

$user.Description += ” – Disabled: ((get-date).toshortdatestring())”

Some Active Directory administrators prefer to move all of disabled user accounts to a designated OU, and have them disabled for X number of days before they delete them. If the script has to move stale accounts you will have to type two more parameters: [switch]$move and [string]$ouName, for example:

DisableOrRemove-StaleUserAccounts.ps1 -disableForInactiveDays 60 -move -ouName “Stale-Accounts”

During the execution of the script the Write-Host cmdlet writes messages to the Windows PowerShell console about all disabled and moved user accounts. See picture below.

DisableAndMoveUsers

Remove inactive accounts

You will probably modify this script to reflect the requirements of your Domain and OU structure. Most likely you will change the number of days for a user account to be considered stale, but once you have all the stale accounts disabled, you will have to delete some of them. In my example, the script uses the Remove-ADUser cmdlet to delete the accounts, and the number of days is set to 10 (in my test AD environment). The following two pictures show the messages written to the Windows PowerShell console:

RemoveUsers

And the Excel file being created as a result of removing disabled accounts:

RemovedAccounts

As you will notice, there is an error while removing an account named: KRBTGT (please note this is my test AD and this test is done with a purpose of producing an error), and the Error Log file displays the source of this error: cannot perform this operation on built-in accounts.

LogFile-error

In addition to removing disabled user accounts, this script will remove users’ mailboxes. You will need a computer that has the Exchange management tools installed, or you can use Windows PowerShell on your local computer to create a remote Shell session to an Exchange server.

$cred = Get-Credential -Credential $user
$session = New-PSSession -ConfigurationName Microsoft.Exchange `
-ConnectionUri http://$computername/powershell -Credential $cred
Import-PSSession $session

As with the rest of the scripts from this site, you can download this one, named:  DisableOrRemove-StaleUserAccounts.ps1 form the download section, under PowerShell folder.





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.

Leave a Reply

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