Active Directory

Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster

Have you ever looked at your Active Directory and wondered, “Why do I still have computers listed that haven't been turned on since World Cup 2016?” Yeah, we've all been there. Keeping AD clean and up-to-date is like trying to organize your garage—it’s easy to put off until it becomes a total mess.

That’s where my PowerShell module, CleanupMonster, comes to the rescue. This little powerhouse is designed to help you effortlessly track down and deal with those old, stale computers cluttering your directory. Whether you want to move, disable, or even give them the boot, CleanupMonster has your back. And it doesn’t stop there—if you need to gather some intel from Entra ID (Azure AD), Intune, or Jamf to make an informed decision, CleanupMonster can handle that, too.

In this post, I'll explain how CleanupMonster can help you clean up your Active Directory without breaking a sweat. Let’s dive in and give your AD the spring cleaning it deserves!

Cleanup your Active Directory

The CleanupMonster PowerShell module was created as an easy way to remove stale computer objects from the Active Directory. However, each company and enterprise has rules regarding what happens to computers and how they should be handled. Then, there is a question of how to tell the computer that it is a stale object. While there are many solutions on the internet, if you look hard enough, CleanupMonster does this in a way that nothing comes close!

There are several ways to determine when a computer in Active Directory becomes inactive. However, the most common methods are LastLogonDate and PasswordLastSet, which should ideally not exceed 30 days. Some people might advise you to use simple PowerShell commands to locate and delete these inactive computers – it can be done in just five lines of code!

Get-ADComputer -filter * -Proper Name, DisplayName, SamAccountName, lastlogondate, PasswordLastSet | ft Name, DisplayName, SamAccountName, lastlogondate, PasswordLastSet

It's true that if you are lucky enough to have a small organization with only a few attributes to consider, you can make informed decisions and maintain a clean Active Directory without using CleanupMonster. However, things get more complicated for larger companies or enterprises. In these cases, there are VPN solutions, employees who haven't connected to the network for months, and those who work on Mac devices that may not get updated regularly. What if a computer object was just prepared for onboarding, but the cleanup script mistakenly identified it as inactive and deleted it? There are many potential issues, which can lead to employees opening tickets and reporting that they can't work anymore.

This is where CleanupMonster comes in. It's a PowerShell module with a single command called Invoke-ADComputersCleanup. Its multiple parameters allow it to be configured in many ways, meeting the most advanced scenarios you can imagine.

Features & Options

CleanupMonster has the following features:

  • Ability to disable, disable and move, move and disable, move or delete computers
  • All five actions from above can have different rules when a given task happens
  • It's able to check when the object was created and prevent the deletion of objects younger than X days
  • It's able to check LastLogonDate and LastPasswordSet and requires it to be certain days old to consider for disabling, moving, or delete
  • If LastLogonDate or LastPasswordSet is empty, it is treated as never used; therefore, it applies the maximum number of days to it.
  • It can check Intune data for LastLogonDate for a given computer, providing the ability to improve data with cloud data for those non-connected machines.
  • It can check Entra ID data (Azure AD) for LastLogonDate for a given computer to improve the assessment of deletion.
  • It's able to check Jamf PRO for LastLogonDate for macOS devices.
  • You can target whole forest, or include/exclude specific domains from this process

This means you can configure it to only disable, move, or delete an object if LastLogonDate is over 90 days, LastPasswordDate is over 90 days, LastLogonDate in Azure AD is over 90 days, LastLogonDate in Intune is over 90 days, and LastLogonDate is over 90 days in Jamf PRO, and additional that the object was created over 90 days ago. This can be further enhanced to action an object based on OperatingSystem (include, exclude) and exclude certain Organizational Units or specific objects wholly based on their Name or DistinguishedName. All of this is configurable with just an option on the cmdlet. Finally, there's one more option called the Pending Deletion list. The pending deletion list keeps track of disabled/moved objects to know how long ago they were disabled/moved so that computer objects can be deleted. This means you can configure CleanupMonster to turn off an object but tell it to wait 90 days before deleting it. If the object gets reenabled during that time, it's automatically removed from the pending list, and the time timer starts again.

If that wasn't enough the module has:

  • Built-in HTML reporting keeping track of what was actioned, what will be actioned, what is on pending deletion list, including logs from each action
  • It's able to write to Event Log when the computer is actioned so it can be tracked in SIEM
  • It has builtin DisableLimit, DeleteLimit that allow only X number of objects actioned per run, preventing abuse or accidental deletions.
  • It provides Safety limits such as SafetyADLimit,SafetyAzureADLimit, SafetyIntuneLimit, SafetyJamfLimit that the script requires certain number of objects to be gathred before continuing. This is to prevent decision making process based on incomplete data because something went wrong during gathering data phase (lack of internet, certain server being down etc).
  • Provides WhatIf functionality to Disabling, Move or Deleting together or separately
  • Full logging of all actions to file

Additionally, with just a few lines of code, you can send daily results to email using PSWriteHTML functionality.

Example configuration #1

I don't want to bore you too much with text, so let's jump into how some of the configurations look like:

