Trevor Sullivan's Tech Room

Minding the gap between administration and development

PowerShell: Clean up AD Computer Accounts

Posted by Trevor Sullivan on 2009/09/19


Update (2009-11-03): I have posted a newer version of this script. Please visit this link for information.

—————

I recently wrote a script to clean up workstation accounts in our Active Directory domain. It’s not perfect, but it was a good learning experience, as I found out there are some gritty details when working with Integer8 values. I plan on adding more features in the future, including logging results of the script to an Excel document, for easy readability. If you have any suggestions, please send them my way; I may or may not have time to implement them, but am always open to constructive feedback.

I would suggest stepping through the code using a tool like PowerGUI so you can understand exactly what is happening. I also take no responsibility for your use of this code (aka. use at your own risk). This script automatically disables and deletes computer accounts in Active Directory! It is not necessarily suitable for all Active Directory configurations, depending on how your GPOs are set up, and so on, so please ensure that you understand its purpose prior to executing it.

Note: This script leverages the Description attribute of your computer accounts in AD, so it can be overwritten!

The way the script works is basically this:

1. You define an OU where disabled computer accounts will be moved to ($DisabledDn)

2. Stage 1: Script attempts to evaluate when computer accounts in the “Disabled OU” were disabled (from the description attribute — see Stage 2)

3. If the parsed disable date is greater than 30 days, it will delete it, otherwise it will leave it alone

4. Stage 2: Script evaluates pwdlastset attribute of workstation accounts in the domain

5. If pwdlastset is greater than 60 days, it will: 1) disable the account, 2) write the current date to the description attribute, and 3) move it to the disabled OU

Note: The first time you execute the script, the Stage 1 area will pretty much do nothing, since no workstations have been populated into the Disabled OU yet. Once the script begins disabling accounts, they will then be evaluated in Stage 1 each time the script is run. It’s kind of  backwards way of thinking about it, so it may not make sense right off the bat, but don’t think about it too much.

Note 2: The Stage 1 and Stage 2 verbage I used above are not identified in the script anywhere. Stage 1 correlates to the DeleteDisabledAccounts function, and Stage 2 correlates to the DisableOldAccounts function.

—————

#
# Author: Trevor Sullivan
#
# Lessons learned:
#
# 1. When using a DirectorySearcher object, property names are lower case
# 2. Must explicitly cast 64-bit integers from AD
#

${DisabledDn} = ‘ou=Disabled,ou=Workstations,dc=mycompany,dc=com’

function DisableOldAccounts(${TargetDn}, ${DisableAge} = 60)
{
${Computers} = GetComputerList ${TargetDn}

foreach (${Computer} in ${Computers})
{
# PwdLastSet is a 64-bit integer that indicates the number of 100-nanosecond intervals since 12:00 AM January 1st, 1601
# The FromFileTime method converts a 64-bit integer to datetime
# http://www.rlmueller.net/Integer8Attributes.htm
$PwdLastSet = [DateTime]::FromFileTime([Int64]”$(${Computer}.Properties[‘pwdlastset’])”)
${CompAge} = ([DateTime]::Now – $PwdLastSet).Days
if (${CompAge} -gt ${DisableAge})
{
LogMessage “$($Computer.Properties[‘cn’]) age is ${CompAge}. Account will be disabled” 2
DisableAccount $Computer.Properties[‘distinguishedname’]
}
else
{
LogMessage “$($Computer.Properties[‘cn’]) age is ${CompAge}, $($Computer.Properties[‘pwdlastset’]), ${PwdLastSet}” 1
}
}
}