# Run the script
$Configuration = @{
    Disable                        = $true
    DisableNoServicePrincipalName  = $null
    DisableIsEnabled               = $true
    DisableLastLogonDateMoreThan   = 90
    DisablePasswordLastSetMoreThan = 90
    DisableExcludeSystems          = @(
        # 'Windows Server*'
    )
    DisableIncludeSystems          = @()
    DisableLimit                   = 2 # 0 means unlimited, ignored for reports
    DisableModifyDescription       = $false
    DisableModifyAdminDescription  = $true

    Delete                         = $true
    DeleteIsEnabled                = $false
    DeleteNoServicePrincipalName   = $null
    DeleteLastLogonDateMoreThan    = 180
    DeletePasswordLastSetMoreThan  = 180
    DeleteListProcessedMoreThan    = 90 # 90 days since computer was added to list
    DeleteExcludeSystems           = @(
        # 'Windows Server*'
    )
    DeleteIncludeSystems           = @(

    )
    DeleteLimit                    = 2 # 0 means unlimited, ignored for reports

    Exclusions                     = @(
        '*OU=Domain Controllers*'
        '*OU=Servers,OU=Production*'
        'EVOMONSTER$'
        'EVOMONSTER.AD.EVOTEC.XYZ'
    )

    Filter                         = '*'
    WhatIfDisable                  = $true
    WhatIfDelete                   = $true
    LogPath                        = "$PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log"
    DataStorePath                  = "$PSScriptRoot\DeleteComputers_ListProcessed.xml"
    ReportPath                     = "$PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html"
    ShowHTML                       = $true
}

# Run one time as admin: Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 0 -Message 'Initialize' -Source 'CleanupComputers'
$Output = Invoke-ADComputersCleanup @Configuration
$Output

When run the script will disable object only if it matches last logon date over 90 days and last password date over 90 days and the object will be enabled. It will also disable maximum of 2 objects and modify admin description of the object with information about details of this action. However, to delete an object, it will require 180 days for the last logon and last password date and be on the processing list for at least 90 days. It will also delete only two objects per run. It won't touch objects in Domain Controllers OU, in Servers OU and it will ignore EVOMonster device. Since both WhatIfDelete and WhatIfDisable are enabled it will not do any real action and it will instead just display what it would do, doing no harm to AD. Finally it will generate HTML report to given path and display it in your favourite browser.

Example configuration #2

Of course, we can make a more complicated example which, as part of disabling, also moves the computer. You can choose the order DisableAndMove or MoveAndDisable depending on which permissions you have granted for your GMSA account. You can also define the path per domain where the computers are moved when required. There's also the ability to treat moving as a separate process: Disable/Move/Delete, rather than doing Disable & Move / Delete. As you can see in the example below, some commented-out settings show you unused but available options.

# connect to graph for Email sending
Connect-MgGraph -Scopes Mail.Send -NoWelcome

$invokeADComputersCleanupSplat = @{
    #ExcludeDomains                      = 'ad.evotec.xyz'
    # safety limits (minimum amount of computers that has to be returned from each source)
    SafetyADLimit                       = 30
    #SafetyAzureADLimit                  = 5
    #SafetyIntuneLimit                   = 3
    #SafetyJamfLimit                     = 50
    # disable settings
    Disable                             = $true
    DisableAndMove                      = $true
    DisableAndMoveOrder                 = 'DisableAndMove' # DisableAndMove, MoveAndDisable
    #DisableIsEnabled                    = $true
    DisableLimit                        = 1
    DisableLastLogonDateMoreThan        = 90
    DisablePasswordLastSetMoreThan      = 90
    #DisableLastSeenAzureMoreThan        = 90
    DisableRequireWhenCreatedMoreThan       = 90

    DisablePasswordLastSetOlderThan     = Get-Date -Year 2023 -Month 1 -Day 1
    #DisableLastSyncAzureMoreThan   = 90
    #DisableLastContactJamfMoreThan = 90
    #DisableLastSeenIntuneMoreThan       = 90
    DisableMoveTargetOrganizationalUnit = @{
        'ad.evotec.xyz' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz'
        'ad.evotec.pl'  = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=pl'
    }

    # move settings
    Move                                = $false
    MoveLimit                           = 1
    MoveLastLogonDateMoreThan           = 90
    MovePasswordLastSetMoreThan         = 90
    #MoveLastSeenAzureMoreThan    = 180
    #MoveLastSyncAzureMoreThan    = 180
    #MoveLastContactJamfMoreThan  = 180
    #MoveLastSeenIntuneMoreThan   = 180
    #MoveListProcessedMoreThan    = 90 # disabled computer has to spend 90 days in list before it can be deleted
    MoveIsEnabled                       = $false # Computer has to be disabled to be moved
    MoveTargetOrganizationalUnit        = @{
        'ad.evotec.xyz' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz'
        'ad.evotec.pl'  = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=pl'
    }

    # delete settings
    Delete                              = $false
    DeleteLimit                         = 2
    DeleteLastLogonDateMoreThan         = 180
    DeletePasswordLastSetMoreThan       = 180
    #DeleteLastSeenAzureMoreThan         = 180
    #DeleteLastSyncAzureMoreThan    = 180
    #DeleteLastContactJamfMoreThan  = 180
    #DeleteLastSeenIntuneMoreThan   = 180
    #DeleteListProcessedMoreThan    = 90 # disabled computer has to spend 90 days in list before it can be deleted
    DeleteIsEnabled                     = $false # Computer has to be disabled to be deleted
    # global exclusions
    Exclusions                          = @(
        '*OU=Domain Controllers*' # exclude Domain Controllers
    )
    # filter for AD search
    Filter                              = '*'
    # logs, reports and datastores
    LogPath                             = "$PSScriptRoot\Logs\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log"
    DataStorePath                       = "$PSScriptRoot\CleanupComputers_ListProcessed.xml"
    ReportPath                          = "$PSScriptRoot\Reports\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html"
    # WhatIf settings
    ReportOnly                          = $false
    WhatIfDisable                       = $true
    WhatIfMove                          = $true
    WhatIfDelete                        = $true
    ShowHTML                            = $true

    DontWriteToEventLog                 = $true
}

$Output = Invoke-ADComputersCleanup @invokeADComputersCleanupSplat

# Now lets send email using Graph
[Array] $DisabledObjects = $Output.CurrentRun | Where-Object { $_.Action -eq 'Disable' }
[Array] $DeletedObjects = $Output.CurrentRun | Where-Object { $_.Action -eq 'Delete' }

$EmailBody = EmailBody -EmailBody {
    EmailText -Text "Hello,"

    EmailText -LineBreak

    EmailText -Text "This is an automated email from Automations run on ", $Env:COMPUTERNAME, " on ", (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), " by ", $Env:UserName -Color None, Green, None, Green, None, Green -FontWeight normal, bold, normal, bold, normal, bold

    EmailText -LineBreak

    EmailText -Text "Following is a summary for the computer object cleanup:" -FontWeight bold
    EmailList {
        EmailListItem -Text "Objects actioned: ", $Output.CurrentRun.Count -Color None, Green -FontWeight normal, bold
        EmailListItem -Text "Objects deleted: ", $DeletedObjects.Count -Color None, Salmon -FontWeight normal, bold
        EmailListItem -Text "Objects disabled: ", $DisabledObjects.Count -Color None, Orange -FontWeight normal, bold
    }

    EmailText -Text "Following objects were actioned:" -LineBreak -FontWeight bold -Color Salmon
    EmailTable -DataTable $Output.CurrentRun -HideFooter {
        New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackGroundColor PinkLace -Inline
        New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackGroundColor EnergyYellow -Inline
        New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackGroundColor LightGreen -Inline
        New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackGroundColor Salmon -Inline
        New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackGroundColor LightBlue -Inline
    }

    EmailText -LineBreak

    EmailText -Text "Regards,"
    EmailText -Text "Automations Team" -FontWeight bold
}

# send email using Mailozaurr
Send-EmailMessage -To 'przemyslaw.klys@test.pl' -From 'przemyslaw.klys@test.pl' -MgGraphRequest -Subject "Automated Computer Cleanup Report" -Body $EmailBody -Priority Low -Verbose -WhatIf

At the bottom of it, you can see how I'm using PSWriteHTML along with the Mailozaurr module, which, based on the module's output, sends me nicely formatted emails.

Fancy reporting & logging

One of the essential parts of any good solution is knowing what happens when it happens and having some option to go back in time and assess the situation. CleanupMonster provides an advanced reporting engine that allows you to view the following:

  • What happened during the run time? Which objects were touched, and what parameters they had
  • Devices history showing the history of script actions over time to look back 1-2-10 months back
  • Devices are on the pending list for deletion, so you can see how many objects are waiting to be purged according to the rules you've used.
  • All devices have a full assessment based on the provided rules, allowing you to assess how many devices require deletion and are still suitable.

All reports are visually attractive, so IT Directors/Managers can have a single view of all computer objects.

Every action the script takes or has taken in the past is accessible and can be used as evidence.

Installing or updating PowerShell module

CleanupMonster is available in PowerShellGallery as an easy-to-use download.

Install-Module CleanupMonster -Force -Verbose

If you need Jamf Pro functionality, you also need to install

Install-Module PowerJamf -Force -Verbose

If you need Azure AD (Entra ID) and Intune functionality, you also need to install

Install-Module GraphEssentials -Force -Verbose

Once the module is installed, you can adjust it to your needs. Please remember to use WhatIfDisable, WhatIfMove, WhatIfDelete, or WhatIf (which is pretendable for everyone). Also, make sure that DeleteLimit, DisableLimit, and MoveLimit start small 1-2-10 objects for the first time so you can quickly recover from the wrong configuration and go from there. Finally, Safety* properties help you ensure any timeouts when getting data won't ruin your AD. Make sure to use them carefully!

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Upgrade Azure Active Directory Connect fails with unexpected error

Today, I made the decision to upgrade my test environment and update the version of…

2 miesiące ago

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

7 miesięcy ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

12 miesięcy ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

1 rok ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

1 rok ago

How to Efficiently Remove Comments from Your PowerShell Script

As part of my daily development, I create lots of code that I subsequently comment…

1 rok ago