function GetComputerList($TargetDn)
{
${tFilter} = ‘(&(objectClass=computer)(|(operatingSystem=Windows 2000 Professional)(operatingSystem=Windows XP*)(operatingSystem=Windows 7*)(operatingSystem=Windows Vista*))’
${Searcher} = New-Object System.DirectoryServices.DirectorySearcher $tFilter
${Searcher}.SearchRoot = “LDAP://${TargetDn}”
${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
# Page size default in Active Directory is 1000
${Searcher}.PageSize = 1000
$Results = $Searcher.FindAll()
LogMessage “Found $($Results.Count) computer accounts to evaluate for disablement” 1
return $Results
}

# Set description on computer account, disable it, and move it to the Disabled OU
function DisableAccount($dn)
{
# LogMessage “DisableAccount method called with param: ${dn}” 1
# Get a reference to the object at <distinguishedName>
$comp = [adsi]”LDAP://${dn}”
# Disable the account
# LogMessage “userAccountControl ($($comp.Name)) is: $($comp.userAccountControl)”
$comp.userAccountControl = $comp.userAccountControl.Value -bxor 2
# Write the current date to the description field
$comp.Description = “$(([DateTime]::Now).ToShortDateString())”

# UNCOMMENT THESE LINES
[Void] $comp.SetInfo()
$comp.psbase.MoveTo(“LDAP://${DisabledDn}”)
}

# Parameter ($DeleteAge): Days from disable date to delete computer account
function DeleteDisabledAccounts($DeleteAge)
{
# Get reference to OU for disabled workstation accounts
${DisabledOu} = [adsi]”LDAP://${DisabledDn}”

${Searcher} = New-Object System.DirectoryServices.DirectorySearcher ‘(objectClass=computer)’
${Searcher}.SearchRoot = $DisabledOu
${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
# Page size is used to return result count > default size limit on domain controllers. See: http://geekswithblogs.net/mnf/archive/2005/12/20/63581.aspx
${Searcher}.PageSize = 1000
LogMessage “Finding computers to evaluate for deletion in container: ${DisabledDn}” 1
${Computers} = ${Searcher}.FindAll()

foreach (${Computer} in ${Computers})
{
${DisableDate} = [DateTime]::Parse(${Computer}.Properties[‘description’])
trap {
LogMessage “Couldn’t parse date for $($Computer.Properties[‘cn’])” 3
continue
}
${CurrentAge} = ([DateTime]::Now – ${DisableDate}).Days
if (${CurrentAge} -gt ${DeleteAge})
{
LogMessage “$(${Computer}.Properties[‘cn’]) age is ${CurrentAge} and will be deleted” 2
#   $DisabledOu.Delete(‘computer’, ‘CN=’ + ${Computer}.Properties[‘cn’])
}
else
{
LogMessage “$(${Computer}.Properties[‘cn’]) age is ${CurrentAge} and will not be deleted” 1
}
}
}

function LogMessage(${tMessage}, ${Severity})
{
switch(${Severity})
{
1 {
$LogPrefix = “INFO”
$fgcolor = [ConsoleColor]::Blue
$bgcolor = [ConsoleColor]::White
}
2 {
$LogPrefix = “WARNING”
$fgcolor = [ConsoleColor]::Black
$bgcolor = [ConsoleColor]::Yellow
}
3 {
$LogPrefix = “ERROR”
$fgcolor = [ConsoleColor]::Yellow
$bgcolor = [ConsoleColor]::Red
}
default {
$LogPrefix = “DEFAULT”
$fgcolor = [ConsoleColor]::Black
$bgcolor = [ConsoleColor]::White
}
}

Add-Content -Path “AD-Workstation-Cleanup.log” -Value “$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}”
Write-Host -ForegroundColor $fgcolor -BackgroundColor $bgcolor -Object “$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}”
}

function Main()
{
Clear-Host

LogMessage “Beginning workstation account cleanup script” 1

# Start by deleting accounts that are already disabled, if they are old enough
DeleteDisabledAccounts 30

# Disable accounts that are older than X days
DisableOldAccounts ([adsi]””).distinguishedName 60

LogMessage “Completed workstation account cleanup script” 1
}

Main

13 Responses to “PowerShell: Clean up AD Computer Accounts”

  1. Trevor, looks like a good start, but prompts some comments . We use OLDCMP in a scheduled capacity (http://www.joeware.net/freetools/tools/oldcmp/index.htm) for it’s reporting and safety features. How does your script compare from a reporting perspective in advance of disabling?

    OLDCMP provides ability to report, then disable, then later delete. As a consultant, I see many one-off scenarios (like with SANs that require a windows acct that appears stale) where an unintentional delete would be disastrous.

    Would be interested to hear how the two compare.

    • pcgeek86 said

      Pete,

      I have heard of oldcmp, but haven’t personally used it before, so I can’t speak to the comparison. I wrote the PowerShell script so that I have full control over what’s going on, and can implement reporting in the way that I want it (e-mail, Excel, etc.).

      Regarding the issue of accidental deletion, I have specified an LDAP filter that specifically targets accounts with Windows operating systems. Similar to the SANs you mentioned, we have a handful of Mac OS X devices that authenticate to Active Directory, and do not conform to the same workstation password requirements that Windows clients do. Because I used the LDAP search filter, anything that isn’t a Windows client will not be targeted. Could you do me a favor and check out these SAN accounts and tell me what value the operatingSystem attribute has in AD?

      Thanks so much for your feedback! I really appreciate it.

      -Trevor

  2. I’ll get that and post for you…do check out what OLDCMP can do…it sets the bar pretty high and if you could match that in PoSh, would be way cool.

    • pcgeek86 said

      Pete,

      I will definitely check out oldcmp and see how I can improve my PowerShell script to meet similar functionality. Do you think that logging to Excel would be appropriate? I know other formats like CSV, HTML, and so on, are a little more platform agnostic, but what do you think?

      Thanks for getting me the attribute info!

      -Trevor

  3. Jeroen Budding said

    I made some small changes to the script. Only want to Disabled pc’s from my OU.
    So i changed the script as below. Now it only doesn’t disable and move any pc.
    What did i do wrong?

    Jeroen

    ${TargetDN} = ‘OU=Baarn,OU=IPS,OU=Invensys Business Units,DC=corp,DC=com’
    ${DisabledDn} = ‘OU=Disabled,OU=Workstations,OU=Baarn,OU=IPS,OU=Invensys Business Units,DC=corp,DC=com’

    function DisableOldAccounts(${TargetDn}, ${DisableAge} = 30)
    {
    ${Computers} = GetComputerList ${TargetDn}

    foreach (${Computer} in ${Computers})
    {
    # PwdLastSet is a 64-bit integer that indicates the number of 100-nanosecond intervals since 12:00 AM January 1st, 1601
    # The FromFileTime method converts a 64-bit integer to datetime
    # http://www.rlmueller.net/Integer8Attributes.htm
    $PwdLastSet = [DateTime]::FromFileTime([Int64]“$(${Computer}.Properties[‘pwdlastset’])”)
    ${CompAge} = ([DateTime]::Now – $PwdLastSet).Days
    if (${CompAge} -gt ${DisableAge})
    {
    LogMessage “$($Computer.Properties[‘cn’]) age is ${CompAge}. Account will be disabled” 2
    DisableAccount $Computer.Properties[‘distinguishedname’]
    }
    else
    {
    LogMessage “$($Computer.Properties[‘cn’]) age is ${CompAge}, $($Computer.Properties[‘pwdlastset’]), ${PwdLastSet}” 1
    }
    }
    }

    function GetComputerList($TargetDn)
    {
    ${tFilter} = ‘(&(objectClass=computer)(|(operatingSystem=Windows 2000 Professional)(operatingSystem=Windows XP*)(operatingSystem=Windows 7*)(operatingSystem=Windows Vista*))’
    ${Searcher} = New-Object System.DirectoryServices.DirectorySearcher $tFilter
    ${Searcher}.SearchRoot = “LDAP://${TargetDn}”
    ${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
    # Page size default in Active Directory is 1000
    ${Searcher}.PageSize = 1000
    $Results = $Searcher.FindAll()
    LogMessage “Found $($Results.Count) computer accounts to evaluate for disablement” 1
    return $Results
    }

    # Set description on computer account, disable it, and move it to the Disabled OU
    function DisableAccount($dn)
    {
    # LogMessage “DisableAccount method called with param: ${dn}” 1
    # Get a reference to the object at
    $comp = [adsi]“LDAP://${dn}”
    # Disable the account
    # LogMessage “userAccountControl ($($comp.Name)) is: $($comp.userAccountControl)”
    $comp.userAccountControl = $comp.userAccountControl.Value -bxor 2
    # Write the current date to the description field
    $comp.Description = “$(([DateTime]::Now).ToShortDateString())”

    # UNCOMMENT THESE LINES
    [Void] $comp.SetInfo()
    $comp.psbase.MoveTo(“LDAP://${DisabledDn}”)
    }

    • pcgeek86 said

      Hello,

      The Main function near the bottom of the script is what’s handling the logic for disabling or deleting accounts. In your modified copy, all that’s happening is functions are getting defined, but never actually called.

      1. Copy the Main function back into your script (including the call to it at the very bottom)
      2. Delete the call to “DeleteDisableAccounts” inside Main
      3. Change this line : DisableOldAccounts ([adsi]“”).distinguishedName 60 to this: DisableOldAccounts $TargetDn 60

      Thanks for your comment!

      -Trevor

  4. Jeroen Budding said

    Hi Trevor,

    Thanks for looking at my problem only still the script doesn’t work.

    ${TargetDN} = ‘OU=Baarn,OU=IPS,OU=Invensys Business Units,DC=corp,DC=com’
    ${DisabledDn} = ‘OU=Disabled,OU=Workstations,OU=Baarn,OU=IPS,OU=Invensys Business Units,DC=corp,DC=com’

    function DisableOldAccounts(${TargetDn}, ${DisableAge} = 60)
    {
    ${Computers} = GetComputerList ${TargetDn}

    foreach (${Computer} in ${Computers})
    {
    # PwdLastSet is a 64-bit integer that indicates the number of 100-nanosecond intervals since 12:00 AM January 1st, 1601
    # The FromFileTime method converts a 64-bit integer to datetime
    # http://www.rlmueller.net/Integer8Attributes.htm
    $PwdLastSet = [DateTime]::FromFileTime([Int64]“$(${Computer}.Properties[‘pwdlastset’])”)
    ${CompAge} = ([DateTime]::Now – $PwdLastSet).Days
    if (${CompAge} -gt ${DisableAge})
    {
    LogMessage “$($Computer.Properties[‘cn’]) age is ${CompAge}. Account will be disabled” 2
    DisableAccount $Computer.Properties[‘distinguishedname’]
    }
    else
    {
    LogMessage “$($Computer.Properties[‘cn’]) age is ${CompAge}, $($Computer.Properties[‘pwdlastset’]), ${PwdLastSet}” 1
    }
    }
    }

    function GetComputerList($TargetDn)
    {
    ${tFilter} = ‘(&(objectClass=computer)(|(operatingSystem=Windows 2000 Professional)(operatingSystem=Windows XP*)(operatingSystem=Windows 7*)(operatingSystem=Windows Vista*))’
    ${Searcher} = New-Object System.DirectoryServices.DirectorySearcher $tFilter
    ${Searcher}.SearchRoot = “LDAP://${TargetDn}”
    ${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
    # Page size default in Active Directory is 1000
    ${Searcher}.PageSize = 1000
    $Results = $Searcher.FindAll()
    LogMessage “Found $($Results.Count) computer accounts to evaluate for disablement” 1
    return $Results
    }

    # Set description on computer account, disable it, and move it to the Disabled OU
    function DisableAccount($dn)
    {
    # LogMessage “DisableAccount method called with param: ${dn}” 1
    # Get a reference to the object at
    $comp = [adsi]“LDAP://${dn}”
    # Disable the account
    # LogMessage “userAccountControl ($($comp.Name)) is: $($comp.userAccountControl)”
    $comp.userAccountControl = $comp.userAccountControl.Value -bxor 2
    # Write the current date to the description field
    $comp.Description = “$(([DateTime]::Now).ToShortDateString())”

    # UNCOMMENT THESE LINES
    [Void] $comp.SetInfo()
    $comp.psbase.MoveTo(“LDAP://${DisabledDn}”)
    }
    function Main()
    {
    Clear-Host

    LogMessage “Beginning workstation account cleanup script” 1

    # Disable accounts that are older than X days
    DisableOldAccounts ${TargetDn} 60

    LogMessage “Completed workstation account cleanup script” 1
    }

    • pcgeek86 said

      It looks like you are still missing the call to the Main() function. It’s being defined at the bottom now, but you still have to call it, by saying simply:

      Main
      • Jeroen Budding said

        Runs now only i get the following error messages.

        11/6/2009 12:36:55 PM INFO: Beginning workstation account cleanup script
        11/6/2009 12:36:55 PM INFO: Found computer accounts to evaluate for disablement
        Cannot index into a null array.
        At line:1 char:24
        + ${Computer}.Properties[ <<<< 'pwdlastset']
        + CategoryInfo : InvalidOperation: (pwdlastset:String) [], RuntimeException
        + FullyQualifiedErrorId : NullArray

        Cannot index into a null array.
        At line:1 char:22
        + $Computer.Properties[ <<<< 'cn']
        + CategoryInfo : InvalidOperation: (cn:String) [], RuntimeException
        + FullyQualifiedErrorId : NullArray

        11/6/2009 12:36:55 PM WARNING: age is 149328. Account will be disabled
        Cannot index into a null array.
        At D:\INVSDATA\Powershell\Scripts\Disable Computer Account.ps1:18 char:37
        + DisableAccount $Computer.Properties[ <<<< 'distinguishedname']
        + CategoryInfo : InvalidOperation: (distinguishedname:String) [], RuntimeException
        + FullyQualifiedErrorId : NullArray

        11/6/2009 12:36:56 PM INFO: Completed workstation account cleanup script

        ${TargetDN} = 'OU=Baarn,OU=IPS,OU=Invensys Business Units,DC=corp,DC=com'
        ${DisabledDn} = ‘OU=Disabled,OU=Workstations,OU=Baarn,OU=IPS,OU=Invensys Business Units,DC=corp,DC=com’

        function DisableOldAccounts(${TargetDn}, ${DisableAge} = 90)
        {
        ${Computers} = GetComputerList ${TargetDn}

        foreach (${Computer} in ${Computers})
        {
        # PwdLastSet is a 64-bit integer that indicates the number of 100-nanosecond intervals since 12:00 AM January 1st, 1601
        # The FromFileTime method converts a 64-bit integer to datetime
        # http://www.rlmueller.net/Integer8Attributes.htm
        $PwdLastSet = [DateTime]::FromFileTime([Int64]“$(${Computer}.Properties['pwdlastset'])”)
        ${CompAge} = ([DateTime]::Now – $PwdLastSet).Days
        if (${CompAge} -gt ${DisableAge})
        {
        LogMessage “$($Computer.Properties['cn']) age is ${CompAge}. Account will be disabled” 2
        DisableAccount $Computer.Properties['distinguishedname']
        }
        else
        {
        LogMessage “$($Computer.Properties['cn']) age is ${CompAge}, $($Computer.Properties['PwdLastSet']), ${PwdLastSet}” 1
        }
        }
        }

        function GetComputerList($TargetDn)
        {
        ${tFilter} = ‘(&(objectClass=computer)(|(operatingSystem=Windows 2000 Professional)(operatingSystem=Windows XP*)(operatingSystem=Windows 7*)(operatingSystem=Windows Vista*))’
        ${Searcher} = New-Object System.DirectoryServices.DirectorySearcher $tFilter
        ${Searcher}.SearchRoot = “LDAP://${TargetDn}”
        ${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
        # Page size default in Active Directory is 1000
        ${Searcher}.PageSize = 1000
        $Results = $Searcher.FindAll()
        LogMessage “Found $($Results.Count) computer accounts to evaluate for disablement” 1
        return $Results
        }

        # Set description on computer account, disable it, and move it to the Disabled OU
        function DisableAccount($dn)
        {
        # LogMessage “DisableAccount method called with param: ${dn}” 1
        # Get a reference to the object at
        $comp = [adsi]“LDAP://${dn}”
        # Disable the account
        # LogMessage “userAccountControl ($($comp.Name)) is: $($comp.userAccountControl)”
        $comp.userAccountControl = $comp.userAccountControl.Value -bxor 2
        # Write the current date to the description field
        $comp.Description = “$(([DateTime]::Now).ToShortDateString())”

        # UNCOMMENT THESE LINES
        [Void] $comp.SetInfo()
        $comp.psbase.MoveTo(“LDAP://${DisabledDn}”)
        }

        function LogMessage(${tMessage}, ${Severity})
        {
        switch(${Severity})
        {
        1 {
        $LogPrefix = “INFO”
        $fgcolor = [ConsoleColor]::Blue
        $bgcolor = [ConsoleColor]::White
        }
        2 {
        $LogPrefix = “WARNING”
        $fgcolor = [ConsoleColor]::Black
        $bgcolor = [ConsoleColor]::Yellow
        }
        3 {
        $LogPrefix = “ERROR”
        $fgcolor = [ConsoleColor]::Yellow
        $bgcolor = [ConsoleColor]::Red
        }
        default {
        $LogPrefix = “DEFAULT”
        $fgcolor = [ConsoleColor]::Black
        $bgcolor = [ConsoleColor]::White
        }
        }

        Add-Content -Path “AD-Workstation-Cleanup.log” -Value “$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}”
        Write-Host -ForegroundColor $fgcolor -BackgroundColor $bgcolor -Object “$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}”
        }

        function Main()
        {
        Clear-Host

        LogMessage “Beginning workstation account cleanup script” 1

        # Disable accounts that are older than X days
        DisableOldAccounts ${TargetDn} 90

        LogMessage “Completed workstation account cleanup script” 1
        }

        Main

  5. Jeroen Budding said

    Hi Sorry was already in the script only forgot to copy into the script.

    Still get the following errors.

    11/6/2009 3:31:57 PM INFO: Beginning workstation account cleanup script
    11/6/2009 3:31:58 PM INFO: Found computer accounts to evaluate for disablement
    Cannot index into a null array.
    At line:1 char:24
    + ${Computer}.Properties[ <<<< 'pwdlastset']
    + CategoryInfo : InvalidOperation: (pwdlastset:String) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    Cannot index into a null array.
    At line:1 char:22
    + $Computer.Properties[ <<<< 'cn']
    + CategoryInfo : InvalidOperation: (cn:String) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    11/6/2009 3:31:58 PM WARNING: age is 149328. Account will be disabled
    Cannot index into a null array.
    At D:\INVSDATA\Powershell\Scripts\Disable Computer Account.ps1:18 char:37
    + DisableAccount $Computer.Properties[ <<<< 'distinguishedname']
    + CategoryInfo : InvalidOperation: (distinguishedname:String) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    11/6/2009 3:31:59 PM INFO: Completed workstation account cleanup script

    ${TargetDN} = 'OU=Baarn,OU=IPS,OU=Invensys Business Units,DC=corp,DC=com'
    ${DisabledDn} = ‘OU=Disabled,OU=Workstations,OU=Baarn,OU=IPS,OU=Invensys Business Units,DC=corp,DC=com’

    function DisableOldAccounts(${TargetDn}, ${DisableAge} = 90)
    {
    ${Computers} = GetComputerList ${TargetDn}

    foreach (${Computer} in ${Computers})
    {
    # PwdLastSet is a 64-bit integer that indicates the number of 100-nanosecond intervals since 12:00 AM January 1st, 1601
    # The FromFileTime method converts a 64-bit integer to datetime
    # http://www.rlmueller.net/Integer8Attributes.htm
    $PwdLastSet = [DateTime]::FromFileTime([Int64]“$(${Computer}.Properties['pwdlastset'])”)
    ${CompAge} = ([DateTime]::Now – $PwdLastSet).Days
    if (${CompAge} -gt ${DisableAge})
    {
    LogMessage “$($Computer.Properties['cn']) age is ${CompAge}. Account will be disabled” 2
    DisableAccount $Computer.Properties['distinguishedname']
    }
    else
    {
    LogMessage “$($Computer.Properties['cn']) age is ${CompAge}, $($Computer.Properties['PwdLastSet']), ${PwdLastSet}” 1
    }
    }
    }

    function GetComputerList($TargetDn)
    {
    ${tFilter} = ‘(&(objectClass=computer)(|(operatingSystem=Windows 2000 Professional)(operatingSystem=Windows XP*)(operatingSystem=Windows 7*)(operatingSystem=Windows Vista*))’
    ${Searcher} = New-Object System.DirectoryServices.DirectorySearcher $tFilter
    ${Searcher}.SearchRoot = “LDAP://${TargetDn}”
    ${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
    # Page size default in Active Directory is 1000
    ${Searcher}.PageSize = 1000
    $Results = $Searcher.FindAll()
    LogMessage “Found $($Results.Count) computer accounts to evaluate for disablement” 1
    return $Results
    }

    # Set description on computer account, disable it, and move it to the Disabled OU
    function DisableAccount($dn)
    {
    # LogMessage “DisableAccount method called with param: ${dn}” 1
    # Get a reference to the object at
    $comp = [adsi]“LDAP://${dn}”
    # Disable the account
    # LogMessage “userAccountControl ($($comp.Name)) is: $($comp.userAccountControl)”
    $comp.userAccountControl = $comp.userAccountControl.Value -bxor 2
    # Write the current date to the description field
    $comp.Description = “$(([DateTime]::Now).ToShortDateString())”

    # UNCOMMENT THESE LINES
    [Void] $comp.SetInfo()
    $comp.psbase.MoveTo(“LDAP://${DisabledDn}”)
    }

    function LogMessage(${tMessage}, ${Severity})
    {
    switch(${Severity})
    {
    1 {
    $LogPrefix = “INFO”
    $fgcolor = [ConsoleColor]::Blue
    $bgcolor = [ConsoleColor]::White
    }
    2 {
    $LogPrefix = “WARNING”
    $fgcolor = [ConsoleColor]::Black
    $bgcolor = [ConsoleColor]::Yellow
    }
    3 {
    $LogPrefix = “ERROR”
    $fgcolor = [ConsoleColor]::Yellow
    $bgcolor = [ConsoleColor]::Red
    }
    default {
    $LogPrefix = “DEFAULT”
    $fgcolor = [ConsoleColor]::Black
    $bgcolor = [ConsoleColor]::White
    }
    }

    Add-Content -Path “AD-Workstation-Cleanup.log” -Value “$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}”
    Write-Host -ForegroundColor $fgcolor -BackgroundColor $bgcolor -Object “$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}”
    }

    function Main()
    {
    Clear-Host

    LogMessage “Beginning workstation account cleanup script” 1

    # Disable accounts that are older than X days
    DisableOldAccounts ${TargetDn} 90

    LogMessage “Completed workstation account cleanup script” 1
    }

    Main

    • pcgeek86 said

      Hello,

      Can you tell me:

        Client OS / service pack
        PowerShell version
        AD forest & domain functional level
        Domain controller OS?

      I’ve only tested the script with Windows 7 Release Candidate / PowerShell v2.0 against a Windows 2003 functional level forest & domain. I would imagine that the ADSI interfaces are all the same, but it sounds like it isn’t able to retrieve the pwdlastset or cn attributes.

      Cheers,
      -Trevor

  6. Jeroen Budding said

    Hi Trevor,

    This is how everything is setup. Hopefully you can find anything otherwish is need to do it another why

    Windows XP SP3
    Powershell V2
    Native mode
    Server 2008 AD

    Thanks for the help you already gave me,

    Jeroen

    • pcgeek86 said

      Jeroen,

      Sorry for the late reply … it keeps coming back to me, then slipping my mind, to get back to you.

      Well as far as I know, it sounds like your setup is OK. You’re running the release version of PowerShell 2.0 I hope (not a CTP or RC version)? For the next troubleshooting steps, I think it would be best to get ADSI Edit (are you familiar with this tool already?) and ensure that you are able to see the Active Directory attributes on the objects that the script is attempting to read. Let me know if you need help or directions on how to do this. The other thing that concerns me is that the one line says “Found computer accounts to evaluate for disablement.” It -should- read, for example, something like “Found 502 computer accounts to evaluate for disablement.” Because that number is missing, it leads me to believe that something is going wrong in the DirectorySearcher that looks for computer accounts under the “$TargetDn”.

      Would you be willing to set up a Live Meeting session sometime so we can step through and troubleshoot what’s going on with this code? I’m curious myself to know what I may have overlooked during its development.

      Cheers,
      -Trevor

Leave a reply to pcgeek86 Cancel reply